harness_capabilities JSON column (migration 019) stores sparse per-group
overrides; code defaults live in src/harness-capabilities.ts (agent-teams
off, workflow off). configFromDb materializes the RESOLVED map into
container.json. reconcileHarnessSettings converges each group's
settings.json on every spawn — manages exactly the teams env key and
disableWorkflows, preserves everything else, write-if-changed with
tmp+rename. The teams key leaves DEFAULT_SETTINGS_JSON: managed keys
enter settings.json only through the reconciler, so pre-existing groups
converge to default-off at their next spawn.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
TOOL_ALLOWLIST named five tools that don't exist on claude-code 2.1.197
(Task was renamed Agent upstream; TodoWrite, TeamCreate, TeamDelete, and
ToolSearch are gone). Remove the phantoms and correct the comment (and
the stale claim in docs/agent-runner-details.md): allowedTools is a
permission auto-approve list, not an availability filter — paired wire
captures show no allowlist effect on the offered tool surface, and this
runner's bypassPermissions moots its permission role. No wire-visible
change.
Add a wire-captured fixture (sdk-tools-baseline.json, regenerated via
dump-sdk-tools.ts inside the agent image with a zero-API-cost 401-stub
capture) and claude.tools.test.ts, which fails on any future
claude-code CLI or Agent SDK bump until the fixture is regenerated and
the lists re-verified. The SDK dep is pinned exactly (0.3.197, was
caret) so drift can only enter deliberately.
Wire measurement on this pin also shows run-to-run NONDETERMINISM in
the offered surface: conditional tools (Glob/Grep) flicker, and
disallowedTools stripping of flag-gated tools is per-query best-effort.
The fixture pairs captures within one regen run to control for this,
records a disallow probe as a diagnostic, and the dump-script header
documents the variance; the deterministic tool block remains the
runner's PreToolUse hook.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- wirings preUpdate: treat legacy pattern rows with NULL engage_pattern
as match-all so unrelated updates no longer reject; still rejects when
the pattern fields themselves change invalidly (+ test)
- resolveWiringDefaults: drop a declared \b adjacent to a non-word char
in the {name} substitution so names like 'Andy (backup)' still engage
(+ tests; mirrors selfChatEngagePattern)
- wireApprovedChannel returns whether the wiring was created; the
free-text interceptor now reports failure instead of a false success
- ncl create hooks log a one-line notice when a channel has no declared
defaults and legacy static defaults apply
- stale comments/docs reconciled: chat-sdk-bridge DM thread-policy note,
seed-discord DISCORD_DEFAULTS sync note, user-dm deliberate 'strict'
rationale, manage-channels/update-nanoclaw/init-first-agent SKILL.md
policy wording
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PJkDAnLGWJUqjgJNXpyqGZ
- restartService() ran before ASSISTANT_HAS_OWN_NUMBER / ASSISTANT_NAME
landed in .env, so the adapter (which reads both once at module load)
kept running in shared mode with the default name until an unrelated
restart
- single restart now sits after the env writes and the late
dedicated→shared flip, right before init-first-agent's welcome DM
- doc header + spinner copy updated to match the new ordering
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PJkDAnLGWJUqjgJNXpyqGZ
- api-details.md: ChannelDefaults/ChannelContextDefaults interfaces, five-tier getChannelDefaults resolution chain, creation helpers, and messaging_group_agents.threads runtime semantics
- setup-wiring.md: two-level model, shared-identity mention-suppression pattern (reusable by iMessage/Signal), and the trunk-update back-compat contract with the one deliberate isGroup exception
- isolation-model.md: cross-reference from the entity model's engage columns to the defaults docs
- CLAUDE.md: channel-defaults.ts key-file row and a short Channel defaults paragraph
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PJkDAnLGWJUqjgJNXpyqGZ
- 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>