CtrlK
BlogDocsLog inGet started
Tessl Logo

jbaruch/nanoclaw-trusted

Rules for trusted NanoClaw groups. Shared memory, session bootstrap, cross-group memory updates. Loaded for trusted and main containers only.

74

Quality

93%

Does it follow best practices?

Impact

No eval scenarios have been run

SecuritybySnyk

Risky

Do not use without reviewing

Overview
Quality
Evals
Security
Files

CHANGELOG.md

Changelog

Unreleased

Skills — apply-time dedup + shared atomic-write helper for memory writes (jbaruch/nanoclaw#365)

The daily_discoveries.md file, both daily-log targets, and (via the shared writer) session-state.json / the bootstrap sentinel previously had three slightly different inline atomic_write_text implementations and no dedup. The deer-flow review surfaced the symptom: rolling memory files grow unbounded because retries re-append the same line, and a partial write at the wrong moment could leave a half-written daily log.

New skills/trusted-memory/scripts/memory_write.py is the single home for the writer recipe — tempfile → flush → fsync → chmod (preserving the target's existing mode across overwrites) → os.replace — plus normalize_for_comparison and dedup_filter for the apply-time dedup. append-to-daily-log.py and register-session.py both import from it now, replacing their private copies. New append-daily-discovery.py wraps the same primitives for the daily_discoveries.md block format (## YYYY-MM-DD HH:MM UTC + **What:** / **Context:** / **Promote to:** quartet), and the daily-discoveries-rule now points the agent at the script instead of the prior LLM-driven Read/Write flow.

Dedup is whitespace-normalized (line endings, runs of whitespace, leading/trailing spaces) — semantic same-fact dedup across reworded entries is deliberately out of scope; the issue's deer-flow pattern only catches exact-byte duplicates that differ by whitespace. All-duplicate calls leave the file's mtime and inode untouched so idempotent retries don't churn the filesystem. Existing on-disk daily logs are never rewritten — the acceptance criterion ("no destructive migration") is enforced by a regression test asserting bit-for-bit content stability on the dedup-skip path.

Tests: 16 new cases (helper normalize_for_comparison / dedup_filter / write_atomic paths including a SIGKILL-based smoke test that proves an interrupted write leaves the file at its pre-call content, never partial) + 4 new dedup cases on the daily-log script + 12 cases on the new discovery script. Existing 28 tests pass against the refactor. Sync surfaces: SKILL.md updated to mention the new dropped_duplicates field on the daily-log JSON and the dedup contract; daily-discoveries-rule.md updated to invoke the script; this CHANGELOG entry. No tile.json change — the helper module lives inside the existing trusted-memory skill and isn't a separately-published surface; the new script ships alongside its siblings.

Rules — 9 task-context rules switched from alwaysApply: true to conditional applyTo: (jbaruch/nanoclaw#552)

The CLAUDE.md @-include chain was loading 28 always-on rules into every agent turn (~932 lines, ~12-15K tokens baseline). Per jbaruch/coding-policy: rule-frontmatter's conditional-rules contract, rules whose prescriptions only fire in specific task contexts should set alwaysApply: false and add an applyTo: clause so the agent's model loads them on-demand rather than carrying the full body in baseline. Nine trusted-tile rules qualified for the conversion:

  • messages-db-schema — load when querying messages.db or referencing its column names
  • github-data-via-gh — load when reading GitHub data via the gh CLI or related Composio tooling
  • wiki-awareness — load when working with /workspace/trusted/wiki/ or its raw sources
  • installed-content-immutable — load when about to write under /home/node/.claude/ or installed-tile paths
  • no-orphan-tasks — load when creating, rescheduling, or cancelling scheduled_tasks rows
  • memory-file-locations — load when writing or reading typed memory files or memory/ subdirectory contents
  • compaction-aware-summaries — load at compaction time or when authoring post-compaction summaries
  • ground-truth-trusted — load when answering substantive trusted-tier questions or producing claims that need verification
  • daily-discoveries-rule — load when learning something new worth recording in daily_discoveries.md

Each conditional rule keeps its body verbatim — only the frontmatter changed (alwaysApply: false + applyTo: "** — <natural-language clause>"). The glob is ** because task-context scoping doesn't bind to a file set; the natural-language clause carries the discriminator per rule-frontmatter's "the model reads both halves" guidance. Six previously-implicit always-on rules (session-bootstrap, verification-protocol, proactive-fact-saving, no-silent-defer, identity-dual-handle, local-context-anchoring) now have explicit "alwaysApply": true in tile.json to match their rule-file frontmatter — closing the split-value inconsistency rule-frontmatter warns against. Final shape across the tile's 26 rule entries: 17 always-on, 9 conditional. The 17 always-on ones (Telegram protocol, container trust levels, global memory, skills policy, identity/session-bootstrap/verification-protocol/proactive-fact-saving, etc.) stay loaded every turn because they govern decision quality or chat hygiene. README rules table gains a Scope column reflecting always-on vs conditional. Issue #552 targets a 40-50% baseline reduction across the full chain; this PR is the trusted-tile portion, with progress-updates in nanoclaw-core covered separately.

Rules — structural split of trusted-behavior per coding-policy: context-artifacts

Split the 13-H2 trusted-behavior.md into 11 single-concept rules per Rules Are Prose → One concept per rule file. Dropped two sections that duplicated already-loaded rules:

  • "Boyscout Rule" — duplicates jbaruch/coding-policy: boy-scout (already auto-loaded); no trusted-tier-specific content worth preserving
  • "Verification" — pointer to jbaruch/nanoclaw-core: ground-truth (already loads in trusted via the core dependency) plus the trusted-tier memory paths which memory-file-locations.md already governs

New per-concept rules:

  • identity-compaction-recovery — SOUL.md re-read after context compaction
  • async-tasks-extended — trusted-tier extension of core async-tasks protocol (reaction upgrade, background-agent spawn, scheduled-task silence, post-compaction restart)
  • skills-policy — invoke skills via Skill(skill: "name"); never read SKILL.md manually; no improvising
  • composio-vs-agents — when to use each
  • proactive-participation — trusted-group participation model
  • reply-threading — always reply-thread with reply_to
  • context-bootstrap-bg-agents — workspace-context block to include in background-agent prompts
  • container-trust-levels — runtime-detection contract; capability-matrix doc pointer
  • global-memory/workspace/global/CLAUDE.md for cross-group facts
  • duplicate-prevention — check before create
  • pending-response-trackingsession-state.json lifecycle for interruption recovery

trusted-behavior.md deleted (was 83 lines; 11 of its 13 sections distributed, 2 dropped as duplicates of already-loaded rules). Rule count: 16 → 26. Sync surfaces per coding-policy: context-artifacts Surface Sync: tile.json entry removed and 11 new entries added; README rules table extended.

Rules — conciseness pass per coding-policy: context-writing-style (tier 3)

  • session-bootstrap — dropped the VERY intensifier from "YOUR VERY FIRST ACTION". very here was carrying no constraint; the directive is "first action".

Rules — conciseness pass per coding-policy: context-writing-style

Always-on rules are loaded into every agent invocation, so meta-justification prose and dated incident references inflate the per-invocation token budget for no operational gain. This pass strips that content while preserving the operative contracts. Cut content is archived here per the rule's "What to Cut → move to CHANGELOG" guidance.

  • identity-dual-handle — renamed H1 from "Identity — Dual-Handle Reference Incident" (the file is now a rule, not an incident record); dropped the opening paragraph that framed the file as "the deploy-tier record of a concrete failure that motivated [the invariant]"; dropped ## Reference incident — 2026-04-27 section in full (debate-setup message routing the agent through both handles; re-triggered same morning; narrative + companion-mitigation context referenced docs/adr/2026-04-27-dual-handle-role-splitting.md). Operative ## How to Apply bullets preserved verbatim.

  • installed-content-immutable — dropped trailing sentence "See docs/adr/2026-04-25-installed-content-erofs.md for the motivating incident (jbaruch/nanoclaw#247) and the staging → promote → publish → update pipeline…"; the operative "Changes flow through staging → promote → publish → update" replaces it in one line without the incident pointer or cross-rule rationale shoulder-tap.

  • github-data-via-gh — cut "49 distinct curl-against-api.github.com command shapes on the operator-observer chat 2026-04-28..05-03 (all in telegram_old-wtf) hit the unauthenticated public-API path and probed more URLs to triangulate after each 404" worked example with incident dates from ## Why Not curl. Failure-mode list preserved as the operative content.

  • messages-db-schema — cut "(the Python stdlib module is the path; 23 sqlite3: command not found events in the observer-chat audit 2026-04-28..05-03 drove this rule)" parenthetical; "values verified live on 2026-05-04" verification date; "Recurring failure mode this rule closes: no such column: trigger_word / chat_jid / trusted errors (18 hits / 9 distinct guesses in the same audit window)" worked-example sentence. Replaced with a compact "Common wrong guesses: …" enumeration that keeps the actionable column-name hints without the audit-window dates.

  • local-context-anchoring — cut "(currently segment / home_fallback / container_default in this orchestrator version)" parenthetical that lists in-flight timezone_source enum values; rewrote the sentence to apply to any non-location source so the rule doesn't decay when the enum expands.

Rules

  • Scrub dangling cross-tile references (jbaruch/nanoclaw-trusted#44, PR #45) — Replace nanoclaw-admin/skills/.../*.py path leaks with contract-shaped wording; rewrite participating writer/reader identities in state-schema.md using stable jbaruch/nanoclaw-admin: tessl__<skill> qualifiers so the stateful-artifacts contract stays explicit without coupling to internal layouts. No skill-behaviour change; no rule-contract change.

  • New always-apply rule local-context-anchoring (jbaruch/nanoclaw-trusted#42, paired with jbaruch/nanoclaw#576) — Directs the agent to anchor every relative time/place phrasing (today / yesterday / tomorrow / now / сегодня / вчера / завтра / сейчас / here / where) to the user's local frame from the orchestrator-injected <context> tag's enriched attrs (local_datetime, local_date, weekday, timezone, timezone_source, and location_lat / location_lng / location_age_minutes on the fresh-pin path). Driving symptom on 2026-05-16: the agent referred to a Friday hack-a-team event as "сегодняшний" (today's) for a user who had already crossed into Saturday morning in Nashville — the calendar payload was fresh in context but date framing was loose because the agent had no resolved local-date anchor to ground "today" on. The pre-#576 <context> tag carried only timezone="..." and forced the agent to re-derive the local frame on every turn; the paired jbaruch/nanoclaw#576 host PR now emits the full anchor, and this rule tells the agent to actually use it. Three sub-sections: (1) Anchor relative phrasings to the local frame with two concrete pre-conversion examples (UTC reminder + +02:00 event both rendered into the user's local "today"/"yesterday"/"tomorrow"); (2) Surface uncertainty when the anchor is weak — on timezone_source="container_default" the agent must say so (date frame may be wrong, user should correct), and on segment / home_fallback / container_default the agent has no location_* signal and must not pretend to know here; (3) Trumps inline data formats — if the output text uses a relative-time word, the local frame controls regardless of source data shape. Scoped to trusted (not core, not admin): core loads for untrusted too where neither tz_state nor location data is reachable, so the rule's anchor wouldn't exist; admin would scope to main-only and miss other trusted groups where the same framing slip applies. Trusted is where every group has tz_state + (post-#574) location reachable. Sync surfaces per coding-policy: context-artifacts Surface Sync: tile.json rules entry added (after identity-dual-handle; tile.json preserves historical insertion order rather than re-sorting); README rules table extended alphabetically between installed-content-immutable and memory-file-locations; this CHANGELOG entry. No skills change; no eval change (closed-loop carve-out applies to this tile). Tile rule count: 15 → 16.

  • gh CLI now installed + authenticated in main/trusted — pivot GitHub-data policy from Composio to gh (jbaruch/nanoclaw#565) — Host PR #565 installed the GitHub CLI in the agent-runner image and forwarded GITHUB_TOKEN via the existing 0600-mode --env-file plumbing for main || trusted tier containers. The three rules added in the recent observer-chat-audit batch (cli-tools-not-installed, github-data-via-composio, plus the GitHub section of ground-truth-trusted) all encoded the prior "no gh, use Composio" reality and are now stale. Changes: delete cli-tools-not-installed.md — its sqlite3-CLI-absence paragraph folds into messages-db-schema.md's opening (already the natural home for the python3 -c 'import sqlite3; ...' pattern), and its gh-CLI-absence paragraph is obsolete; rename github-data-via-composio.mdgithub-data-via-gh.md and rewrite to make gh --json the primary GitHub-data path, with Composio GITHUB_* retained as fallback only for operations gh can't express; update ground-truth-trusted.md's GitHub section to point at github-data-via-gh and flip the sub-agent note (sub-agents inherit the parent's env so gh works inside them, in contrast to Composio MCP which they can't reach). The 49-curl-probe-shape audit number stays — that failure mode is still real; the rule now redirects to gh instead of Composio. Tile rule count: 16 → 15 (cli-tools-not-installed retired). Sync surfaces per coding-policy: context-artifacts Surface Sync: tile.json rules entries updated (one removed, one renamed); README rules table updated (row removed, row renamed and description rewritten, ground-truth-trusted description tightened to drop the "via Composio" phrase). Untrusted-tier containers are NOT in scope — they have gh on PATH (installed in the same image) but no token, so Composio remains their GitHub-data path; this tile loads only for trusted/main per its tile.json summary, so the rules' audience is exactly the tier where gh is authenticated.

  • Compress no-silent-defer.md (jbaruch/nanoclaw#552 step 2 of the requires: + compression + reorg plan) — Audit on telegram_old-wtf (trusted-tier baseline) measured RULES.md at 932 lines / 28 rules. The trusted-tile contribution is 422 lines / 16 rules. no-silent-defer.md 42 → 39 lines: folded the 4-step "What to do instead" enumeration into one paragraph (the steps don't have decision points between them — they're a single action). Kept the three concrete-handoff cases (resumable-cycle continuation, separately scheduled task, explicit owner message), the forbidden-patterns enumeration with its last_verified + status: open examples, the skip-summary file contract (/workspace/group/.skip-summary-<tessl-skill-id>.json naming convention is load-bearing — collision avoidance is the whole reason for the per-skill suffix), the writer/reader/surfacer ownership split, the docs/skip-summary-schema.md pointer, and the "applies to" enumeration of affected skills. trusted-behavior.md was initially in scope for this PR but was reverted after the OpenAI policy reviewer correctly flagged that compression alone doesn't satisfy coding-policy: context-artifacts's "one concept per rule file" rule — at 13 sub-sections the rule is structurally a candidate for splitting, not compressing. A follow-up PR will split it, but that's a riskier change touching cross-references from the nanoclaw-admin tile and the orchestrator's container-runner, so it ships separately rather than blocking this targeted compression. Companion core-tile PR ships the context-recovery + ground-truth + telegram-protocol compressions. Step 1 of #552 (requires: frontmatter on the host orchestrator) merged separately as jbaruch/nanoclaw#553; step 3 (tile reorganization + requires: declarations) is a follow-up PR after step 1's mechanism is deployed and observed.

  • Three new always-applied rules for recurring agent hallucinations (jbaruch/nanoclaw-admin#193, #194, #195) — Trusted-tier agents kept reaching for tools that aren't installed or aren't the right path even when the operational runbook on NAS documented it: 32 gh: command not found + 23 sqlite3: command not found events in the operator-observer-chat audit (2026-04-28..05-03), 49 distinct curl https://api.github.com/... command shapes (all unauthenticated, all probing 404s on guessed slugs), and 18 no such column SQL errors with 9 distinct guesses against messages.db (trigger_word for trigger_pattern, trusted as a column when it's actually inside container_config JSON, etc.). Originally the operator added the relevant prose to /workspace/trusted/RUNBOOK.md on the deployed NAS, but that runbook is gitignored host-local content (the broad trusted/ gitignore meant to keep personal content out of the public fork swept in operational knowledge too), so accumulated lessons drift silently, fork into different per-deployment shapes, and never reach a fresh NanoClaw install. This PR moves the operational portion to versioned tile rules: cli-tools-not-installed (gh + sqlite3 absent, prescribes Composio GITHUB_* and python3 -c 'import sqlite3'), github-data-via-composio (60-vs-5000 rate limit, missing {successful, error} envelope, private-repo visibility gap, and the explicit "don't hand-roll auth" guard), messages-db-schema (authoritative PRAGMA table_info-style listings for registered_groups, chats, messages, scheduled_tasks, tz_state, follow_me_tasks, phase_completions, with the live PRAGMA dump from /home/jbaruch/nanoclaw/store/messages.db on 2026-05-04 + the cross-tile reference to nanoclaw-admin: follow-me-two-phase-lock for mutation-time concurrency). Each rule cites the observer-chat counts so the agent reading this sees the failure modes are recurring, not hypothetical. Sibling cross-references between the three rules so an agent landing on one can navigate to the others. Sync surfaces per coding-policy: context-artifacts Surface Sync: tile.json updated by promote pipeline, README rules table extended with three new rows in alphabetical position, this CHANGELOG entry. CI workflow is unchanged — the tessl skill review step is per-skill, and these are rules. Acceptance criterion in each issue: zero recurrence on the operator-observer chat for one full week starting today; the runbook prose stays on NAS as a follow-up cleanup once the new rules' install + auto-load are observed reaching agents in the field.

  • identity-dual-handle / installed-content-immutable — migrate postmortems to docs/adr/ (jbaruch/nanoclaw-admin#183, umbrella jbaruch/nanoclaw-admin#180 RULES.md diet) — Two always-loaded rules contained incident postmortem narratives that don't change runtime behavior — they exist as historical context for why the rule exists. Moving them to docs/adr/ removes them from the agent prefix while preserving the institutional memory. Each rule keeps the imperative + 1–2 line anchor pointing to the ADR. Verbatim transfer; no spec content changes. Files: docs/adr/2026-04-27-dual-handle-role-splitting.md, docs/adr/2026-04-25-installed-content-erofs.md. Measured: identity-dual-handle drops 2,142 → 1,242 bytes (-900); installed-content-immutable drops 2,339 → 1,038 bytes (-1,301); combined -2,201 bytes from the trusted-tier prefix.

  • trusted-behavior.md — compress core-overlap and relocate trust-tier matrix (jbaruch/nanoclaw-admin#180 trusted-tile bundled-cleanup half) — The "Async Tasks — Extended Protocol" preamble re-explained core's react → background → deliver pattern; replaced with a one-liner pointing at the core rule and tightened the numbered steps. The "Container Trust Levels" subsection's full capability matrix (mounts, plugins, Composio access, idle timeout, RAM/CPU caps) was reference data the agent doesn't need per-turn — moved to docs/trust-tier-capabilities.md. The runtime detection ("read-only-filesystem error on a write to the group folder → untrusted, don't retry") stays in the rule. The "Proactive Participation" 4-bullet enumeration collapses to one paragraph keeping the gate (participate but don't pad silence). Drop: 4,908 → 4,041 bytes (-867).

  • skill-dependencies.md — remove rule, relocate to docs/skill-execution-order.md (jbaruch/nanoclaw-admin#180) — The rule was developer reference describing what skill calls what and which state files form the read/write contract between cron-driven skills (heartbeat, morning-brief, nightly-housekeeping). Reference material that skill authors read on demand, not a runtime gate the agent acts on per-turn — and coding-policy: stateful-artifacts already requires per-skill schema docs at skills/<name>/state-schema.md, so a central listing in the always-loaded prefix duplicates the schema homes. Verbatim transfer to docs/skill-execution-order.md; rule deleted; tile.json rules.skill-dependencies entry removed; no cross-references in any other rule or skill (verified with grep -r). The currency note in the new doc flags that the per-step descriptions reflect the pre-jbaruch/nanoclaw#404 housekeeping split — content was moved verbatim, not re-derived; a follow-up PR can update it post-split. Drop: -1,922 bytes from the spawn prefix (full file).

  • Combined trusted bundle 1: -2,789 bytes from the trusted-tier prefix.

  • skill-dependencies heartbeat-flow edit (jbaruch/nanoclaw-core#38) is folded into the rule retirement above — earlier in the same Unreleased window, nanoclaw-trusted#31 removed the check-unanswered Step 0.7 bullet from rules/skill-dependencies.md after the skill was deleted in jbaruch/nanoclaw-core#38. Since the rule itself is now retired in this PR (content relocated to docs/skill-execution-order.md), that prior bullet's edit lands as part of the relocated doc rather than the always-loaded rule.

  • 4-rule compression cleanup (jbaruch/nanoclaw-admin#180 trusted-tile bundled-cleanup half, second batch) — Tighten tables, categories, and example lists in four trusted rules without changing any runtime gate:

    • memory-file-locations.md: 7-row directory-structure table collapses to one paragraph; the wiki/sources rows were off-topic for a memory-locations rule. The 3-numbered-invariants list keeps its substance (orphans get indexed, MEMORY.md paths must resolve, no duplicate files) — the typed-memory-root-vs-memory/ constraint moved into the preceding paragraph. The common-mistake reminder stays. Drop: 1,663 → 1,150 bytes (-513).
    • no-orphan-tasks.md: 3-numbered cadence-check list collapses to one paragraph naming the existing flows (nightly-housekeeping, heartbeat, morning-brief). The "What belongs in nightly-housekeeping" criteria-bullets collapse to one sentence; the tessl__nightly-housekeeping/SKILL.md path reference replaced with a one-line note that post-#404 the canonical pattern is an independent sub-skill row at the same cadence, not a numbered step in a monolith. Drop: 1,342 → 1,270 bytes (-72).
    • compaction-aware-summaries.md: "What compaction summaries naturally preserve (don't duplicate)" 3-bullet list collapses to one sentence — the gate ("focus the summary budget on the human context that only exists in conversation") and the 5-priority-list and pre-compaction-save sections are unchanged. Drop: 1,356 → 1,306 bytes (-50).
    • proactive-fact-saving.md: 7-row Categories table flattens to one paragraph; "How to save" 3-step retains the file-creation / index-update / daily-log-append sequence; "When NOT to save" 4-bullet list collapses to one sentence keeping the four exceptions. Aggression-level paragraph and the closing "context compaction is the enemy" line stay verbatim. Drop: 1,977 → 1,604 bytes (-373).
    • Combined trusted bundle 2: -1,008 bytes from the trusted-tier prefix.

Skills

  • trusted-memory append-to-daily-log.py follow-up — header fix + production overrides — Two regressions in the originally-shipped helper (post-merge audit found via comparison with nanoclaw-trusted#21, the parallel implementation that didn't merge):

    • Header mismatch with archive-helper. The original wrote # Daily Log — YYYY-MM-DD. The canonical convention is # Daily Summary — YYYY-MM-DD — matched by the regex at nanoclaw-admin/skills/nightly-housekeeping/scripts/archive-helper.py:57 (re.compile(r"^# Daily Summary — \d{4}-\d{2}-\d{2}\s*$")). With the wrong header, archive-helper's nightly archival silently skipped trusted-memory's daily files — they would accumulate forever in daily/, never rolling into weekly. New DAILY_HEADER_TEMPLATE constant centralises the canonical wording with a comment pointing at the archiver regex; the new test test_header_matches_archive_helper_regex runs the actual regex against the helper's first-line output so a future "let's tighten the header style" change can't regress this without breaking a test.
    • No production override mechanism. Container deployments that mount the memory tree somewhere other than /workspace/{group,trusted}/memory/daily (test runners, alternate mount layouts, debugging) need a way to redirect the target dir. Add --group-daily / --trusted-daily CLI flags + NANOCLAW_GROUP_DAILY / NANOCLAW_TRUSTED_DAILY env-var fallbacks (flag wins over env wins over default constant). Four new tests cover the precedence chain.
    • SKILL.md updated to call out both the canonical header (citing the archive-helper regex) and the override mechanism.
  • trusted-memory Rolling Memory Updates — append-to-daily-log.py (jbaruch/nanoclaw#275, umbrella jbaruch/nanoclaw#412) — replaces the LLM-driven Read/Write flow on /workspace/group/memory/daily/YYYY-MM-DD.md and /workspace/trusted/memory/daily/YYYY-MM-DD.md with a deterministic helper that holds fcntl.LOCK_EX on a sibling <file>.lock for the entire read-modify-write cycle. Concurrent writers (default container during a turn, maintenance container during a scheduled task, sub-skills mid-step) now serialise on the lock instead of clobbering each other's appends. The helper accepts lines via --line (repeatable), --lines-file, or stdin; resolves today's UTC date (overridable via --date YYYY-MM-DD for testability); creates the target file with # Daily Log — YYYY-MM-DD header on first call; appends at end-of-file regardless of timestamp ordering; and emits a stderr warning + out_of_order: true in the JSON result when the new line's HH:MM precedes the file's last entry (cross-group writers and clock-skew retries arrive late legitimately, but silent reorder would mask actual bugs). Atomic write via tempfile + fsync + chmod-to-preserve-mode + os.replace matches the register-session.atomic_write_text contract so file mode is preserved across overwrites. Stdout: single-line JSON {"path", "appended_lines", "final_line_count", "created", "out_of_order"}. Tests cover header creation on absent file, in-place append, multi-line via repeated --line, --lines-file, stdin, empty-input usage error, conflicting --line+--lines-file usage error, bad date format, out-of-order warn-and-append, file-mode preservation, JSON schema, the --target trusted path resolution, and a concurrent-subprocess clobber test that two writers each push 50 unique lines and asserts all 100 land. SKILL.md Rolling Memory Updates section rewritten to invoke the helper instead of describing the LLM Write flow. Tracked under script-delegation umbrella jbaruch/nanoclaw#412.

  • system-status SKILL.md content rewrite — finish #65 (jbaruch/nanoclaw-admin#65) — PR #14 (d35ae5d) shipped the directory rename, tile.json update, the new scripts/system-status-checks.py, and the test suite, but the new skills/system-status/SKILL.md was created with the legacy content verbatim — same name: check-system-health frontmatter, same three python3 -c "..." inline blocks the rewrite was supposed to remove. The CHANGELOG entry below describes the rewrite that PR #14 intended; this PR actually performs it: frontmatter namesystem-status, body collapsed to Step 1 (run system-status-checks.py) / Step 2 (act on alerts), description tightened to read-only-trusted-tier scope, dismiss-mechanism section dropped (admin's domain), explicit "What this skill is NOT" section added. tessl skill review skills/system-status reports 100%.

  • check-system-health renamed to system-status + content rewritten — Resolves the cross-tile name collision flagged in nanoclaw-admin#52 / #65 (admin and trusted both shipped a skill named check-system-health, both installing under tessl__check-system-health/ so the second-installed copy shadowed the first silently). Per the owner-recorded split decision: admin keeps the full SQLite + filesystem + IPC + container probe with the dismiss-mechanism management; trusted gets a read-only narrower subset under the new non-colliding name. The three previous python3 -c "..." inline blocks (which violated coding-policy: script-delegation) move to scripts/system-status-checks.py as a single deterministic JSON-producing probe; SKILL.md becomes prose-and-action with explicit Step 1 (run script) / Step 2 (act on alerts). The dismiss-mechanism section is dropped — trusted is read-only with respect to admin's dismiss state. tile.json rule entry renamed check-system-healthsystem-status. tests/test_system_status_checks.py covers seven scenarios: missing DB → exit 1, clean DB → empty alerts, stuck-task threshold crossing, recent failure within 24h, old failure outside 24h excluded, message rowcount above threshold, DB size above threshold.

Rules

  • no-silent-defer — extract skip-summary spec to docs/skip-summary-schema.md (jbaruch/nanoclaw-admin#182, umbrella jbaruch/nanoclaw-admin#180 RULES.md diet) — The Skip-summary file contract section was the single largest contributor to the compiled RULES.md prefix at ~3.2K. The schema is reference material — skill authors and pre-publish lint scripts read it; the rule's runtime gate is unchanged by where the spec lives. Field-by-field schema, lifecycle / actor responsibilities, and the planned pre-publish lint move to docs/skip-summary-schema.md. The rule keeps the imperative ("a skill that defers / skips writes the file at /workspace/group/.skip-summary-<tessl-skill-id>.json<tessl-skill-id> is the full tessl__<name> form, one file per skill — owner writes, surfacer reads-sends-deletes") and points to the doc for the rest. Verbatim transfer of the moved content; no spec changes. Measured: rule drops 6,344 → 3,132 bytes (-3,212).

  • ground-truth-trusted — GitHub Composio-first; unauth 404 ≠ non-existence (jbaruch/nanoclaw-trusted#29) — Extends rules/ground-truth-trusted.md with a section that names the Composio GitHub tools (GITHUB_GET_A_PULL_REQUEST, GITHUB_GET_A_REPOSITORY, GITHUB_SEARCH_ISSUES_AND_PULL_REQUESTS, GITHUB_GET_FILE_CONTENT via mcp__composio__COMPOSIO_MULTI_EXECUTE_TOOL) as the primary path for any owner-adjacent repo (jbaruch/*, ligolnik/*, tessl-io/*), and explicitly forbids treating a 404 from unauthenticated curl https://api.github.com/... as proof of non-existence — especially before retracting a prior statement. Sub-agent caveat noted: Agent-spawned sub-agents lack Composio MCP, so GitHub state must be fetched in the parent and passed via prompt. Triggered by a 2026-05-02 ~03:30 UTC incident where the agent retracted correctly-cited ligolnik/nanoclaw-public PRs after an unauth-curl 404; deploy-tier feedback at /workspace/trusted/feedback_github-composio-first.md generalises here for all trusted-tier containers.

  • no-silent-defer — structured skip-summary file contract (jbaruch/nanoclaw#277 part A, umbrella jbaruch/nanoclaw#412) — Adds a "Skip-summary file contract (deterministic surfacing)" section to rules/no-silent-defer.md. Prose enforcement of "remember to surface the skip" is itself prose under context pressure — the same shape that drove jbaruch/nanoclaw#265 (CFP tier3_skipped: nightly budget buried in a daily summary). The new contract requires any sub-skill that defers or skips work to emit /workspace/group/.skip-summary-<skill>.json with a versioned shape ({schema_version, skill, step, reason, items, technical_failure, occurred_at}); the caller deterministically checks for the file's existence after the sub-skill returns and surfaces it via mcp__nanoclaw__send_message. The owner skill atomic-writes the file (per coding-policy: stateful-artifacts) and deletes it as soon as the surfacing call completes — a stale file would re-fire the message every subsequent run. technical_failure: true|false lets the owner route transient failures (API timeout, lock contention) differently from policy skips (budget exhausted, deferred-by-design). Linting that verifies skills citing this rule actually emit + delete the file is tracked separately as jbaruch/nanoclaw#277 part B (lives in admin tile alongside the follow-me-two-phase-lock.md reference-citation lint).

  • identity-dual-handle (new) — Companion to the abstract dual-handle invariant in the jbaruch/nanoclaw-core tile's rules/core-behavior.md. Records the 2026-04-27 incident where the agent received a single message that addressed it by both its display-name trigger AND its Telegram @username, parsed the two surface forms as two addressees, and took on two roles in one turn. The abstract invariant ("never split yourself based on surface form") stays in core; the deploy-tier concrete record lives here. Tracked by jbaruch/nanoclaw-core#36 (the hygiene companion to that issue's core-side strip — cross-repo auto-close doesn't fire, the issue gets closed manually after both PRs merge). Note: the identity-theft form of the same confusion class — agents claiming an example handle as their own — is mitigated separately by the runtime identity preamble (buildIdentityPreamble from ASSISTANT_NAME / ASSISTANT_USERNAME) in jbaruch/nanoclaw-public; this rule covers the role-splitting form, which the preamble doesn't address.

  • installed-content-immutable (new) — Codifies the kernel-level read-only contract on /home/node/.claude/skills/ and /home/node/.claude/.tessl/ introduced in jbaruch/nanoclaw#247. Two read-only bind-mounts layer on top of the writable /home/node/.claude parent so Write/Edit against installed content returns EROFS rather than silently mutating an in-memory copy that gets rebuilt at the next container spawn. Documents what's still writable (transcripts, debug, todos, telemetry, session-env, auto-memory overlay), and the canonical staging → promote (tessl__promote-tiles) → publish (publish-tile.yml) → update (./scripts/deploy.sh) → spawn pipeline operators must use to actually change a skill or rule.

  • no-silent-defer (new) — Codifies that "deferred" is only legitimate when there's a concrete handoff (resumable-cycle continuation, separately-scheduled task with a different cadence, or an explicit message to the owner). Otherwise the work is skipped, and the skip MUST be surfaced via mcp__nanoclaw__send_message rather than buried in a daily summary. Triggered by an incident on 2026-04-27 where nightly-housekeeping Step 6 routed 9 unanalyzed CFP candidates to status: open + last_verified: today with bot_notes: "AI relevance pass deferred (lean-relevant default)" — none had been through the AI relevance pass, and the morning-brief last_verified ≤7 days safety net was actively defeated by the bogus stamp. Companion compliance updates to check-cfps and nightly-housekeeping ship in nanoclaw-admin#63.

Test infrastructure

  • pytest baseline + CI gate (new) — Mirrors the admin-tile scaffold (nanoclaw-admin#59): pyproject.toml carries [tool.pytest.ini_options] and a tests/-scoped ruff config; requirements-dev.txt pins pytest==8.3.4 and ruff==0.7.4; .github/workflows/test.yml runs ruff check tests/, ruff format --check tests/, then python -m pytest on every PR and push to main. Initial coverage targets the two new scripts in this PR (register-session.py, needs-bootstrap.py). Folds the trusted slice of nanoclaw-admin#55 into this PR per OpenAI policy reviewer requiring tests in the same PR as new modules.

Scripts

  • register-session.py / needs-bootstrap.py emit JSON status — Per jbaruch/coding-policy: script-delegation (JSON-producing). register-session.py prints {"session_id", "session_name", "schema_version", "wrote_state", "wrote_sentinel"}; needs-bootstrap.py prints {"needs_bootstrap", "current", "stored", "reason"}. Exit codes remain the authoritative success signal — JSON is for callers that want to log or inspect. SKILL.md updated to mention both contracts.

Skills

  • trusted-memory: hook-aware bootstrap note — Restores the 2-line clarification that the agent-runner's session-start-auto-context hook (jbaruch/nanoclaw#141) auto-injects MEMORY.md, RUNBOOK.md, and the most-recent daily log before this skill runs, so the bootstrap's value is the broader set the hook does NOT cover (group-shared trusted/ memory, weekly logs, highlights.md) plus the per-session sentinel + state-stamping. The note originated in admin's deleted copy (nanoclaw-admin@13de2a98) and was lost when admin's trusted-memory was deleted in nanoclaw-admin#60 (rebase resolved the modify/delete conflict by keeping the deletion). Trusted's canonical copy didn't carry it forward; this restores the context.

  • trusted-memory absorbs admin's improvements — The admin tile carried a parallel trusted-memory copy that diverged after nanoclaw-admin#31 extracted inline Python into helper scripts. Per audit decision (closes nanoclaw-admin#52), trusted is the canonical home for this skill. This change pulls admin's structural improvements into trusted's copy:

    • scripts/needs-bootstrap.py (new) — sentinel-vs-$CLAUDE_SESSION_ID check, replacing the inline 8-line block; exit 0 = bootstrap needed, exit 1 = skip.
    • scripts/register-session.py (new) — atomic session-state.json + /tmp/session_bootstrapped write under fcntl.LOCK_EX. Reads session_id from /workspace/store/messages.db with a 10s timeout; tolerates sqlite3.Error as session_id=None rather than crashing the whole bootstrap; refuses to write an empty sentinel (would silently disable bootstrap forever).
    • scripts/sync-session-id.py (deleted) — superseded by register-session.py. The new script subsumes its session-id mirroring AND adds the sentinel write that previously lived inline as Step 8.
    • state-schema.md (new) — documents session-state.json shape (sessions.<name> subtree + top-level session_id back-compat from jbaruch/nanoclaw#55) and the /tmp/session_bootstrapped sentinel contract per jbaruch/coding-policy: stateful-artifacts. Replaces the dangling reference_session-state-migration.md link in admin's SKILL.md (file was referenced but never written).
    • SKILL.md updated to admin's structure with two adjustments: dangling reference_memory-types.md link replaced by inlining the per-type frontmatter examples (the link target was likewise never written), and the reference_session-state-migration.md link redirected to the new state-schema.md.

    Removal from admin's tile lands in nanoclaw-admin separately. Refs nanoclaw-admin#52.

Surface sync

  • tile.json adds entrypoint: README.md per jbaruch/coding-policy: context-artifacts.
  • README.md and CHANGELOG.md introduced (none existed previously). Both will be maintained going forward as required by the policy.

The README's rules-table summaries are auto-extracted first-paragraph excerpts from each rule file. Refine them per rule when the wording is misleading; this commit is a structural bootstrap, not authored prose.

CHANGELOG.md

README.md

requirements-dev.txt

tile.json