- askNumberOwnership is now the first prompt: dedicated number (recommended),
personal number, or back to channel selection; the personal path shows an
interception screen (self-chat-only consequences + Telegram / dedicated
number / /add-whatsapp-cloud alternatives) defaulting to '← Pick a
different channel' via the existing BACK_TO_CHANNEL_SELECTION sentinel
- dedicated path keeps askChatPhone reframed around the operator's personal
number; entering the bot's own number routes through the same interception
screen and flips to shared mode
- shared path skips askChatPhone (chat number = bot number), optionally asks
for an @<AgentName>-only self-chat engage pattern passed to
init-first-agent --engage-pattern, and ends with self-chat-only completion
copy pointing at /manage-channels
- writeAssistantHasOwnNumber generalized to writeEnvVar(key, value); both
modes persist ASSISTANT_NAME (fixes the 'Andy' outbound-prefix mismatch)
and ASSISTANT_HAS_OWN_NUMBER so mode switches on re-run don't go stale;
WhatsApp option in setup:auto gains a 'best with a dedicated number' hint
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PJkDAnLGWJUqjgJNXpyqGZ
- Mark both config.ts exports @deprecated; WhatsApp adapter copies read the .env keys directly now
- Re-exports retained one release so stale adapter copies (origin/channels whatsapp.ts:42) keep compiling
- No value or readEnvFile changes
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PJkDAnLGWJUqjgJNXpyqGZ
- setup/register.ts: import the channels barrel (registration-only, no
factory invocation), require --channel explicitly, resolve wiring engage
defaults and unknown_sender_policy through the declaration helpers gated
on hasDeclaredChannelDefaults (legacy heuristic/'strict' for undeclared
adapters), add --is-group / --engage-mode / --unknown-sender-policy flags
with --trigger kept as the explicit pattern override
- scripts/init-first-agent.ts: same barrel import + helper gating for the
DM wiring and messaging-group policy; new optional --engage-pattern flag;
sender_scope/'drop' hardcodes kept as deliberate owner-bootstrap choices
- scripts/init-cli-agent.ts: mg + wiring values now come from the cli
adapter's declaration (value-identical to the old hardcodes)
- move validateEngageAgainstChannel + EngageValues from
src/cli/resources/wirings.ts to src/channels/channel-defaults.ts so ncl
and the setup register step share one mention/pattern/sticky validator
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PJkDAnLGWJUqjgJNXpyqGZ
- Factor both approve paths (connect button, new-agent name reply) into one
wireApprovedChannel helper; engage defaults now come from
resolveWiringDefaults with isGroup = event.message.isGroup ?? mg.is_group,
never threadId — the one deliberate behavior change: groups on
non-threaded platforms wire via the group default instead of pattern '.'
- Fix channel-approval adapter resolution to mg.instance ?? channel_type
(reviewer correction 5) and enrich the approval card with the resolved
engage rule ('will respond to @-mentions in this group' / 'all messages')
- sender_scope 'known' / accumulate / shared / priority 0 stay hardcoded as
deliberate card-flow choices, not channel defaults
- Tests: non-threaded group wires group default (sticky→mention through the
faithful fallback), undeclared DM stays pattern '.', both approve paths
produce identical wirings; telegram fixture declared at registration tier
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PJkDAnLGWJUqjgJNXpyqGZ
- Auto-created messaging_groups take unknown_sender_policy from the channel
declaration (DM vs group context); undeclared adapters resolve through the
behavior-faithful fallback, identical to the old hardcoded value
- Fanout computes each wiring's effectiveThreadId via resolveThreadPolicy
(threads override, declaration, live capability) and threads it through
sticky engage lookup, session-mode force, resolveSession, delivery address,
typing, and the subscribe gate; event.replyTo is operator intent and never
policy-stripped
- Capability pre-strip for non-threaded adapters stays unchanged
- Tests: NULL-threads inherit parity, threads=0 opt-out, replyTo exemption,
declared vs fallback auto-create policy; metadata-preservation tests now
register a live threaded adapter (the realistic config the router sees)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PJkDAnLGWJUqjgJNXpyqGZ
- Migration 019: nullable messaging_group_agents.threads (NULL = inherit
adapter declaration, 1/0 = per-wiring override), no backfill; schema.ts
reference DDL and MessagingGroupAgent type updated to match.
- crud.ts genericCreate restructured into two passes with a new
ResourceDef.resolveDefaults hook between explicit-arg collection and
static col.default, so static defaults never pre-empt context-aware
resolution; new preUpdate hook gives update the same cross-checks.
- wirings resource: new --threads and --priority flags, engage defaults
filled from the channel declaration ({name} substituted), validation on
create AND update (pattern-needs-pattern, mentions:'never' rejection,
sticky->mention coercion when the effective thread policy is off).
- messaging-groups resource fills unknown_sender_policy from the channel
declaration; hasDeclaredChannelDefaults gates both hooks so stale
(undeclared) adapters keep the legacy static defaults exactly.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PJkDAnLGWJUqjgJNXpyqGZ
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>