Same treatment as whatsapp-formatting (previous commit): slack-formatting
is channel payload, not trunk. It ships only a SKILL.md (no composed-doc
fragment) and reaches agents via ~/.claude/skills; /add-slack and the
slack setup installers now copy it from the channels branch, which
already carries an identical copy. Changelog entry combined for both.
BREAKING CHANGE: existing Slack installs lose the slack-formatting skill
from ~/.claude/skills after updating; re-run /add-slack (idempotent).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A2YZQDTw9TQrH3m8NBtVAW
Trunk shipped WhatsApp formatting instructions to every install — the
skill-whatsapp-formatting.md fragment landed in every agent's composed
CLAUDE.md whether or not WhatsApp was wired. The skill is channel payload:
it now lives on the channels branch and is copied in by /add-whatsapp and
the setup installers (install-whatsapp.sh, add-whatsapp.sh).
BREAKING CHANGE: existing WhatsApp installs lose the fragment on their
next container spawn after updating; re-run /add-whatsapp (idempotent)
to restore it. See CHANGELOG.md for the manual copy alternative.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A2YZQDTw9TQrH3m8NBtVAW
Storage: every JS-side datetime('now') write becomes new Date().toISOString()
(messages_out all writers, processing_ack all stamps, delivered, retry
backoff, roles/members/destinations, skill SQL via strftime ISO-Z). Dormant
deliver_after raw string compares wrapped in datetime().
Display: ncl human output renders ISO instants as local "YYYY-MM-DD HH:mm"
(host + container renderers; --json stays ISO; the stamp shape round-trips
through parseZonedToUtc). Task run logs, conversation archive filenames,
clidash, the dashboard pusher, and the uninstall backup stamp all render
local; instruction texts updated in lockstep. CLAUDE.md documents the
convention.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A2YZQDTw9TQrH3m8NBtVAW
insertTaskRow used SQL datetime('now') — a naive-UTC stamp the container
formatter parses as local time, rendering <task time> hours off from the
ISO-Z chat timestamps beside it. Stamp ISO like every other messages_in
writer.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A2YZQDTw9TQrH3m8NBtVAW
Improvised `until … do sleep; done` polling loops with no timeout can
loop forever when a page condition never becomes true (page fails to
load, selector changes, network stalls). This doesn't just fail the
command — it wedges the whole agent turn: the runner keeps the model
stream open, later messages are pushed in and silently marked complete
without a reply, and heartbeat freshness masks it from the host's
stuck-detection, so a session can hang for hours.
Add a "Waiting for a custom condition — ALWAYS bound it" section to the
agent-browser skill: prefer built-in `wait` subcommands, ban unbounded
loops, and give a ready pattern capped by both `timeout` and a
max-attempts counter that always exits. Treat a timeout as a real
failure (snapshot + report), not grounds to re-enter the wait.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Scheduled tasks move from MCP tools to a first-class ncl resource:
list / get / create / update / cancel / pause / resume / run / append-log,
with args declared on every verb (strict validation, fix-carrying errors)
and deep per-verb help with executable examples.
A series is CronJob-like: the live (pending/paused) row is the next
occurrence, completed rows are its run history. tasks list reads as a
compact run-history table, server-rendered so the container agent prints
the same aligned table. Short named ids (<slug>-<hex>) are
filesystem/thread-safe and copy-pasteable. Agents keep a per-series work
journal: the run log at tasks/<series>.md (append-log + auto-logged
final text).
Each series runs in its own isolated system session
(system:tasks:<series>); the live-task cap is dropped — isolation
replaces throttling. Spent task sessions are GC'd by the sweep.
--script on a task runs a bash gate BEFORE the agent wakes. It prints a
JSON verdict as its last stdout line: {"wakeAgent": bool, "data": {...}}.
wakeAgent:false handles the occurrence without waking the agent; data is
threaded into the fire's prompt, so a gate can fetch/inspect and hand the
agent exactly what changed. Failing scripts back off exponentially
(2·2^(n-1) min, cap 60) and auto-pause the series after 8 consecutive
failures with a host-written run-log note; manual runs are excluded from
the streak.
BREAKING: the schedule_task / list_tasks / update_task / cancel_task /
pause_task / resume_task MCP tools are removed. Agents schedule via
ncl tasks (available under cli_scope=group). Existing task rows keep
firing — storage (messages_in kind='task') is unchanged; only the
management surface moved. Migration: docs/ncl-tasks-migration.md.
Review round (gavrielc):
- Recurrence frequency guard: create/update refuse a recurrence more
frequent than 4 fires/day with a warning steering to gate scripts;
--dangerously-override-recurrence-limit bypasses after explicit user
confirmation. Counted over the next 24h in the instance TZ. Guarded on
update too, so create-slow-then-update cannot sneak past.
- ncl tasks help scripts: resource help topics (new helpTopics seam on
registerResource) carry the full gate-script guide — contract, examples,
state, failure/backoff semantics, testing directions.
- Agent-facing scheduling instructions slimmed: no recurring-frequency
prose in the fragment; one pointer to the help topic.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Every ncl verb now declares its args: strict validation with fix-carrying
error messages, generated per-verb --help, and multi-word custom-op key
resolution (spaces vs dashes). Longest-prefix command fallback keeps dashed
positional ids intact.
Responses gain a server-rendered 'human' frame field (formatHuman hook): the
host renders tables once and every client prints them verbatim — container
agents cannot import host formatters, so this is how they get aligned output
instead of a raw column dump. --help responses carry 'human' for clean
multi-line rendering.
Robustness fixes riding along:
- generic list returns newest rows first, so the LIMIT no longer hides
recent rows
- stdout flushed before exit — >64KB responses were silently truncated
Additive only: new response-frame fields, no schema changes.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Extends the existing label-pr workflow with an author allowlist that
applies a core-team label. The label is auto-provisioned on first use
so no manual repo setup is needed.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The section still described the pre-envelope model where every provider
result was written to messages_out verbatim. Since the envelope parser
landed, the agent-runner parses the final text for <message to="name">
blocks (dispatchResultText, poll-loop.ts): one messages_out row per
block, bare/<internal> text is scratchpad (logged, never sent), unknown
destinations are dropped, unwrapped output gets a one-time re-wrap
nudge, and non-retryable error results are delivered as error notices
instead of being dropped.
The claude provider mapped rate-limit events with
`message.type === 'system' && subtype === 'rate_limit_event'`, but
`@anthropic-ai/claude-agent-sdk` 0.3.x ships rate limits as a top-level
`SDKRateLimitEvent` (`{ type: 'rate_limit_event', ... }`) with no `system`
subtype. The old condition never matched, so the quota-classified error
event was never emitted and rate-limit signals were silently dropped.
Match the top-level `type` instead.