From 8beb9157b5aa7161f0dea1abc7d5291809bf08ad Mon Sep 17 00:00:00 2001 From: callumflack Date: Sat, 11 Jul 2026 10:17:44 +1000 Subject: [PATCH 1/4] chore(agents): recover portable control-plane spine --- README.md | 17 ++- bin/agents-kit.mjs | 2 + docs/dot-agents-transport-spec.md | 117 ++++++++++++++---- scripts/verify-template.mjs | 4 +- .../default/.agents/AGENT-CONTROL-PLANE.md | 72 ++++++++--- templates/default/.agents/README.md | 12 +- templates/default/.agents/checks/README.md | 7 ++ templates/default/.agents/commands/README.md | 7 ++ templates/default/.agents/gates/README.md | 6 +- .../default/.agents/gates/agent-tooling.md | 7 +- .../default/.agents/gates/factory-failure.md | 2 +- templates/default/.agents/gates/rule-rinse.md | 12 -- templates/default/.agents/resolvers/README.md | 2 +- .../.agents/resolvers/agent-tooling.md | 36 ++++-- .../.agents/resolvers/factory-failure.md | 22 +++- .../default/.agents/resolvers/rule-rinse.md | 60 --------- templates/default/.agents/router.md | 4 +- .../.agents/skills/agents-kit/SKILL.md | 15 ++- .../scripts/check-agents-kit-health.py | 34 ++++- templates/default/AGENTS.md | 6 +- templates/default/history/lessons/README.md | 2 +- 21 files changed, 290 insertions(+), 156 deletions(-) create mode 100644 templates/default/.agents/checks/README.md create mode 100644 templates/default/.agents/commands/README.md delete mode 100644 templates/default/.agents/gates/rule-rinse.md delete mode 100644 templates/default/.agents/resolvers/rule-rinse.md diff --git a/README.md b/README.md index 5f5fbd0..50533e9 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,9 @@ For a visual walkthrough of the harness, open [dot-agents-system.html](dot-agent - `AGENTS.md` points to the control plane. - `.agents/router.md` dispatches task shape to resolver, gate, and skill. - `.agents/resolvers/*` scope reads, writes, owners, and non-goals. -- `.agents/gates/*` verify done with observable checks. +- `.agents/gates/*` name observable done checks. +- `.agents/commands/*` collapse repeated workflow hops. +- `.agents/checks/*` prove objective agent-process or repo-ownership invariants. - `.agents/skills/*` teach repeatable technique. - `.agents/logs/*` orient handoff. - `history/*` preserves dated evidence. @@ -105,12 +107,14 @@ skills-lock.json README.md agent-tooling.md factory-failure.md - rule-rinse.md gates/ README.md agent-tooling.md factory-failure.md - rule-rinse.md + commands/ + README.md + checks/ + README.md skills/ README.md agents-kit/ @@ -141,6 +145,8 @@ skills-lock.json .agents/router.md .agents/resolvers/* .agents/gates/* +.agents/commands/* +.agents/checks/* .agents/logs/* history/* .scratch/* @@ -156,8 +162,9 @@ After install: 2. Replace placeholder router rows only after live repo evidence exists. 3. Add repo-specific resolvers for recurring task lanes. 4. Add gates with concrete checks, not vague verification language. -5. Keep logs for handoff context, not live law. -6. Keep dated evidence in `history/`. +5. Add commands only for repeated hop-collapsers; add checks only for objective oracles the repo can observe. +6. Keep logs for handoff context, not live law. +7. Keep dated evidence in `history/`. ## Verify diff --git a/bin/agents-kit.mjs b/bin/agents-kit.mjs index fddfa1e..12cc3ae 100755 --- a/bin/agents-kit.mjs +++ b/bin/agents-kit.mjs @@ -83,6 +83,8 @@ function isReviewOnly(file) { file === ".agents/router.md" || file.startsWith(".agents/resolvers/") || file.startsWith(".agents/gates/") || + file.startsWith(".agents/commands/") || + file.startsWith(".agents/checks/") || file.startsWith(".agents/logs/") || file.startsWith("history/") || file.startsWith(".scratch/") diff --git a/docs/dot-agents-transport-spec.md b/docs/dot-agents-transport-spec.md index 1c15463..eb54e38 100644 --- a/docs/dot-agents-transport-spec.md +++ b/docs/dot-agents-transport-spec.md @@ -13,7 +13,63 @@ in, or directly implied by, the live `unity-surfaces` `.agents` system. No blog post is part of this transport. The post comes after the installable harness exists. -## Current `agents-kit` Repo Shape +## July 2026 Rinse Decisions + +The Unity rinse adds five portable decisions from the June commit sequence. + +- `dfbec319` is the main portable source: port the spine `repeated friction -> owning surface -> smallest durable constraint -> nearest oracle`; port the authority split where commands collapse hops, checks prove, and gates name done checks; route resolver/gate/router quality work through `agent-tooling` rather than a separate `rule-rinse` lane. +- `6075a07f` is a pattern, not a default check: copy/class assertion enforcement belongs in a repo-owned `.agents/checks/*` or hook only when that repo has the same objective test invariant. Do not ship the Unity-specific test-copy check in the default seed. +- `316a4972` is mostly a host-adapter pattern: role/adaptor structure belongs in a repo only after it has real host agent surfaces. Do not ship Unity's `test-reviewer` or doc-drift roles as generic defaults. +- `6703041b` is the counterweight: trim enforcement when it becomes heavier than the miss. The subagent repair belongs first in `.agents/resolvers/agent-tooling.md`; promote to `.agents/checks/*` only if a transcript or tool-call oracle exists. +- `aa3dd514` is the delegated-subagent proof commit: port the resolver rule and its "no transcript oracle yet" reasoning, but do not port Unity's dated log into the default seed. + +## Additional Commit Archaeology + +Comparing Unity history and the current `agents-kit` template gives this transport decision table. + +| Commit | Unity change | Transport decision | +| --- | --- | --- | +| `83d08dca` | Adds the factory-failure control loop, router row, gate, and health coverage. | Already core seed behavior. Keep generic failure classification and live-owner repair. | +| `fd778261` | Renames learning artifacts to `history/lessons` and makes lessons an artifact shelf, not live law. | Already core seed behavior. Keep `history/lessons` and `factory-failure` lifecycle ownership. | +| `af55c851` | Adds anti-consolidation rules for lesson artifacts. | Already/now core seed behavior. Preserve source episodes and promotion state. | +| `8c6c0e3f` | Adds a Unity factory-state checker under `agents-kit` scripts. | Do not port the checker. Later doctrine supersedes it: repo/factory checks belong in `.agents/checks/*`, not `agents-kit` scripts. | +| `22c499b1` | Adds stack-landing dirty-diff rules for stacked PR work. | Do not ship by default. It is useful repo doctrine when a repo uses the `stack` workflow, but too specific for the portable seed. | +| `9c93e919` | Adds Unity git handover gate and branch/upstream rules. | Do not ship as default doctrine. Target repos have different branch protections; keep only stage-explicit-paths and review-only install protection. | +| `31bed50f` | Tightens Unity testing gate around class/style checksum assertions. | Do not ship by default. Treat as an example of a repo-owned test invariant. | +| `6075a07f` | Adds PostToolUse hook plus copy/class assertion check. | Do not ship by default. Port the doctrine pattern only: hooks/checks are for mechanically observable invariants. | +| `316a4972` | Adds roles, hooks, governed loops, and adapter doctrine. | Do not ship by default. Keep role/adapter and loop machinery out until a target repo has actual host-agent surfaces or recurring loop contracts. | +| `6703041b` | Trims remote-guard enforcement after it became too heavy. | Port the restraint: avoid heavy gates when a smaller resolver rule or git/platform guardrail is enough. | +| `dfbec319` | Consolidates command/check/gate authority and retires stale lanes. | Ported: commands/checks surfaces, health coverage, and agent-tooling-owned quality passes. | +| `aa3dd514` | Adds delegated subagent flow to `agent-tooling`. | Ported: resolver rule and check-promotion condition. | +| `32845c27` | Thins and hardens the Unity control plane after the rinse. | Use as the live comparison point for the portable spine. Keep the five-field receipt, fail-closed proof expectations, and generic lanes; reject Unity product surfaces, roles, loops, hooks, logs, lesson bodies, and installed-skill inventory. | +| `2a7e7645` | Separates repo-owned materialization hashes from installer-owned import metadata. | Adopt the ownership split without fake provenance: a seed manifest declares `agents-kit` and an empty versioned materialization map; `skills-lock.json` remains the import-metadata owner. Implementation and verification remain part of this refactor's done gate. | + +Two integrity decisions remain required before this transport is complete: + +- The package verifier must reject conflict markers by inspecting shipped template content, and a negative self-test must prove that rejection. `git diff --check` remains an independent maintainer oracle. +- The seed must declare skill ownership in `.agents/skills/manifest.json` without inventing imported skills or hashes. The health checker and installer review-only behavior must validate that contract before the manifest decision can be recorded as implemented. + +## Codebase Comparison Result + +The live Unity `.agents` tree is intentionally larger than the default seed: it includes product resolvers, package gates, role adapters, governed loops, hooks, commands, checks, many installed skills, and dated logs/lessons. The portable seed should carry the reusable owner/oracle contract, not Unity's product surface inventory. + +What must match at the seed level: + +- `AGENTS.md` stays a boot pointer. +- Router defaults stay generic: `agent-tooling` and `factory-failure`. +- `agent-tooling` owns control-plane edits, commands/checks placement, skill inventory, and delegated subagent behavior. +- `factory-failure` owns classification, learning lifecycle, anti-consolidation, and live-owner repair. +- `commands/` and `checks/` exist as first-class surfaces. +- `agents-kit` scripts check the control plane only. + +What should not match by default: + +- Unity product rows: web, runtime-boundary, shared-package, testing. +- Unity commands/checks: git-handover, test-proof, runtime-boundary, visible-copy, factory-state. +- Unity hooks, roles, loops, host adapters, and installed skill inventory. +- Unity logs and lesson bodies, except for generic lesson shape and lifecycle doctrine. + +## Pre-Refactor `agents-kit` Repo Shape The old repo already answers the installer question. @@ -87,6 +143,12 @@ AGENTS.md .agents/gates/ done-means-done checks +.agents/commands/ + executable hop-collapsers for recurring hot paths + +.agents/checks/ + mechanical pass/fail oracles for agent-process or repo-ownership invariants + .agents/skills/ repo-local techniques and bundled helper scripts @@ -117,6 +179,8 @@ The important structure is the authority split: | `.agents/router.md` | routing law | | `.agents/resolvers/*` | owner and scope law | | `.agents/gates/*` | done law | +| `.agents/commands/*` | hop-collapsing entrypoints | +| `.agents/checks/*` | mechanical oracles | | `.agents/skills/*` | technique | | `.agents/logs/*` | handoff orientation, never live law | | `.scratch/*` | active execution state, not doctrine | @@ -140,12 +204,14 @@ templates/default/ README.md agent-tooling.md factory-failure.md - rule-rinse.md gates/ README.md agent-tooling.md factory-failure.md - rule-rinse.md + commands/ + README.md + checks/ + README.md skills/ README.md agents-kit/ @@ -188,8 +254,7 @@ Port these live concepts directly: - update logs when handoff changes; - keep repair rule live. - `.agents/README.md`: - - "AGENTS points, router dispatches, resolvers scope, gates verify, skills - teach, logs orient, history is evidence"; + - "AGENTS points, router dispatches, resolvers scope, gates name proof, commands collapse hops, checks prove, skills teach, logs orient, history is evidence"; - map of each surface; - no learning live rules from logs or history; - resolver quality loop. @@ -200,14 +265,14 @@ Port these live concepts directly: - pre-edit ownership receipt; - placement test; - promotion timing; - - resolver/gate/skill distinction; - - scripts boundary; + - resolver/gate/command/check/skill distinction; + - agents-kit scripts boundary; - artifact ownership; - settlement check; - doctrine pressure check; - lessons lifecycle boundary. - `.agents/router.md`: - - generic rows only: agent-tooling, factory-failure, rule-rinse; + - generic rows only: agent-tooling and factory-failure; - clear priority rules; - no Unity product rows. - `.agents/resolvers/agent-tooling.md`: @@ -216,18 +281,22 @@ Port these live concepts directly: - skill inventory boundary; - installed skill mirror boundary; - local-only skill authoring rule; + - delegated subagent work rule: spawn once, report id, keep write sets disjoint, poll/await cleanly, verify returned result, close worker; - allowed writes and non-goals; - no product code by default. - `.agents/gates/agent-tooling.md`: - receipt required for nontrivial lane edits; - - health check required for router/resolver/gate/skill/lesson structure; + - health check required for router/resolver/gate/command/check/skill/lesson structure; - lessons stay in `history/lessons`; - skill provenance checked before inventory edits; + - commands collapse repeated hops and do not own verdicts; + - checks own objective pass/fail; - `AGENTS.md` stays short; - `.agents` contains no current-focus/build-loop runtime state; - local-only files are not accidentally staged. - `.agents/resolvers/factory-failure.md`: - classify failures before repair; + - use the friction-promotion shape for repeated or expensive misses; - distinguish normal product iteration from agent-process failure; - choose owner surface before patching; - learning lifecycle belongs here, not in `history/lessons`; @@ -240,11 +309,10 @@ Port these live concepts directly: - selected owner gate run; - exactly one outcome recorded; - skipped checks report blocker and residual risk. -- `.agents/resolvers/rule-rinse.md` and `.agents/gates/rule-rinse.md`: - - rules are live hypotheses; - - test against real scenarios; - - add non-goals and cold-agent tests; - - record rinse in logs. +- `.agents/commands/README.md` and `.agents/checks/README.md`: + - commands collapse repeated manual hops without owning verdicts; + - checks prove objective repo-observable invariants; + - no check is added when the repo cannot observe the miss. - `.agents/logs/README.md`: - logs orient resumption; - logs are not changelogs, commit proof, or live law; @@ -261,6 +329,7 @@ Port these live concepts directly: - verify router references; - verify resolver required sections and gate paths; - verify gates state done criteria; + - verify gates and commands only name existing `.agents/checks/*` paths; - verify skill frontmatter; - verify locked skill paths when `skills-lock.json` exists; - verify `AGENTS.md` remains short; @@ -282,12 +351,12 @@ Use this first. Pick the narrowest matching row. | Trigger | Resolver | Gate | Skill | | --- | --- | --- | --- | | failed checks, repeated misses, stale task state, learning-promotion questions, repair-loop decisions, maintenance-rinse findings | `.agents/resolvers/factory-failure.md` | `.agents/gates/factory-failure.md` | `.agents/skills/agents-kit` when patching `.agents` or learning surfaces; otherwise route by classified owner | -| agent tooling, docs placement, `.agents` control-plane, `.scratch`, PRDs, skills, host-specific agent setup, lesson artifacts | `.agents/resolvers/agent-tooling.md` | `.agents/gates/agent-tooling.md` | `.agents/skills/agents-kit` for `.agents` changes; otherwise matching `.agents/skills/*` | -| resolver/gate/router quality pass | `.agents/resolvers/rule-rinse.md` | `.agents/gates/rule-rinse.md` | none by default | +| agent tooling, docs placement, `.agents` control-plane, `.agents/commands`, `.agents/checks`, `.scratch`, PRDs, skills, host-specific agent setup, lesson artifacts, initial repo setup, package-script plumbing, resolver/gate/router quality pass | `.agents/resolvers/agent-tooling.md` | `.agents/gates/agent-tooling.md` | `.agents/skills/agents-kit` for `.agents` changes; otherwise matching `.agents/skills/*` | ## Priority Rules - Agent tooling work stays in the control-plane lane unless the user explicitly switches into product implementation. +- Resolver, gate, and router quality passes use agent-tooling plus `.agents/AGENT-CONTROL-PLANE.md`; do not create a second live lane for rule cleanup. - Factory failure classifies a miss before repair; it does not replace the selected owner resolver or gate. - Repo-specific product rows should be added only after live repo evidence exists. ``` @@ -323,6 +392,8 @@ Review-only / local-doctrine files: - `.agents/router.md` - `.agents/resolvers/*` - `.agents/gates/*` +- `.agents/commands/*` +- `.agents/checks/*` - `.agents/logs/*` - `history/*` - `.scratch/*` @@ -385,11 +456,13 @@ Regression checks: 4. Add `skills-lock.json` as an empty template object, or make the health checker explicitly tolerate its absence in fresh installs. 5. Add `factory-failure` resolver and gate. -6. Port `check-skill-frontmatter.py`. -7. Update `verify-template.mjs` required files. -8. Update `README.md` to describe install/adopt/update against the new harness. -9. Run source verification and smoke tests. -10. Only after that, write the public post. +6. Add `commands/README.md` and `checks/README.md`; do not ship Unity product checks. +7. Retire the default `rule-rinse` lane; quality passes route through `agent-tooling`. +8. Port `check-skill-frontmatter.py`. +9. Update `verify-template.mjs` required files. +10. Update `README.md` to describe install/adopt/update against the new harness. +11. Run source verification and smoke tests. +12. Only after that, write the public post. ## Non-Goals @@ -398,6 +471,7 @@ Regression checks: - Do not transport `history/solutions`. - Do not make a workflow engine. - Do not turn logs into source of truth. +- Do not ship a giant subagent gate without a real transcript/tool-call oracle. - Do not make the blog post part of the first transport. - Do not make the old template and live Unity doctrine compete. @@ -406,6 +480,7 @@ Regression checks: The extraction is done only when: - `templates/default` reflects the live `unity-surfaces` harness roles; +- the authority split is visible: commands collapse hops, checks prove, gates name done checks; - old template-only concepts are removed or intentionally retained with live evidence; - `npm run verify` passes; diff --git a/scripts/verify-template.mjs b/scripts/verify-template.mjs index 86381df..8abb95f 100755 --- a/scripts/verify-template.mjs +++ b/scripts/verify-template.mjs @@ -17,11 +17,11 @@ const requiredFiles = [ ".agents/resolvers/README.md", ".agents/resolvers/agent-tooling.md", ".agents/resolvers/factory-failure.md", - ".agents/resolvers/rule-rinse.md", ".agents/gates/README.md", ".agents/gates/agent-tooling.md", ".agents/gates/factory-failure.md", - ".agents/gates/rule-rinse.md", + ".agents/commands/README.md", + ".agents/checks/README.md", ".agents/skills/README.md", ".agents/skills/agents-kit/SKILL.md", ".agents/skills/agents-kit/scripts/check-agents-kit-health.py", diff --git a/templates/default/.agents/AGENT-CONTROL-PLANE.md b/templates/default/.agents/AGENT-CONTROL-PLANE.md index cca1340..629c064 100644 --- a/templates/default/.agents/AGENT-CONTROL-PLANE.md +++ b/templates/default/.agents/AGENT-CONTROL-PLANE.md @@ -12,10 +12,35 @@ Add friction where silent agent drift is expensive. Remove friction where the re Compression makes the system loadable. Constraint makes it followable. Friction makes it honest. +## Spine + +```text +repeated friction -> owning surface -> smallest durable constraint -> nearest oracle +``` + +Promote only when the miss repeated, cost real time, the owner is known, and a nearby prompt, resolver, gate, command, check, hook, CI job, or package script would have prevented it. + +Do not centralize wisdom. Put the smallest rule where it acts. + ## Rule of Thumb If a cold agent does not need it to orient, route, constrain scope, apply reusable technique, verify done, or resume handoff, it does not belong in `.agents`. +## Public Model + +The public model is deliberately thin: + +```text +agents judge +commands collapse hops +checks prove +memory remembers +``` + +These are public words, not necessarily folders. Do not create `.agents/memory/`. Memory lives as `.agents/logs/*`, `history/lessons/*`, and other dated repo evidence. + +Router, resolvers, and gates are project adapters. Use them when a repo has real ownership hazards, expensive done conditions, or repeated wrong-surface misses. + ## Shape ```text @@ -23,18 +48,20 @@ AGENTS.md boot pointer into the live control plane .agents/router.md task trigger -> resolver -> gate -> skill .agents/resolvers/ decision rules for task shape .agents/gates/ concrete done checks +.agents/commands/ executable hop-collapsers for recurring hot paths +.agents/checks/ agent-process and repo-ownership mechanical oracles .agents/skills/ repo-local techniques .agents/skills/agents-kit/scripts/ control-plane health checks only .agents/logs/ session notes and handoff context history/ dated evidence, audits, decisions history/plans/ completed plans -history/lessons/ captured lesson artifacts; factory-failure owns the lifecycle +history/lessons/ captured lesson artifacts; factory-failure owns the lifecycle ``` `AGENTS.md` points. It does not teach. Boot happens at every new agent session, and the pointed-to `.agents/` files continue to govern the work after startup. -Resolvers decide scope. Gates decide done. Skills hold technique. Logs orient resumption. History explains why something was once believed. Git proves commits and diffs. None of these should impersonate the others. +Resolvers decide scope. Gates name done checks. Commands collapse repeated hops. Checks prove agent-process truth. Skills hold technique. Logs orient resumption. History explains why something was once believed. Git proves commits and diffs. None of these should impersonate the others. ## Authority Map @@ -46,6 +73,8 @@ Every artifact is either live law, current app doctrine, active execution state, | `.agents/router.md` | routing law | classify the task, resolver, gate, and default skill | | `.agents/resolvers/*` | owner and scope law | own reads, allowed writes, non-goals, and cold-agent tests | | `.agents/gates/*` | done law | own observable checks before done is claimed | +| `.agents/commands/*` | executable hop-collapsing entrypoints | run common workflows and call checks; do not hide ownership | +| `.agents/checks/*` | mechanical oracle law | return pass/fail for objective agent-process or repo-ownership invariants | | `.agents/skills/*` | technique | teach repeatable methods; do not hide routing or done rules | | `.agents/logs/*` | handoff receipts | orient resumption; never treat as live law | | repo docs | current domain or product vocabulary | keep terms and relationships current | @@ -76,20 +105,14 @@ A pre-edit ownership receipt is the pre-action callout that proves the agent has Use it for nontrivial edits, and always for durable product code, `.agents` control-plane changes, docs/tracker/skill/history changes, and expensive repo-specific lanes. ```text -Request: -Resolver: -Why this resolver: -Source of truth / evidence order: Owner surface: Allowed writes: Forbidden surfaces: Done gate: -First oracle (fastest relevant check after editing): -Next oracle (stronger/broader check before done): -Skill used (last, if material): +First real check: ``` -The receipt is not a plan, not a session log, and not a new resolver. It can be a short chat update. It prevents stale ownership and wrong-surface edits before action. When multiple sources can disagree, name the evidence order instead of pretending there is only one source. +The receipt is not a plan, not a session log, and not a new resolver. It can be a short chat update. It prevents stale ownership and wrong-surface edits before action. Name evidence order only when sources can disagree. Name a next oracle only when the first check is deliberately narrower than the final done claim. Logs happen after durable work. A log may compress receipt fields into orientation when they help future resumption, but the log does not replace the pre-edit receipt. @@ -102,9 +125,11 @@ When a rule, note, or technique needs a home, place it by function: | What kind of task is this? | `.agents/router.md` | | What scope, reads, writes, and non-goals apply? | `.agents/resolvers/*` | | What makes the work done? | `.agents/gates/*` | +| What loop should the human/agent run? | `.agents/commands/*` | +| What objective invariant proves pass/fail? | `.agents/checks/*` | | What repeatable method or tool technique helps? | `.agents/skills/*` | | What objective control-plane invariant needs repeatable checking? | `.agents/skills/agents-kit/scripts/*` | -| What objective factory or repo invariant needs repeatable checking? | `docs/checks/*` or package scripts | +| What objective product or package invariant needs repeatable checking? | package scripts or tests | | What happened in this run? | `.agents/logs/*` | | What evidence or decision was true at a date? | `history/*` | | What completed plan should be archived? | `history/plans/*` | @@ -119,7 +144,8 @@ Use timing before placement gets fuzzy: | --- | --- | | before editing, to classify scope, reads, allowed writes, or non-goals | resolver | | before claiming done, to prove the task is complete | gate | -| as an objective repeatable filesystem, AST, command, or probe check | script, test, or `docs/checks/*` | +| as a recurring hot path that collapses manual routing or sensor assembly | `.agents/commands/*` | +| as an objective repeatable filesystem, AST, command, or probe check | `.agents/checks/*`, tests, or package scripts | | to explain why the product, app, or boundary is shaped that way | docs or ADR | | to perform a repeatable technique across cases | skill | @@ -172,9 +198,19 @@ Generic skill design is not enough. Repo-local skill design must ask: - Is this reusable across tasks, or just evidence from one run? - Would putting this in a skill make the agent skip a required repo boundary? -## Scripts +## Commands And Checks + +Commands are optional hop-collapsing entrypoints. Checks, gates, product tests, package scripts, and CI own truth. + +A command earns its file when it removes repeated manual routing or sensor assembly without owning the verdict. + +Use `.agents/commands/*` for human-invoked hop-collapsers. A command may gather facts, print receipts, and call checks. It should not become a broad orchestrator. + +Use `.agents/checks/*` for objective agent-process or repo-ownership invariants. Checks exit nonzero on failure. + +Product build/test/runtime checks that engineers run outside agent workflow belong in package scripts or package tests. Docs explain product/domain truth; they do not own executable agent oracles. -Scripts in `.agents` are control-plane health checks, not factory-state sensors or product task runners. +## Agents-Kit Scripts A script belongs under `.agents/skills/agents-kit/scripts/` only when it mechanically checks the `.agents` control plane itself: @@ -186,7 +222,7 @@ Current accepted `agents-kit` scripts: - `check-agents-kit-health.py`: lints the live control-plane structure. -A script does not belong in `.agents` when it checks factory artifacts, builds product code, runs app workflows, performs migration work, manages active project status, or replaces a resolver/gate decision. Put factory and repo checks in `docs/checks/*`; put product commands in package scripts; put one-off evidence in `history/*`. +A script does not belong under `.agents/skills/agents-kit/scripts/` when it checks factory artifacts, builds product code, runs app workflows, performs migration work, manages active project status, or replaces a resolver/gate decision. Put agent/repo checks in `.agents/checks/*`; put product commands in package scripts; put one-off evidence in `history/*`. ## Artifacts @@ -200,7 +236,7 @@ Artifact placement: | Artifact | Home | | --- | --- | -| Active operating rule | `.agents/router.md`, `.agents/resolvers/*`, `.agents/gates/*`, or `.agents/skills/*` | +| Active operating rule | `.agents/router.md`, `.agents/resolvers/*`, `.agents/gates/*`, `.agents/commands/*`, `.agents/checks/*`, or `.agents/skills/*` | | Session handoff note | `.agents/logs/*` | | Completed plan | `history/plans/*` | | Execution ledger | `history/*` | @@ -247,7 +283,7 @@ Use the smallest live surface that will still be read in four weeks: | task classification | `.agents/router.md` | | scope, reads, writes, or non-goals | `.agents/resolvers/*` | | done check | `.agents/gates/*` | -| mechanical invariant | tests or `docs/checks/*` | +| mechanical invariant | tests, package scripts, or `.agents/checks/*` | | run-specific evidence | `history/*` | Prefer updating the existing owning doctrine over adding a second source of truth. @@ -274,6 +310,6 @@ Do not bloat `AGENTS.md` to compensate for weak downstream files. The control plane should be lintable. -A health pass should check that router rows point to real files, every resolver has non-goals and a cold-agent test, every gate has concrete checks, every named skill exists, `history/lessons/` exists, logs are not treated as doctrine, history is not treated as current state, retired runtime-state files are absent from `.agents`, and lesson notes have promotion-state metadata. +A health pass should check that router rows point to real files, every resolver has non-goals and a cold-agent test, every command and gate path points to a real check when it names one, every named skill exists, `history/lessons/` exists, logs are not treated as doctrine, history is not treated as current state, retired runtime-state files are absent from `.agents`, and lesson notes have promotion-state metadata. The health check is not ceremony. It is how the system resists entropy. diff --git a/templates/default/.agents/README.md b/templates/default/.agents/README.md index 339fcb0..dfce984 100644 --- a/templates/default/.agents/README.md +++ b/templates/default/.agents/README.md @@ -2,7 +2,7 @@ Live agent operating files for this repo. -`AGENTS.md` points. Router dispatches. Resolvers scope. Gates verify. Skills teach technique. Logs orient handoff. History is evidence. +`AGENTS.md` points. Router dispatches. Resolvers scope. Gates name proof. Commands collapse hops. Checks prove. Skills teach technique. Logs orient handoff. History is evidence. Rule of thumb: if a cold agent does not need it to orient, route, constrain scope, apply reusable technique, verify done, or resume handoff, it does not belong in `.agents`. @@ -18,7 +18,7 @@ AGENTS.md dispatch table: task trigger -> resolver -> gate -> skill .agents/AGENT-CONTROL-PLANE.md - doctrine for where control-plane rules, gates, skills, logs, history, and lessons belong + doctrine for where control-plane rules, gates, commands, checks, skills, logs, history, and lessons belong .agents/resolvers/ task-specific decision rules @@ -26,6 +26,12 @@ AGENTS.md .agents/gates/ done-means-done checklists +.agents/commands/ + executable hop-collapsers for recurring hot paths + +.agents/checks/ + mechanical pass/fail oracles for agent-process or repo-ownership invariants + .agents/skills/ repo-local techniques loaded only when routed @@ -49,7 +55,7 @@ history/lessons/ Repair classification lives in `.agents/resolvers/factory-failure.md`. -Use this map only for placement after the classifier names the owner: router dispatches, resolvers scope, gates verify, skills teach technique, logs orient handoff, and history preserves evidence. +Use this map only for placement after the classifier names the owner: router dispatches, resolvers scope, gates name proof, commands collapse hops, checks prove, skills teach technique, logs orient handoff, and history preserves evidence. Do not add workflow detail to `AGENTS.md`. Do not learn live rules from `.agents/logs/` or `history/`. diff --git a/templates/default/.agents/checks/README.md b/templates/default/.agents/checks/README.md new file mode 100644 index 0000000..14dbbf9 --- /dev/null +++ b/templates/default/.agents/checks/README.md @@ -0,0 +1,7 @@ +# Checks + +Checks prove objective agent-process or repo-ownership invariants that normal product tests do not naturally express. + +A check should be runnable, deterministic enough to trust, and return nonzero on failure. Gates may name checks; commands may call checks; the check owns the pass/fail detail. + +Do not add a check for behavior the repo cannot observe. If the miss lives only in tool-call choreography or transcript behavior, keep the rule in the narrow resolver until a real oracle exists. diff --git a/templates/default/.agents/commands/README.md b/templates/default/.agents/commands/README.md new file mode 100644 index 0000000..755c369 --- /dev/null +++ b/templates/default/.agents/commands/README.md @@ -0,0 +1,7 @@ +# Commands + +Commands collapse repeated manual hops. They are optional entrypoints for workflows that agents or humans invoke often enough to justify a file. + +A command may gather facts, print receipts, and call checks. It does not own the verdict. Ownership still lives in the selected resolver, gate, check, test, or package script. + +Create a command only when it removes repeated routing or sensor assembly. Keep one-off runbooks in `history/` or task docs. diff --git a/templates/default/.agents/gates/README.md b/templates/default/.agents/gates/README.md index abb82d8..6f43604 100644 --- a/templates/default/.agents/gates/README.md +++ b/templates/default/.agents/gates/README.md @@ -1,7 +1,7 @@ # Gates -Gates decide done. +Gates name done checks. -Each gate should name concrete checks, not vibes. If a check cannot be run, report why and keep the residual risk visible. +Each gate should name concrete checks, commands, probes, or evidence, not vibes. If a check cannot be run, report why and keep the residual risk visible. -Use `.agents/gates/rule-rinse.md` to improve gates themselves. +Use `.agents/resolvers/agent-tooling.md` and `.agents/gates/agent-tooling.md` to improve gates themselves. diff --git a/templates/default/.agents/gates/agent-tooling.md b/templates/default/.agents/gates/agent-tooling.md index 6a03ffe..2bfdcef 100644 --- a/templates/default/.agents/gates/agent-tooling.md +++ b/templates/default/.agents/gates/agent-tooling.md @@ -3,8 +3,8 @@ Done means: - `.agents/skills/agents-kit/` exists when `.agents` control-plane behavior is being edited; -- nontrivial edits in this lane were preceded by a pre-edit ownership receipt naming request, resolver, why this resolver, source of truth or evidence order, owner surface, allowed writes, forbidden surfaces, done gate, first oracle, next oracle, and skill used last when a skill materially shaped the work; -- `python3 .agents/skills/agents-kit/scripts/check-agents-kit-health.py` passes when router, resolver, gate, skill, or lesson-artifact structure changes; +- nontrivial edits used the canonical five-field ownership receipt from `.agents/AGENT-CONTROL-PLANE.md`; evidence order or a next oracle was added only when the task required it; +- `python3 .agents/skills/agents-kit/scripts/check-agents-kit-health.py` passes when router, resolver, gate, command, check, skill, or lesson-artifact structure changes; - captured lesson artifacts live under `history/lessons/`, not `.agents/`; - `skills-lock.json` matches the intended installed skill set; - skill inventory changes prove source-of-truth lookup happened before edits: `skills-lock.json`, existing `.agents/skills//`, and any user-provided install command or upstream docs were checked; @@ -15,6 +15,9 @@ Done means: - touched `history/lessons/*` notes have valid shape and promotion-state metadata; - learning lifecycle decisions point to `.agents/resolvers/factory-failure.md` and close through `.agents/gates/factory-failure.md`, not agent-tooling; - `agents-kit` scripts are limited to objective control-plane health checks; +- objective repo or agent-process checks live under `.agents/checks/*`, not `agents-kit`; +- command files collapse repeated hops and call checks; they do not own verdicts or hide owner selection; +- delegated subagent work follows `.agents/resolvers/agent-tooling.md`: spawn once, report id, keep write sets disjoint, verify the returned result, then close the worker; - `.agents` contains no current-focus or build-loop runtime state; - plan-specific one-off work is not authored as a live resolver/gate; - `git status --short --ignored` shows no accidental local-only staged files. diff --git a/templates/default/.agents/gates/factory-failure.md b/templates/default/.agents/gates/factory-failure.md index d166879..3a4b850 100644 --- a/templates/default/.agents/gates/factory-failure.md +++ b/templates/default/.agents/gates/factory-failure.md @@ -19,7 +19,7 @@ Done means: - no-op with reason; - the selected owner resolver and gate are run when a live surface is patched; - the factory-failure gate is run after the owner gate for live-owner repairs; -- `python3 .agents/skills/agents-kit/scripts/check-agents-kit-health.py` passes when router, resolver, gate, skill, or learning structure changes; +- `python3 .agents/skills/agents-kit/scripts/check-agents-kit-health.py` passes when router, resolver, gate, command, check, skill, or lesson-artifact structure changes; - the relevant repo-owned check passes when repairing task-state drift, or the remaining state blocker is recorded as HITL/blocker; - touched `history/lessons/*` notes record a promotion state: context-only, live-promotion, no-learning, or HITL; - live-promotion is done only when the original miss scenario is replayed as a cold-agent test against the patched surface, or the new mechanical check is shown failing on the pre-patch state, and the replay result is recorded in the lesson note (or the failure log when no lesson note exists); diff --git a/templates/default/.agents/gates/rule-rinse.md b/templates/default/.agents/gates/rule-rinse.md deleted file mode 100644 index 8f4efc3..0000000 --- a/templates/default/.agents/gates/rule-rinse.md +++ /dev/null @@ -1,12 +0,0 @@ -# Rule Rinse Gate - -A rule-rinse pass is done only when: - -- the target rule names its trigger; -- required reads are concrete files or file groups; -- allowed writes and non-goals are explicit; -- the gate has concrete checks; -- at least one cold-agent test exists; -- new routes, gates, boundaries, or slice plans pass the settlement check: proven, provisional, retirement/change trigger, and live-vs-history placement; -- one likely failure mode has been added or tightened; -- the pass is recorded in `.agents/logs/`. diff --git a/templates/default/.agents/resolvers/README.md b/templates/default/.agents/resolvers/README.md index e42de18..3d150d9 100644 --- a/templates/default/.agents/resolvers/README.md +++ b/templates/default/.agents/resolvers/README.md @@ -19,4 +19,4 @@ Each resolver should state: If resolver confidence matters, record an explicit `Last Rinsed` note with the date, scenarios, and log link. Do not leave vague `Status: draft` labels behind as stale metadata. -Use `.agents/resolvers/rule-rinse.md` for that pass. +Use `.agents/resolvers/agent-tooling.md` plus `.agents/AGENT-CONTROL-PLANE.md` for that pass. diff --git a/templates/default/.agents/resolvers/agent-tooling.md b/templates/default/.agents/resolvers/agent-tooling.md index 24dabe6..8fbfe00 100644 --- a/templates/default/.agents/resolvers/agent-tooling.md +++ b/templates/default/.agents/resolvers/agent-tooling.md @@ -2,7 +2,7 @@ ## Trigger -Use for `AGENTS.md`, `.agents/**`, `.scratch/**`, docs or tracker setup, skills, skill locks, lesson artifacts, or host-specific agent setup. +Use for `AGENTS.md`, `.agents/**`, `.scratch/**`, docs or tracker setup, skills, skill locks, lesson artifacts, host-specific agent setup, initial repo setup, package-script plumbing, or resolver/gate/router quality passes. ## Required Reads @@ -12,6 +12,7 @@ Use for `AGENTS.md`, `.agents/**`, `.scratch/**`, docs or tracker setup, skills, - `.agents/router.md` - `.agents/skills/agents-kit/SKILL.md` when changing `.agents` control-plane behavior - relevant skill, docs, tracker, scratch, or host directory +- relevant `.agents/commands/*` or `.agents/checks/*` file when changing commands or checks - `history/lessons/README.md` when changing lesson-artifact shape - `skills-lock.json` when skill inventory changes @@ -20,19 +21,15 @@ Use for `AGENTS.md`, `.agents/**`, `.scratch/**`, docs or tracker setup, skills, Before nontrivial edits in this lane, state the pre-edit ownership receipt: ```text -Request: -Resolver: -Why this resolver: -Source of truth / evidence order: Owner surface: Allowed writes: Forbidden surfaces: Done gate: -First oracle: -Next oracle: -Skill used (last, if material): +First real check: ``` +Name evidence order only when sources can disagree. Name a next oracle only when the first check is deliberately narrower than the final done claim. + The receipt does not replace `.agents/logs/`. If durable control-plane or log doctrine changed, write or update the session log at closeout. ## Skill Inventory Boundary @@ -58,10 +55,22 @@ python3 .agents/skills/agents-kit/scripts/check-skill-frontmatter.py .agents/ski If a repo-local skill is listed in `skills-lock.json`, treat it as an installed mirror. Do not edit that skill or files under its skill directory for local behavior repair unless the user explicitly asks to change that installed skill. Put local operating rules in the narrow live resolver, gate, log doctrine, history learning, or product docs instead. +## Delegated Subagent Work + +When the user asks this agent to run a subagent, treat delegation as agent tooling, not a new skill requirement. + +- Spawn each requested worker once and report the agent id. +- Keep parent and worker write sets disjoint; do not duplicate the worker's assigned work in the parent. +- Do not block the parent on a long synchronous wait unless integration depends on the result; poll briefly, then resume when the worker reports back. +- When the worker returns, inspect its diff or findings, verify the claimed owner surfaces, and close the worker. +- If the same spawn, wait, or duplicated-work miss repeats and a tool-call transcript oracle exists, promote the narrow invariant to `.agents/checks/*`; otherwise route the repair through `.agents/resolvers/factory-failure.md`. + +This does not apply to user-owned external threads; those follow the host thread tooling. + ## Owned Surfaces - agent bootloader; -- router/resolvers/gates/log rules; +- router/resolvers/gates/commands/checks/log rules; - `agents-kit` harness check scripts; - local docs, tracker, and scratch packets; - repo-local skills; @@ -85,6 +94,7 @@ If a repo-local skill is listed in `skills-lock.json`, treat it as an installed - turning `history/lessons/` into live law or a lifecycle owner; - creating `.scratch` only to represent chat-stated focus; - putting current-focus or build-loop runtime state into `.agents`; +- using `.agents/skills/agents-kit/scripts/*` for objective repo checks that belong in `.agents/checks/*`; - using `.agents/skills/agents-kit/scripts/*` for product builds, app workflows, migration runners, active project status, or dashboard sync. ## Gate @@ -99,6 +109,12 @@ If the user states a focus in chat, follow that focus through the router. `.agen If adding or changing an `agents-kit` script, the agent can name the exact control-plane invariant it checks and why that check is objective enough to automate. +If adding or changing an `.agents/checks/*` file, the agent can name the exact invariant it proves and why the repo can observe it objectively. + +If rinsing router/resolver/gate quality, the agent uses this resolver plus `.agents/AGENT-CONTROL-PLANE.md`, patches the narrow owner surface, runs agents-kit health, and does not route through a separate rule-rinse lane. + +Given "run a subagent to review tests", the parent spawns one worker, reports its id, keeps parent edits separate, verifies the returned findings, and closes the worker. It does not do the same review in parallel unless the user asked for redundant reviews. + ## Failure Signs - third-party skills are hand-authored under `.agents/skills/*` instead of installed through the repo skill manager; @@ -108,3 +124,5 @@ If adding or changing an `agents-kit` script, the agent can name the exact contr - chat-stated focus is forced into `.scratch` even though no plan or issue loop exists; - current-focus or build-loop runtime state is added back into `.agents`; - an `agents-kit` script checks product state or runs product work instead of checking the control plane. +- resolver, gate, or router quality work is routed through a separate rule-rinse lane instead of this resolver. +- delegated subagent work is spawned twice, duplicated in the parent, or treated as done without inspecting the worker result. diff --git a/templates/default/.agents/resolvers/factory-failure.md b/templates/default/.agents/resolvers/factory-failure.md index 8320b82..b88f517 100644 --- a/templates/default/.agents/resolvers/factory-failure.md +++ b/templates/default/.agents/resolvers/factory-failure.md @@ -109,11 +109,11 @@ Owner examples: | product regression | selected product resolver, app code, and tests | selected product gate plus factory-failure gate | | stale spec | owning spec, plan, or test | evidence for the stale expectation plus selected owner gate and factory-failure gate | | external blocker | `.scratch/*`, blocker note, or relevant task state | exact HITL/blocker recorded when tracker state changes | -| routing failure | `.agents/router.md` | agent-tooling or rule-rinse gate plus factory-failure gate | -| scope/classification failure | `.agents/resolvers/*` | rule-rinse gate plus factory-failure gate | -| weak gate | `.agents/gates/*` | rule-rinse gate plus factory-failure gate | +| routing failure | `.agents/router.md` | agent-tooling gate plus factory-failure gate | +| scope/classification failure | `.agents/resolvers/*` | agent-tooling gate plus factory-failure gate | +| weak gate | `.agents/gates/*` | agent-tooling gate plus factory-failure gate | | technique gap | `.agents/skills/*` | agent-tooling gate plus factory-failure gate | -| missing mechanical invariant | tests, scripts, or checks owned by the repo | relevant owner gate plus factory-failure gate | +| missing mechanical invariant | `.agents/checks/*`, tests, or package scripts owned by the repo | relevant owner gate plus factory-failure gate | ## Learning Loop @@ -137,12 +137,22 @@ Promotion states: | State | Meaning | Required action | | --- | --- | --- | | no-learning | Run-specific evidence only | keep in normal history/logs; do not create `history/lessons/*` | -| context-only | Useful future context, but no mandatory behavior change | write or update `history/lessons/*` with the reason it is context-only. A context-only lesson must name its consult path — the specific resolver Required Reads line or skill that will surface it on its Applies When route; if no live surface will ever read it, the correct state is live-promotion or HITL, not context-only | +| context-only | Useful future context, but no mandatory behavior change | write or update `history/lessons/*` with the reason it is context-only. A context-only lesson must name its consult path — the specific resolver Required Reads line or skill that will surface it on its Applies-When route; if no live surface will ever read it, the correct state is live-promotion or HITL, not context-only | | live-promotion | Future behavior must change | patch router, resolver, gate, skill, or mechanical check; update the lesson note if one exists | | HITL | Promotion owner or desired behavior is unclear | record the blocker and do not silently create law | A learning loop is incomplete if a note says only "not yet promoted" without explaining whether it is context-only, live-promotion, or HITL. +### Anti-Consolidation Rule + +Do not treat lesson artifacts as a continuously rewritten memory store. + +Preserve source logs and history notes as episodic evidence. Prefer a new dated lesson artifact over broadening an old one. Update an existing lesson only to correct wrong information, add source links, clarify promotion state, or name the live owner surface that changed. + +Do not merge multiple lesson artifacts into a general doctrine note during maintenance. Maintenance must classify each artifact as keep, promote, archive, delete, or HITL. + +If future behavior must change, patch the narrow live owner surface. If it does not, keep the lesson context-only. + ## Allowed Writes - the narrow owner surface selected by the classifier: @@ -151,7 +161,7 @@ A learning loop is incomplete if a note says only "not yet promoted" without exp - `.agents/resolvers/*` for scope/classification failures; - `.agents/gates/*` for weak done checks; - `.agents/skills/*` for repeatable technique gaps; - - repo-owned scripts, tests, or checks for missing mechanical invariants; + - `.agents/checks/*`, tests, or package scripts for missing mechanical invariants; - `.scratch/*` for task-state drift; - `history/lessons/*` for reusable context that is not yet live behavior; - `.agents/logs/*` for handoff context changes. diff --git a/templates/default/.agents/resolvers/rule-rinse.md b/templates/default/.agents/resolvers/rule-rinse.md deleted file mode 100644 index ea0910d..0000000 --- a/templates/default/.agents/resolvers/rule-rinse.md +++ /dev/null @@ -1,60 +0,0 @@ -# Rule Rinse Resolver - -## Trigger - -Use when creating, reviewing, or improving `.agents/router.md`, `.agents/resolvers/*`, `.agents/gates/*`, or agent skill routing. - -## Required Reads - -- The rule being rinsed. -- The matching gate. -- `.agents/AGENT-CONTROL-PLANE.md` when the pass changes control-plane shape, placement, or doctrine. -- Relevant source files and docs for the scenario. -- At least one real prior artifact when available: `history/`, git diff, audit note, or current failing behavior. - -## Owned Surfaces - -- `.agents/router.md` -- `.agents/resolvers/*` -- `.agents/gates/*` -- `.agents/skills/*` when the issue is technique or skill routing -- `.agents/AGENT-CONTROL-PLANE.md` when the issue is placement or doctrine -- `.agents/logs/*` session notes for the rinse - -## Method - -1. State the scenario in one sentence. -2. Pretend to be a cold agent using only `AGENTS.md`, router, resolver, and gate. -3. Identify the first ambiguity, overreach, or missing boundary. -4. Inspect code/docs that would settle it. -5. Patch the narrowest rule file. -6. Add or tighten a cold-agent test. -7. Record the pass in `.agents/logs/`. - -If the user asked for critique only, propose the patch instead of applying it. - -## Allowed Writes - -- The narrowest router, resolver, gate, skill, or control-plane doctrine file needed to prevent the miss. -- `.agents/logs/YYMMDD-*.md` session note for the pass. - -## Non-Goals - -- Do not implement product code during a rule-rinse pass unless the user explicitly switches to implementation. -- Do not broaden `AGENTS.md` to compensate for a weak resolver. -- Do not promote dated `history/` files into live rules. - -## Gate - -`.agents/gates/rule-rinse.md` - -## Cold-Agent Test - -Given a concrete task, a cold agent can identify the resolver, name allowed writes, name non-goals, and choose the verification gate without asking for extra doctrine. - -## Failure Signs - -- The rule sounds plausible but names no owned files. -- The rule has no non-goals. -- The gate says "verify" but gives no concrete checks. -- The resolver would cause broad refactors for a narrow request. diff --git a/templates/default/.agents/router.md b/templates/default/.agents/router.md index b6b88e7..2c983b2 100644 --- a/templates/default/.agents/router.md +++ b/templates/default/.agents/router.md @@ -5,11 +5,11 @@ Use this first. Pick the narrowest matching row. | Trigger | Resolver | Gate | Skill | | --- | --- | --- | --- | | failed checks, repeated misses, stale task state, learning-promotion questions, repair-loop decisions, maintenance-rinse findings | `.agents/resolvers/factory-failure.md` | `.agents/gates/factory-failure.md` | `.agents/skills/agents-kit` when patching `.agents` or learning surfaces; otherwise route by classified owner | -| agent tooling, docs placement, `.agents` control-plane, `.scratch`, PRDs, skills, host-specific agent setup, lesson artifacts | `.agents/resolvers/agent-tooling.md` | `.agents/gates/agent-tooling.md` | `.agents/skills/agents-kit` for `.agents` changes; otherwise matching `.agents/skills/*` | -| resolver/gate/router quality pass | `.agents/resolvers/rule-rinse.md` | `.agents/gates/rule-rinse.md` | none by default | +| agent tooling, docs placement, `.agents` control-plane, `.agents/commands`, `.agents/checks`, `.scratch`, PRDs, skills, host-specific agent setup, lesson artifacts, initial repo setup, package-script plumbing, resolver/gate/router quality pass | `.agents/resolvers/agent-tooling.md` | `.agents/gates/agent-tooling.md` | `.agents/skills/agents-kit` for `.agents` changes; otherwise matching `.agents/skills/*` | ## Priority Rules - Agent tooling work stays in the control-plane lane unless the user explicitly switches into product implementation. +- Resolver, gate, and router quality passes use agent-tooling plus `.agents/AGENT-CONTROL-PLANE.md`; do not create a second live lane for rule cleanup. - Factory failure classifies a miss before repair; it does not replace the selected owner resolver or gate. - Repo-specific product rows should be added only after live repo evidence exists. diff --git a/templates/default/.agents/skills/agents-kit/SKILL.md b/templates/default/.agents/skills/agents-kit/SKILL.md index 1d145cd..c76df22 100644 --- a/templates/default/.agents/skills/agents-kit/SKILL.md +++ b/templates/default/.agents/skills/agents-kit/SKILL.md @@ -15,14 +15,17 @@ Use the nearest live source: 2. `.agents/router.md` owns task routing. 3. `.agents/resolvers/*` owns scope, reads, writes, owners, and non-goals. 4. `.agents/gates/*` owns done checks. -5. `.agents/skills/*` owns repeatable technique. -6. `.agents/logs/*` owns handoff notes, not live law. -7. `history/*` owns dated evidence, not current law. -8. `skills-lock.json` records installed skill mirrors. +5. `.agents/commands/*` owns executable hop-collapsers. +6. `.agents/checks/*` owns objective pass/fail oracles. +7. `.agents/skills/*` owns repeatable technique. +8. `.agents/logs/*` owns handoff notes, not live law. +9. `history/*` owns dated evidence, not current law. +10. `skills-lock.json` records installed skill mirrors. ## Modes - Control-plane edit: change the narrowest `.agents/**`, `AGENTS.md`, `skills-lock.json`, or `history/*` owner surface. +- Command/check edit: change `.agents/commands/*` only for repeated hop-collapsers; change `.agents/checks/*` only for objective oracles the repo can observe. - Skill edit: change `.agents/skills//SKILL.md` only after checking `skills-lock.json` and the existing skill directory. - Health-script edit: change `.agents/skills/agents-kit/scripts/*`; run the local health gate. - Read-only assessment: inspect live files, name owner and oracle, then report without edits. @@ -54,12 +57,14 @@ Put instructions where they act. | What task route applies? | `.agents/router.md` | | What scope, reads, writes, and non-goals apply? | `.agents/resolvers/*` | | What proves done? | `.agents/gates/*` | +| What repeated hop should be collapsed? | `.agents/commands/*` | +| What objective oracle returns pass/fail? | `.agents/checks/*` | | What repeatable method helps? | `.agents/skills/*` | | What objective control-plane invariant needs checking? | `.agents/skills/agents-kit/scripts/*` | | What happened this run? | `.agents/logs/*` | | What dated evidence exists? | `history/*` | -Do not duplicate full control-plane doctrine in this skill. +Do not duplicate full control-plane doctrine in this skill. Delegated subagent choreography belongs in `.agents/resolvers/agent-tooling.md` unless a real transcript or tool-call oracle exists; only then promote the narrow invariant to `.agents/checks/*`. ## Skill Boundary diff --git a/templates/default/.agents/skills/agents-kit/scripts/check-agents-kit-health.py b/templates/default/.agents/skills/agents-kit/scripts/check-agents-kit-health.py index fb41e14..0d7f282 100755 --- a/templates/default/.agents/skills/agents-kit/scripts/check-agents-kit-health.py +++ b/templates/default/.agents/skills/agents-kit/scripts/check-agents-kit-health.py @@ -36,6 +36,16 @@ def extract_agent_paths(router_text: str) -> set[str]: return set(re.findall(r"`((?:\.agents|history)/[^`]+?\.md)`", router_text)) +def extract_check_paths(text: str) -> set[str]: + paths = set() + for match in re.findall(r"(\.agents/checks/[A-Za-z0-9._/-]+)", text): + path = match.rstrip(".,:;)") + if path.endswith("/") or "*" in path: + continue + paths.add(path) + return paths + + def warn(warnings: list[str], message: str) -> None: warnings.append(message) @@ -93,6 +103,22 @@ def check_gates(errors: list[str]) -> None: done_markers = ["Done means", "done only when", "Minimum gate"] if not any(marker in text for marker in done_markers): fail(errors, f"{rel(path)} does not state done criteria") + for check_path in sorted(extract_check_paths(text)): + if not (ROOT / check_path).is_file(): + fail(errors, f"{rel(path)} names missing check: {check_path}") + + +def check_commands(errors: list[str]) -> None: + commands_root = ROOT / ".agents" / "commands" + if not commands_root.is_dir(): + return + for path in sorted(commands_root.iterdir()): + if not path.is_file(): + continue + text = read(path) + for check_path in sorted(extract_check_paths(text)): + if not (ROOT / check_path).is_file(): + fail(errors, f"{rel(path)} names missing check: {check_path}") def check_skills(errors: list[str]) -> None: @@ -181,6 +207,8 @@ def check_control_plane(errors: list[str]) -> None: require_file(errors, "AGENTS.md") require_file(errors, ".agents/README.md") require_file(errors, ".agents/AGENT-CONTROL-PLANE.md") + require_file(errors, ".agents/commands/README.md") + require_file(errors, ".agents/checks/README.md") require_file(errors, "history/lessons/README.md") agents_text = read(ROOT / "AGENTS.md") @@ -194,7 +222,8 @@ def check_control_plane(errors: list[str]) -> None: "Health", "Placement Test", "Rule of Thumb", - "Scripts", + "Commands And Checks", + "Agents-Kit Scripts", "Skills", ]: if f"## {phrase}" not in doctrine: @@ -255,7 +284,7 @@ def check_lessons(errors: list[str]) -> None: if state == "live-promotion": owner_paths = re.findall( - r"`((?:AGENTS\.md|\.agents/(?:router\.md|resolvers/[^`]+|gates/[^`]+|skills/[^`]+|logs/README\.md)|docs/checks/[^`]+|(?:apps|packages)/[^`]+))`", + r"`((?:AGENTS\.md|\.agents/(?:router\.md|resolvers/[^`]+|gates/[^`]+|commands/[^`]+|checks/[^`]+|skills/[^`]+|logs/README\.md)|(?:apps|packages)/[^`]+))`", text, ) if not owner_paths: @@ -279,6 +308,7 @@ def main() -> int: check_router(errors) check_resolvers(errors, warnings) check_gates(errors) + check_commands(errors) check_skills(errors) check_skill_inventory(errors) check_lessons(errors) diff --git a/templates/default/AGENTS.md b/templates/default/AGENTS.md index 844dc46..cfb7e15 100644 --- a/templates/default/AGENTS.md +++ b/templates/default/AGENTS.md @@ -14,8 +14,8 @@ Boot file only. Points to the live `.agents` control plane; do not put workflow - Stage explicit paths only. - Do not commit secrets, local credentials, `.env*`, caches, build output, or copied `node_modules`. -- `.agents/` is the control plane: router, resolvers, gates, logs, skills. -- Repo-specific product, runtime, docs, and tracker rules belong in resolvers, gates, docs, or skills, not in this boot file. +- `.agents/` is the control plane: router, resolvers, gates, commands, checks, logs, skills. +- Repo-specific product, runtime, docs, and tracker rules belong in resolvers, gates, commands, checks, docs, or skills, not in this boot file. ## Repair Rule @@ -28,4 +28,4 @@ If the user says `stop`, `no`, `not that`, or repeats the same correction class: - do not fix forward until the task is re-aligned; - then route repair through `.agents/resolvers/factory-failure.md`. -`AGENTS.md` points. Resolvers decide shape. Gates decide done. Skills hold technique. Logs orient the next session. +`AGENTS.md` points. Resolvers decide shape. Gates name proof. Commands collapse hops. Checks prove. Skills hold technique. Logs orient the next session. diff --git a/templates/default/history/lessons/README.md b/templates/default/history/lessons/README.md index 3be2000..3b8133e 100644 --- a/templates/default/history/lessons/README.md +++ b/templates/default/history/lessons/README.md @@ -20,7 +20,7 @@ Prefer a new dated lesson artifact over rewriting an old one. Update an existing Do not merge lesson artifacts into a grand unified doctrine note. Maintenance passes classify each artifact as keep, promote, archive, delete, or HITL; they do not consolidate lessons by default. -If future behavior must change, patch the narrow live owner surface: router, resolver, gate, skill, script, or check. If behavior does not need to change, keep the lesson context-only and preserve the episode trail. +If future behavior must change, patch the narrow live owner surface: router, resolver, gate, command, check, skill, or script. If behavior does not need to change, keep the lesson context-only and preserve the episode trail. ## Shape From 34ea903ccd13d39300455a4cf29bae594b5ebaa7 Mon Sep 17 00:00:00 2001 From: callumflack Date: Sat, 11 Jul 2026 10:24:09 +1000 Subject: [PATCH 2/4] fix(agents): make seed verification fail closed --- README.md | 6 +- bin/agents-kit.mjs | 1 + docs/dot-agents-transport-spec.md | 9 +- package.json | 2 +- scripts/test-verify-template.mjs | 158 ++++++++++++++++++ scripts/verify-template.mjs | 42 ++++- .../default/.agents/AGENT-CONTROL-PLANE.md | 4 +- templates/default/.agents/README.md | 2 +- .../default/.agents/gates/agent-tooling.md | 6 +- .../.agents/resolvers/agent-tooling.md | 10 +- .../scripts/check-agents-kit-health.py | 127 +++++++++++--- .../default/.agents/skills/manifest.json | 18 ++ 12 files changed, 343 insertions(+), 42 deletions(-) create mode 100644 scripts/test-verify-template.mjs create mode 100644 templates/default/.agents/skills/manifest.json diff --git a/README.md b/README.md index 50533e9..3df0bd1 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ A portable seed for a repo-local `.agents` control plane. Use it as a seed: install the template into a target repo, review the diff, and localize it there. -Treat this repo as a quarry: take the control-plane pieces that fit, but do not make `agents-kit` a runtime dependency. After install, the target repo owns local doctrine; `.agents/skills/agents-kit/**` stays seed-managed unless deliberately forked. +Treat this repo as a quarry: take the control-plane pieces that fit, but do not make `agents-kit` a runtime dependency. After install, the target repo owns local doctrine; `.agents/skills/manifest.json` declares skill ownership, and `.agents/skills/agents-kit/**` stays seed-managed unless deliberately forked. For a visual walkthrough of the harness, open [dot-agents-system.html](dot-agents-system.html). @@ -117,6 +117,7 @@ skills-lock.json README.md skills/ README.md + manifest.json agents-kit/ SKILL.md scripts/ @@ -141,6 +142,7 @@ During `update`, these files are review-only and are not overwritten: ```text AGENTS.md skills-lock.json +.agents/skills/manifest.json .agents/AGENT-CONTROL-PLANE.md .agents/router.md .agents/resolvers/* @@ -180,4 +182,4 @@ In an installed target repo: python3 .agents/skills/agents-kit/scripts/check-agents-kit-health.py ``` -The source verifier checks the transported template file list and runs the installed health check against `templates/default`. +The source verifier self-test proves a clean template passes and rejects conflict markers and invalid skill ownership. The verifier then checks the transported template file list and runs the installed health check against `templates/default`. Pass `--root ` to verify a copied template through the same entrypoint. diff --git a/bin/agents-kit.mjs b/bin/agents-kit.mjs index 12cc3ae..4a3c31a 100755 --- a/bin/agents-kit.mjs +++ b/bin/agents-kit.mjs @@ -79,6 +79,7 @@ function parseArgs(argv) { function isReviewOnly(file) { return file === "AGENTS.md" || file === "skills-lock.json" || + file === ".agents/skills/manifest.json" || file === ".agents/AGENT-CONTROL-PLANE.md" || file === ".agents/router.md" || file.startsWith(".agents/resolvers/") || diff --git a/docs/dot-agents-transport-spec.md b/docs/dot-agents-transport-spec.md index eb54e38..60b5446 100644 --- a/docs/dot-agents-transport-spec.md +++ b/docs/dot-agents-transport-spec.md @@ -42,12 +42,12 @@ Comparing Unity history and the current `agents-kit` template gives this transpo | `dfbec319` | Consolidates command/check/gate authority and retires stale lanes. | Ported: commands/checks surfaces, health coverage, and agent-tooling-owned quality passes. | | `aa3dd514` | Adds delegated subagent flow to `agent-tooling`. | Ported: resolver rule and check-promotion condition. | | `32845c27` | Thins and hardens the Unity control plane after the rinse. | Use as the live comparison point for the portable spine. Keep the five-field receipt, fail-closed proof expectations, and generic lanes; reject Unity product surfaces, roles, loops, hooks, logs, lesson bodies, and installed-skill inventory. | -| `2a7e7645` | Separates repo-owned materialization hashes from installer-owned import metadata. | Adopt the ownership split without fake provenance: a seed manifest declares `agents-kit` and an empty versioned materialization map; `skills-lock.json` remains the import-metadata owner. Implementation and verification remain part of this refactor's done gate. | +| `2a7e7645` | Separates repo-owned materialization hashes from installer-owned import metadata. | Adopted without fake provenance: the seed manifest declares `agents-kit` and an empty versioned materialization map; `skills-lock.json` remains the import-metadata owner. The health check proves the split without interpreting installer `computedHash`. | -Two integrity decisions remain required before this transport is complete: +The two integrity decisions are implemented and covered by the source verification gate: -- The package verifier must reject conflict markers by inspecting shipped template content, and a negative self-test must prove that rejection. `git diff --check` remains an independent maintainer oracle. -- The seed must declare skill ownership in `.agents/skills/manifest.json` without inventing imported skills or hashes. The health checker and installer review-only behavior must validate that contract before the manifest decision can be recorded as implemented. +- The package verifier rejects conflict markers by inspecting shipped template content, and a negative self-test proves that rejection. `git diff --check` remains an independent maintainer oracle. +- The seed declares `agents-kit` ownership in `.agents/skills/manifest.json` without inventing imported skills or hashes. The health checker validates schema, owner overlap, orphan hashes, declared skill presence, and materialized directory ownership; the installer treats the exact manifest path as review-only. ## Codebase Comparison Result @@ -214,6 +214,7 @@ templates/default/ README.md skills/ README.md + manifest.json agents-kit/ SKILL.md scripts/ diff --git a/package.json b/package.json index 8e0b0c2..99cf1c2 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,7 @@ "README.md" ], "scripts": { - "verify": "node scripts/verify-template.mjs", + "verify": "node scripts/test-verify-template.mjs && node scripts/verify-template.mjs", "check": "npm run verify" } } diff --git a/scripts/test-verify-template.mjs b/scripts/test-verify-template.mjs new file mode 100644 index 0000000..e7ffb2a --- /dev/null +++ b/scripts/test-verify-template.mjs @@ -0,0 +1,158 @@ +#!/usr/bin/env node +import assert from "node:assert/strict" +import { spawnSync } from "node:child_process" +import fs from "node:fs/promises" +import os from "node:os" +import path from "node:path" +import { fileURLToPath } from "node:url" + +const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..") +const sourceRoot = path.join(repoRoot, "templates", "default") +const verifier = path.join(repoRoot, "scripts", "verify-template.mjs") + +async function withTemplate(run) { + const parent = await fs.mkdtemp(path.join(os.tmpdir(), "agents-kit-verify-")) + const root = path.join(parent, "template") + try { + await fs.cp(sourceRoot, root, { recursive: true }) + await run(root) + } finally { + await fs.rm(parent, { recursive: true, force: true }) + } +} + +function verify(root) { + return spawnSync(process.execPath, [verifier, "--root", root], { + cwd: repoRoot, + encoding: "utf8", + }) +} + +function output(result) { + return `${result.stdout}${result.stderr}` +} + +async function readManifest(root) { + const manifestPath = path.join(root, ".agents", "skills", "manifest.json") + return { + manifestPath, + manifest: JSON.parse(await fs.readFile(manifestPath, "utf8")), + } +} + +async function writeJson(file, value) { + await fs.writeFile(file, `${JSON.stringify(value, null, 2)}\n`) +} + +await withTemplate(async (root) => { + const result = verify(root) + assert.equal(result.status, 0, output(result)) +}) + +await withTemplate(async (root) => { + const target = path.join(root, ".agents", "README.md") + await fs.appendFile(target, "\n<<<<<<< ours\n||||||| base\n=======\n>>>>>>> theirs\n") + const result = verify(root) + assert.notEqual(result.status, 0, "conflict markers should fail verification") + assert.match(output(result), /conflict marker.*\.agents\/README\.md/i) +}) + +await withTemplate(async (root) => { + const target = path.join(root, ".agents", "README.md") + await fs.appendFile(target, "\n=======\n") + const result = verify(root) + assert.equal(result.status, 0, output(result)) +}) + +await withTemplate(async (root) => { + const { manifestPath, manifest } = await readManifest(root) + manifest.repoSkills["agents-kit"].ownership = "repo" + await writeJson(manifestPath, manifest) + const result = verify(root) + assert.notEqual(result.status, 0, "agents-kit must remain seed-owned") + assert.match(output(result), /repoSkills\.agents-kit ownership must be seed/) +}) + +await withTemplate(async (root) => { + const lockPath = path.join(root, "skills-lock.json") + await writeJson(lockPath, { + skills: { + "agents-kit": { + localPath: ".agents/skills/agents-kit/SKILL.md", + computedHash: "b".repeat(64), + }, + }, + }) + const { manifestPath, manifest } = await readManifest(root) + manifest.materializedImports.hashes["agents-kit"] = "a".repeat(64) + await writeJson(manifestPath, manifest) + const result = verify(root) + assert.notEqual(result.status, 0, "overlapping skill owners should fail") + assert.match(output(result), /skill is both repo-declared and externally locked: agents-kit/) +}) + +await withTemplate(async (root) => { + const { manifestPath, manifest } = await readManifest(root) + manifest.materializedImports.hashes.orphan = "a".repeat(64) + await writeJson(manifestPath, manifest) + const result = verify(root) + assert.notEqual(result.status, 0, "orphan materialization hashes should fail") + assert.match(output(result), /materialization hash has no externally locked skill: orphan/) +}) + +await withTemplate(async (root) => { + const skillPath = path.join(root, ".agents", "skills", "imported", "SKILL.md") + await fs.mkdir(path.dirname(skillPath), { recursive: true }) + await fs.writeFile(skillPath, "---\nname: imported\ndescription: test fixture\n---\n") + await writeJson(path.join(root, "skills-lock.json"), { + skills: { + imported: { + localPath: ".agents/skills/imported/SKILL.md", + computedHash: "c".repeat(64), + }, + }, + }) + const result = verify(root) + assert.notEqual(result.status, 0, "a locked skill without materialization coverage should fail") + assert.match(output(result), /externally locked skill missing materialization hash: imported/) +}) + +await withTemplate(async (root) => { + const skillPath = path.join(root, ".agents", "skills", "imported", "SKILL.md") + await fs.mkdir(path.dirname(skillPath), { recursive: true }) + await fs.writeFile(skillPath, "---\nname: imported\ndescription: test fixture\n---\n") + await writeJson(path.join(root, "skills-lock.json"), { + skills: { + imported: { + localPath: ".agents/skills/imported/SKILL.md", + computedHash: "OPAQUE", + }, + }, + }) + const { manifestPath, manifest } = await readManifest(root) + manifest.materializedImports.hashes.imported = "d".repeat(64) + await writeJson(manifestPath, manifest) + const result = verify(root) + assert.notEqual(result.status, 0, "an invalid computedHash shape should fail") + assert.match(output(result), /computedHash is not 64 lowercase hex: imported/) +}) + +await withTemplate(async (root) => { + const { manifestPath, manifest } = await readManifest(root) + manifest.repoSkills.missing = { ownership: "repo" } + await writeJson(manifestPath, manifest) + const result = verify(root) + assert.notEqual(result.status, 0, "a declared skill without a body should fail") + assert.match(output(result), /repo-declared skill missing SKILL\.md: \.agents\/skills\/missing/) +}) + +await withTemplate(async (root) => { + const skillPath = path.join(root, ".agents", "skills", "unowned", "SKILL.md") + await fs.mkdir(path.dirname(skillPath), { recursive: true }) + await fs.writeFile(skillPath, "---\nname: unowned\ndescription: test fixture\n---\n") + const result = verify(root) + assert.notEqual(result.status, 0, "a materialized directory without an owner should fail") + assert.match(output(result), /skill has no manifest owner: \.agents\/skills\/unowned/) +}) + +console.log("template verifier self-test: PASS") diff --git a/scripts/verify-template.mjs b/scripts/verify-template.mjs index 8abb95f..1ddc1e8 100755 --- a/scripts/verify-template.mjs +++ b/scripts/verify-template.mjs @@ -6,7 +6,15 @@ import process from "node:process" import { fileURLToPath } from "node:url" const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..") -const templateRoot = path.join(repoRoot, "templates", "default") +function parseRoot(argv) { + if (argv.length === 0) return path.join(repoRoot, "templates", "default") + if (argv.length === 2 && argv[0] === "--root" && argv[1]) { + return path.resolve(argv[1]) + } + throw new Error("usage: verify-template.mjs [--root ]") +} + +const templateRoot = parseRoot(process.argv.slice(2)) const requiredFiles = [ "AGENTS.md", @@ -23,6 +31,7 @@ const requiredFiles = [ ".agents/commands/README.md", ".agents/checks/README.md", ".agents/skills/README.md", + ".agents/skills/manifest.json", ".agents/skills/agents-kit/SKILL.md", ".agents/skills/agents-kit/scripts/check-agents-kit-health.py", ".agents/skills/agents-kit/scripts/check-skill-frontmatter.py", @@ -42,6 +51,20 @@ async function exists(relativePath) { } } +async function listFiles(root, base = root) { + const entries = await fs.readdir(root, { withFileTypes: true }) + const files = [] + for (const entry of entries) { + const fullPath = path.join(root, entry.name) + if (entry.isDirectory()) { + files.push(...await listFiles(fullPath, base)) + } else if (entry.isFile()) { + files.push(path.relative(base, fullPath)) + } + } + return files.sort() +} + const missing = [] for (const file of requiredFiles) { if (!await exists(file)) missing.push(file) @@ -53,6 +76,23 @@ if (missing.length > 0) { process.exit(1) } +const conflictMarkers = ["<<<<<<<", "|||||||", ">>>>>>>"] +const conflicts = [] +for (const file of await listFiles(templateRoot)) { + const text = await fs.readFile(path.join(templateRoot, file), "utf8") + for (const marker of conflictMarkers) { + if (text.includes(marker)) conflicts.push({ file, marker }) + } +} + +if (conflicts.length > 0) { + console.error("Template contains conflict markers:") + for (const { file, marker } of conflicts) { + console.error(`- conflict marker ${marker} in ${file}`) + } + process.exit(1) +} + const health = spawnSync("python3", [ path.join(templateRoot, ".agents/skills/agents-kit/scripts/check-agents-kit-health.py"), ], { diff --git a/templates/default/.agents/AGENT-CONTROL-PLANE.md b/templates/default/.agents/AGENT-CONTROL-PLANE.md index 629c064..203cb9e 100644 --- a/templates/default/.agents/AGENT-CONTROL-PLANE.md +++ b/templates/default/.agents/AGENT-CONTROL-PLANE.md @@ -187,7 +187,9 @@ line of the opening boundary block or the final line before the artifact: `Skill used: .agents/skills//SKILL.md`. Omit the line when no skill materially shaped the response. -Installed skills are supply-chain artifacts. Before a skill is added or changed, resolve its owner: `skills-lock.json`, the existing skill directory, and any user-provided install command or upstream docs. If the skill has an upstream installer, use it. Hand-authored skills are for local-only repo technique, and the session log must say that explicitly. +Skill provenance is explicit: `.agents/skills/manifest.json` owns repo and seed skills plus versioned local materialization hashes; `skills-lock.json` owns installer import metadata, whose `computedHash` remains opaque. The owner sets are disjoint, repo declarations win over same-name global skills, and global fallback applies only to undeclared skills. Before changing a skill, inspect both ownership files and its directory. A materialization hash proves local snapshot integrity, not upstream provenance. + +Imported bodies remain tracked until an immutable upstream reference can restore the same materialized bytes. If a skill has an upstream installer, use it. Hand-authored skills are for repo technique and must be declared in `.agents/skills/manifest.json`. Generic skill design is a baseline, not the repo-local control-plane rule. `skill-creator` teaches skill packaging. `.agents/skills/agents-kit` teaches safe surgery on this repo's router, resolvers, gates, skills, logs, history, and lessons. diff --git a/templates/default/.agents/README.md b/templates/default/.agents/README.md index dfce984..20466b0 100644 --- a/templates/default/.agents/README.md +++ b/templates/default/.agents/README.md @@ -33,7 +33,7 @@ AGENTS.md mechanical pass/fail oracles for agent-process or repo-ownership invariants .agents/skills/ - repo-local techniques loaded only when routed + repo-declared techniques plus installed dependency bodies; manifest + lock own provenance .agents/skills/agents-kit/scripts/ control-plane health checks only diff --git a/templates/default/.agents/gates/agent-tooling.md b/templates/default/.agents/gates/agent-tooling.md index 2bfdcef..e87fd8b 100644 --- a/templates/default/.agents/gates/agent-tooling.md +++ b/templates/default/.agents/gates/agent-tooling.md @@ -6,10 +6,10 @@ Done means: - nontrivial edits used the canonical five-field ownership receipt from `.agents/AGENT-CONTROL-PLANE.md`; evidence order or a next oracle was added only when the task required it; - `python3 .agents/skills/agents-kit/scripts/check-agents-kit-health.py` passes when router, resolver, gate, command, check, skill, or lesson-artifact structure changes; - captured lesson artifacts live under `history/lessons/`, not `.agents/`; -- `skills-lock.json` matches the intended installed skill set; -- skill inventory changes prove source-of-truth lookup happened before edits: `skills-lock.json`, existing `.agents/skills//`, and any user-provided install command or upstream docs were checked; +- `.agents/skills/manifest.json`, `skills-lock.json`, and materialized skill directories have disjoint, complete ownership; +- skill inventory changes prove source-of-truth lookup happened before edits: both ownership files, existing `.agents/skills//`, and any user-provided install command or upstream docs were checked; - third-party skills are installed with the repo skill manager, not hand-authored under `.agents/skills/*`; -- hand-authored `.agents/skills//` additions are explicitly logged as local-only skills; +- hand-authored `.agents/skills//` additions are declared as repo-owned skills; - hand-authored or edited local-only skills pass `python3 .agents/skills/agents-kit/scripts/check-skill-frontmatter.py .agents/skills//SKILL.md`; - `AGENTS.md` stays short and points to `.agents/`; - touched `history/lessons/*` notes have valid shape and promotion-state metadata; diff --git a/templates/default/.agents/resolvers/agent-tooling.md b/templates/default/.agents/resolvers/agent-tooling.md index 8fbfe00..ba60f5f 100644 --- a/templates/default/.agents/resolvers/agent-tooling.md +++ b/templates/default/.agents/resolvers/agent-tooling.md @@ -14,7 +14,7 @@ Use for `AGENTS.md`, `.agents/**`, `.scratch/**`, docs or tracker setup, skills, - relevant skill, docs, tracker, scratch, or host directory - relevant `.agents/commands/*` or `.agents/checks/*` file when changing commands or checks - `history/lessons/README.md` when changing lesson-artifact shape -- `skills-lock.json` when skill inventory changes +- `.agents/skills/manifest.json` and `skills-lock.json` when skill inventory changes ## Pre-Edit Ownership Receipt @@ -36,14 +36,14 @@ The receipt does not replace `.agents/logs/`. If durable control-plane or log do Before creating, installing, editing, or routing to a repo skill, identify the source of truth: -1. inspect `skills-lock.json`; +1. inspect `.agents/skills/manifest.json` and `skills-lock.json`; 2. inspect the existing `.agents/skills//` directory if it exists; 3. inspect the user-provided install command or upstream skill docs when the user names one; 4. only then patch router, resolver, gate, skill, or learning surfaces. If the skill is third-party or the user provides an install command, use that installer and let it update `.agents/skills/*` and `skills-lock.json`. Do not hand-author a mirror. -Hand-author `.agents/skills//` only when the task is explicitly to create a new local-only repo skill. Record that local-only decision in the session log. +Hand-author `.agents/skills//` only when the task is explicitly to create repo technique, and declare its ownership in `.agents/skills/manifest.json` in the same change. When hand-authoring or editing a local-only repo skill, run: @@ -53,7 +53,7 @@ python3 .agents/skills/agents-kit/scripts/check-skill-frontmatter.py .agents/ski ## Skill Edit Boundary -If a repo-local skill is listed in `skills-lock.json`, treat it as an installed mirror. Do not edit that skill or files under its skill directory for local behavior repair unless the user explicitly asks to change that installed skill. Put local operating rules in the narrow live resolver, gate, log doctrine, history learning, or product docs instead. +If a skill is listed in `skills-lock.json`, treat it as an installed mirror. Do not edit that skill or files under its skill directory for local behavior repair unless the user explicitly asks to change that installed skill. Put local operating rules in the narrow live resolver, gate, manifest-declared wrapper, history learning, or product docs instead. ## Delegated Subagent Work @@ -85,6 +85,7 @@ This does not apply to user-owned external threads; those follow the host thread - `history/lessons/**`; - host-specific agent mirrors when intended; - `skills-lock.json`; +- `.agents/skills/manifest.json`; - `.gitignore` for local-only agent directories. ## Non-Goals @@ -117,6 +118,7 @@ Given "run a subagent to review tests", the parent spawns one worker, reports it ## Failure Signs +- skill ownership is absent from `.agents/skills/manifest.json` or overlaps `skills-lock.json`; - third-party skills are hand-authored under `.agents/skills/*` instead of installed through the repo skill manager; - installed skill mirrors listed in `skills-lock.json` are patched for local doctrine instead of the owning resolver, gate, docs, or learning surface; - `AGENTS.md` grows into a handbook; diff --git a/templates/default/.agents/skills/agents-kit/scripts/check-agents-kit-health.py b/templates/default/.agents/skills/agents-kit/scripts/check-agents-kit-health.py index 0d7f282..defb452 100755 --- a/templates/default/.agents/skills/agents-kit/scripts/check-agents-kit-health.py +++ b/templates/default/.agents/skills/agents-kit/scripts/check-agents-kit-health.py @@ -137,35 +137,113 @@ def check_skills(errors: list[str]) -> None: fail(errors, f"{rel(skill_md)} missing description") -def locked_skill_paths(errors: list[str]) -> dict[str, str]: - lock_path = ROOT / "skills-lock.json" +def read_json_object(errors: list[str], path: str) -> dict[str, object]: + target = ROOT / path try: - lock = json.loads(lock_path.read_text()) + value = json.loads(target.read_text()) except FileNotFoundError: - fail(errors, "missing file: skills-lock.json") + fail(errors, f"missing file: {path}") return {} except json.JSONDecodeError as error: - fail(errors, f"skills-lock.json is invalid JSON: {error}") + fail(errors, f"{path} is invalid JSON: {error}") + return {} + if not isinstance(value, dict): + fail(errors, f"{path} must contain a JSON object") return {} + return value + + +def locked_skill_paths(errors: list[str], lock_path: str) -> dict[str, str]: + lock = read_json_object(errors, lock_path) skills = lock.get("skills") if not isinstance(skills, dict): - fail(errors, "skills-lock.json missing skills object") + fail(errors, f"{lock_path} missing skills object") return {} locked: dict[str, str] = {} for name, metadata in skills.items(): if not isinstance(metadata, dict): - fail(errors, f"skills-lock.json entry is not an object: {name}") + fail(errors, f"{lock_path} entry is not an object: {name}") continue local_path = metadata.get("localPath") or f".agents/skills/{name}/SKILL.md" + if not isinstance(local_path, str): + fail(errors, f"{lock_path} localPath is not a string: {name}") + continue + computed_hash = metadata.get("computedHash") + if not isinstance(computed_hash, str) or re.fullmatch(r"[0-9a-f]{64}", computed_hash) is None: + fail(errors, f"{lock_path} computedHash is not 64 lowercase hex: {name}") locked[name] = local_path return locked +def repo_skill_manifest( + errors: list[str], +) -> tuple[dict[str, object], dict[str, str], str]: + path = ".agents/skills/manifest.json" + manifest = read_json_object(errors, path) + + if manifest.get("version") != 1: + fail(errors, f"{path} version must be 1") + + external_lock = manifest.get("externalLock") + if external_lock != "skills-lock.json": + fail(errors, f"{path} externalLock must be skills-lock.json") + external_lock = "skills-lock.json" + + resolution = manifest.get("resolution") + expected_resolution = { + "repoDeclaredWins": True, + "globalFallback": "undeclared-only", + "externalBodies": "tracked-until-immutable-ref", + } + if not isinstance(resolution, dict): + fail(errors, f"{path} missing resolution object") + else: + for key, expected in expected_resolution.items(): + if resolution.get(key) != expected: + fail(errors, f"{path} resolution.{key} must be {json.dumps(expected)}") + + materialized = manifest.get("materializedImports") + hashes: dict[str, str] = {} + if not isinstance(materialized, dict): + fail(errors, f"{path} missing materializedImports object") + else: + if materialized.get("algorithm") != "sha256-path-content-v1": + fail(errors, f"{path} materializedImports.algorithm must be sha256-path-content-v1") + raw_hashes = materialized.get("hashes") + if not isinstance(raw_hashes, dict): + fail(errors, f"{path} materializedImports missing hashes object") + else: + for name, value in raw_hashes.items(): + if not isinstance(value, str) or re.fullmatch(r"[0-9a-f]{64}", value) is None: + fail(errors, f"{path} materialization hash is not sha256 hex: {name}") + continue + hashes[name] = value + + repo_skills = manifest.get("repoSkills") + if not isinstance(repo_skills, dict): + fail(errors, f"{path} missing repoSkills object") + repo_skills = {} + else: + for name, metadata in repo_skills.items(): + if not isinstance(metadata, dict): + fail(errors, f"{path} repoSkills entry is not an object: {name}") + continue + if metadata.get("ownership") not in {"seed", "repo", "adopted"}: + fail(errors, f"{path} repoSkills ownership is invalid: {name}") + + agents_kit = repo_skills.get("agents-kit") + if not isinstance(agents_kit, dict) or agents_kit.get("ownership") != "seed": + fail(errors, f"{path} repoSkills.agents-kit ownership must be seed") + + return repo_skills, hashes, external_lock + + def check_skill_inventory(errors: list[str]) -> None: skills_root = ROOT / ".agents" / "skills" - locked = locked_skill_paths(errors) + repo_skills, hashes, external_lock = repo_skill_manifest(errors) + locked = locked_skill_paths(errors, external_lock) if not skills_root.is_dir(): return @@ -178,28 +256,27 @@ def check_skill_inventory(errors: list[str]) -> None: for name, local_path in sorted(locked.items()): if not (ROOT / local_path).is_file(): fail(errors, f"locked skill missing localPath: {name} -> {local_path}") + if name not in hashes: + fail(errors, f"externally locked skill missing materialization hash: {name}") + + for name in sorted(set(repo_skills) & set(locked)): + fail(errors, f"skill is both repo-declared and externally locked: {name}") + + for name in sorted(repo_skills): + if not (skills_root / name / "SKILL.md").is_file(): + fail(errors, f"repo-declared skill missing SKILL.md: .agents/skills/{name}") - log_lines = "\n".join( - read(path) - for path in sorted((ROOT / ".agents" / "logs").glob("*.md")) - ).splitlines() + for name in sorted(hashes): + if name not in locked: + fail(errors, f"materialization hash has no externally locked skill: {name}") + elif name not in skill_dirs: + fail(errors, f"materialization hash has no local skill directory: {name}") for name in sorted(skill_dirs): - if name in locked: - continue - if name == "agents-kit": - continue - local_only_line = False - for line in log_lines: - if re.search(r"\blocal[- ]only\b", line, re.IGNORECASE) and re.search( - rf"`?{re.escape(name)}`?", line - ): - local_only_line = True - break - if not local_only_line: + if name not in locked and name not in repo_skills: fail( errors, - f"unlocked skill lacks local-only log evidence: .agents/skills/{name}", + f"skill has no manifest owner: .agents/skills/{name}", ) diff --git a/templates/default/.agents/skills/manifest.json b/templates/default/.agents/skills/manifest.json new file mode 100644 index 0000000..b648b8e --- /dev/null +++ b/templates/default/.agents/skills/manifest.json @@ -0,0 +1,18 @@ +{ + "version": 1, + "externalLock": "skills-lock.json", + "resolution": { + "repoDeclaredWins": true, + "globalFallback": "undeclared-only", + "externalBodies": "tracked-until-immutable-ref" + }, + "materializedImports": { + "algorithm": "sha256-path-content-v1", + "hashes": {} + }, + "repoSkills": { + "agents-kit": { + "ownership": "seed" + } + } +} From 261e763bf8080f65c90482f4e947f2b8f7ffcafc Mon Sep 17 00:00:00 2001 From: callumflack Date: Sat, 11 Jul 2026 10:26:49 +1000 Subject: [PATCH 3/4] refactor(agents): rinse shipped operator skill --- docs/agents-kit-maintainer.md | 3 + docs/dot-agents-transport-spec.md | 1 + .../.agents/skills/agents-kit/SKILL.md | 95 +++++-------------- 3 files changed, 30 insertions(+), 69 deletions(-) diff --git a/docs/agents-kit-maintainer.md b/docs/agents-kit-maintainer.md index a075242..fe94e2f 100644 --- a/docs/agents-kit-maintainer.md +++ b/docs/agents-kit-maintainer.md @@ -30,12 +30,15 @@ It may mention: - `.agents/resolvers/*` - `.agents/gates/*` - `.agents/skills/*` +- `.agents/skills/manifest.json` - `.agents/logs/*` - `history/*` - `skills-lock.json` - `python3 .agents/skills/agents-kit/scripts/check-skill-frontmatter.py "$PWD/.agents/skills//SKILL.md"` - `python3 .agents/skills/agents-kit/scripts/check-agents-kit-health.py` +It should stay procedural and point to the installed router, control-plane doctrine, resolver, and gate rather than repeat their placement maps. + It must not mention: - `templates/default/**` diff --git a/docs/dot-agents-transport-spec.md b/docs/dot-agents-transport-spec.md index 60b5446..aa97103 100644 --- a/docs/dot-agents-transport-spec.md +++ b/docs/dot-agents-transport-spec.md @@ -48,6 +48,7 @@ The two integrity decisions are implemented and covered by the source verificati - The package verifier rejects conflict markers by inspecting shipped template content, and a negative self-test proves that rejection. `git diff --check` remains an independent maintainer oracle. - The seed declares `agents-kit` ownership in `.agents/skills/manifest.json` without inventing imported skills or hashes. The health checker validates schema, owner overlap, orphan hashes, declared skill presence, and materialized directory ownership; the installer treats the exact manifest path as review-only. +- The shipped `agents-kit` operator skill is rinsed to a short installed-repo procedure: find the nearest owner, inspect dirty state plus both skill ownership records, state the canonical five-field receipt, and run frontmatter and health gates without repeating the control-plane placement map. ## Codebase Comparison Result diff --git a/templates/default/.agents/skills/agents-kit/SKILL.md b/templates/default/.agents/skills/agents-kit/SKILL.md index c76df22..0a0e7ef 100644 --- a/templates/default/.agents/skills/agents-kit/SKILL.md +++ b/templates/default/.agents/skills/agents-kit/SKILL.md @@ -1,100 +1,57 @@ --- name: agents-kit -description: "Use when editing or reviewing this repo's installed .agents control plane, agents-kit bundled health scripts, or repo-local agent doctrine." +description: "Use when editing or reviewing this repo's installed .agents control plane, bundled health scripts, or repo-local agent doctrine." --- # Agents Kit -Maintain this repo's installed `.agents` control plane. Keep this skill procedural. If a rule belongs in the router, resolver, gate, doctrine, script, log, or history, patch that owner instead. +Maintain this repo's installed `.agents` control plane. Use the nearest live owner; do not duplicate its law here. -## Scope +## Orient -Use the nearest live source: +1. Read `AGENTS.md`, `.agents/router.md`, and the routed resolver and gate. +2. Read `.agents/AGENT-CONTROL-PLANE.md` for placement or doctrine changes and `.agents/resolvers/agent-tooling.md` for control-plane, skill, or delegated-agent work. +3. Inspect the exact files you may change before editing. -1. `AGENTS.md` boots the local instructions. -2. `.agents/router.md` owns task routing. -3. `.agents/resolvers/*` owns scope, reads, writes, owners, and non-goals. -4. `.agents/gates/*` owns done checks. -5. `.agents/commands/*` owns executable hop-collapsers. -6. `.agents/checks/*` owns objective pass/fail oracles. -7. `.agents/skills/*` owns repeatable technique. -8. `.agents/logs/*` owns handoff notes, not live law. -9. `history/*` owns dated evidence, not current law. -10. `skills-lock.json` records installed skill mirrors. - -## Modes +## Before Editing -- Control-plane edit: change the narrowest `.agents/**`, `AGENTS.md`, `skills-lock.json`, or `history/*` owner surface. -- Command/check edit: change `.agents/commands/*` only for repeated hop-collapsers; change `.agents/checks/*` only for objective oracles the repo can observe. -- Skill edit: change `.agents/skills//SKILL.md` only after checking `skills-lock.json` and the existing skill directory. -- Health-script edit: change `.agents/skills/agents-kit/scripts/*`; run the local health gate. -- Read-only assessment: inspect live files, name owner and oracle, then report without edits. +Inspect the working tree and both skill ownership records: -## Before Editing +```bash +git status --short +sed -n '1,240p' .agents/skills/manifest.json +sed -n '1,240p' skills-lock.json +``` -State a compact ownership receipt before nontrivial edits: +Keep unrelated dirty files untouched. State the canonical receipt: ```text -Request: -Mode: -Source of truth / evidence order: Owner surface: Allowed writes: Forbidden surfaces: Done gate: -First oracle: -Next oracle: +First real check: ``` -Check `git status --short` first. Keep unrelated dirty files untouched. Stage explicit paths only. - -## Placement Test - -Put instructions where they act. +## Skill Ownership -| If it answers... | Put it in... | -| --- | --- | -| What task route applies? | `.agents/router.md` | -| What scope, reads, writes, and non-goals apply? | `.agents/resolvers/*` | -| What proves done? | `.agents/gates/*` | -| What repeated hop should be collapsed? | `.agents/commands/*` | -| What objective oracle returns pass/fail? | `.agents/checks/*` | -| What repeatable method helps? | `.agents/skills/*` | -| What objective control-plane invariant needs checking? | `.agents/skills/agents-kit/scripts/*` | -| What happened this run? | `.agents/logs/*` | -| What dated evidence exists? | `history/*` | +- Seed-owned skills are declared with `"ownership": "seed"` in `.agents/skills/manifest.json`; preserve their generic target-repo contract. +- Repo-owned skills are declared with `"ownership": "repo"`; edit them only for reusable repo technique. +- Imported skills are recorded in `skills-lock.json`; treat their directories as mirrors and refresh them through their owning source. +- Manifest declarations and lock entries must be disjoint and must account for every materialized skill directory. +- Before changing any skill, inspect its directory and any named source instructions as well as both ownership records. -Do not duplicate full control-plane doctrine in this skill. Delegated subagent choreography belongs in `.agents/resolvers/agent-tooling.md` unless a real transcript or tool-call oracle exists; only then promote the narrow invariant to `.agents/checks/*`. +Put target-local operating law in the routed owner. Do not hide routing or done rules inside a skill. Do not use bundled health scripts for product work or repo checks. -## Skill Boundary +## Verify -Before adding or changing a skill, inspect `skills-lock.json`, the existing skill directory, and any user-provided install command or upstream docs. - -- Treat `.agents/skills/agents-kit/**` as seed-managed whether or not it appears in `skills-lock.json`. Do not edit it for target-local behavior unless the user deliberately forks it. -- Put target-local doctrine in the router, resolver, gate, docs, logs, or history. -- If another skill is listed in `skills-lock.json`, treat it as an installed mirror. Do not hand-edit it for local behavior repair. -- If another skill comes from upstream docs or an external source, refresh it through that source's documented command and keep `skills-lock.json` in sync; do not hand-edit the mirror for local behavior repair. -- Hand-author a skill only when it is explicitly local-only repo technique. - -For local-only skill authoring or edits, run: +Run edited skill frontmatter and installed control-plane health checks: ```bash python3 .agents/skills/agents-kit/scripts/check-skill-frontmatter.py "$PWD/.agents/skills//SKILL.md" -``` - -## Gates - -Run the narrowest real gate that matches the change: - -```bash -python3 .agents/skills/agents-kit/scripts/check-skill-frontmatter.py "$PWD/.agents/skills/agents-kit/SKILL.md" python3 .agents/skills/agents-kit/scripts/check-agents-kit-health.py ``` -## Do Not Reintroduce +Run the routed gate's narrower product, docs, or repository oracle when the task requires one. If a check cannot run, report why and keep the residual risk visible. -- `.agents/active-work.md` -- `.agents/current-work.md` -- `history/solutions/` -- `history/learnings/` -- product task runners, dashboard sync, or app workflow state under `.agents/skills/agents-kit/scripts/` +Do not reintroduce current-focus or build-loop state under `.agents`, old learning or solution directories in place of `history/lessons/`, product runners or workflow state under bundled health scripts, or duplicated control-plane doctrine in this skill. From 15101f89bef205b8d3af949bacff0088b32be5be Mon Sep 17 00:00:00 2001 From: callumflack Date: Sat, 11 Jul 2026 10:38:07 +1000 Subject: [PATCH 4/4] fix(agents): protect legacy skill ownership --- README.md | 7 +- bin/agents-kit.mjs | 43 +++++++++++ docs/dot-agents-transport-spec.md | 8 ++- package.json | 2 +- scripts/test-installer.mjs | 72 +++++++++++++++++++ scripts/test-verify-template.mjs | 65 +++++++++++++++-- scripts/verify-template.mjs | 8 +++ .../default/.agents/AGENT-CONTROL-PLANE.md | 2 +- .../default/.agents/gates/agent-tooling.md | 1 + .../.agents/resolvers/agent-tooling.md | 2 + .../scripts/check-agents-kit-health.py | 39 ++++++++-- .../scripts/hash-materialized-skill.mjs | 37 ++++++++++ 12 files changed, 271 insertions(+), 15 deletions(-) create mode 100644 scripts/test-installer.mjs create mode 100644 templates/default/.agents/skills/agents-kit/scripts/hash-materialized-skill.mjs diff --git a/README.md b/README.md index 3df0bd1..9a2989a 100644 --- a/README.md +++ b/README.md @@ -60,7 +60,7 @@ Use `adopt` when a repo already has local agent files. npx github:callumflack/agents-kit adopt --target /path/to/repo ``` -`adopt` creates missing seed files only. Existing local files are reported as `keep local`; differing files get review diffs. Merge useful seed doctrine manually. +`adopt` creates missing seed files only. Existing local files are reported as `keep local`; differing files get review diffs. Merge useful seed doctrine manually. If an older target has imported or materialized skills but no `.agents/skills/manifest.json`, `adopt` refuses to invent owners or materialization hashes; create and verify the manifest explicitly first. ### Update @@ -70,7 +70,7 @@ Use `update` to roll an existing installation forward from this seed. npx github:callumflack/agents-kit update --target /path/to/repo ``` -`update` requires a clean target git worktree before writing. It creates missing files, keeps local doctrine by default, and prints review diffs for changed files. +`update` requires a clean target git worktree before writing. It creates missing files, keeps local doctrine by default, and prints review diffs for changed files. The same fail-closed manifest preflight protects older skill inventories. Replace changed non-doctrine seed files: @@ -123,6 +123,7 @@ skills-lock.json scripts/ check-agents-kit-health.py check-skill-frontmatter.py + hash-materialized-skill.mjs logs/ README.md .scratch/ @@ -182,4 +183,4 @@ In an installed target repo: python3 .agents/skills/agents-kit/scripts/check-agents-kit-health.py ``` -The source verifier self-test proves a clean template passes and rejects conflict markers and invalid skill ownership. The verifier then checks the transported template file list and runs the installed health check against `templates/default`. Pass `--root ` to verify a copied template through the same entrypoint. +The source verifier self-tests prove clean installation and template health, legacy-upgrade refusal, conflict-marker rejection, shipped seed ownership, generic installed ownership, path confinement, and materialized-import hashes. The verifier then checks the transported template file list and runs the installed health check against `templates/default`. Pass `--root ` to verify a copied template through the same entrypoint. diff --git a/bin/agents-kit.mjs b/bin/agents-kit.mjs index 4a3c31a..7fb0b25 100755 --- a/bin/agents-kit.mjs +++ b/bin/agents-kit.mjs @@ -126,6 +126,47 @@ async function sameFile(source, target) { } } +async function assertManifestUpgradeSafe(targetRoot) { + const manifestPath = path.join(targetRoot, ".agents", "skills", "manifest.json") + if (await exists(manifestPath)) return + + const lockPath = path.join(targetRoot, "skills-lock.json") + let lockedNames = [] + if (await exists(lockPath)) { + let lock + try { + lock = JSON.parse(await fs.readFile(lockPath, "utf8")) + } catch (error) { + throw new Error(`cannot inspect legacy skills-lock.json: ${error.message}`) + } + if (!lock?.skills || typeof lock.skills !== "object" || Array.isArray(lock.skills)) { + throw new Error("cannot inspect legacy skills-lock.json: missing skills object") + } + lockedNames = Object.keys(lock.skills) + } + + const skillsRoot = path.join(targetRoot, ".agents", "skills") + let materializedNames = [] + try { + const entries = await fs.readdir(skillsRoot, { withFileTypes: true }) + materializedNames = entries + .filter((entry) => (entry.isDirectory() || entry.isSymbolicLink()) && entry.name !== "agents-kit") + .map((entry) => entry.name) + } catch (error) { + if (error.code !== "ENOENT") throw error + } + + if (lockedNames.length === 0 && materializedNames.length === 0) return + + const details = [] + if (lockedNames.length > 0) details.push(`locked imports: ${lockedNames.sort().join(", ")}`) + if (materializedNames.length > 0) details.push(`materialized skill directories: ${materializedNames.sort().join(", ")}`) + throw new Error( + `refusing to create .agents/skills/manifest.json in an existing skill inventory (${details.join("; ")}). ` + + "The installer will not infer ownership or materialization hashes. Create and verify the manifest explicitly, then rerun.", + ) +} + function printDiff(source, target, file) { const diff = spawnSync("diff", [ "-u", @@ -249,6 +290,7 @@ async function adopt(options) { } const targetRoot = path.resolve(options.target) + await assertManifestUpgradeSafe(targetRoot) const files = await listFiles(templateRoot) const skipped = [] @@ -295,6 +337,7 @@ async function adopt(options) { async function update(options) { const targetRoot = path.resolve(options.target) assertCleanWorktree(targetRoot, options) + await assertManifestUpgradeSafe(targetRoot) const files = await listFiles(templateRoot) const changed = [] diff --git a/docs/dot-agents-transport-spec.md b/docs/dot-agents-transport-spec.md index aa97103..18b2dae 100644 --- a/docs/dot-agents-transport-spec.md +++ b/docs/dot-agents-transport-spec.md @@ -47,7 +47,7 @@ Comparing Unity history and the current `agents-kit` template gives this transpo The two integrity decisions are implemented and covered by the source verification gate: - The package verifier rejects conflict markers by inspecting shipped template content, and a negative self-test proves that rejection. `git diff --check` remains an independent maintainer oracle. -- The seed declares `agents-kit` ownership in `.agents/skills/manifest.json` without inventing imported skills or hashes. The health checker validates schema, owner overlap, orphan hashes, declared skill presence, and materialized directory ownership; the installer treats the exact manifest path as review-only. +- The seed declares `agents-kit` ownership in `.agents/skills/manifest.json` without inventing imported skills or hashes. The source verifier enforces that seed default; installed health remains generic for deliberate forks and validates schema, owner overlap, confined lock paths, computed materialization hashes, declared skill presence, and materialized directory ownership. The installer treats the exact manifest path as review-only and refuses to infer a missing manifest for an existing skill inventory. - The shipped `agents-kit` operator skill is rinsed to a short installed-repo procedure: find the nearest owner, inspect dirty state plus both skill ownership records, state the canonical five-field receipt, and run frontmatter and health gates without repeating the control-plane placement map. ## Codebase Comparison Result @@ -221,6 +221,7 @@ templates/default/ scripts/ check-agents-kit-health.py check-skill-frontmatter.py + hash-materialized-skill.mjs logs/ README.md history/ @@ -333,7 +334,8 @@ Port these live concepts directly: - verify gates state done criteria; - verify gates and commands only name existing `.agents/checks/*` paths; - verify skill frontmatter; - - verify locked skill paths when `skills-lock.json` exists; + - verify locked skill paths stay inside the repo when `skills-lock.json` exists; + - compare every imported body with its `sha256-path-content-v1` materialization hash while treating installer `computedHash` as opaque metadata; - verify `AGENTS.md` remains short; - verify `history/lessons/README.md`; - fail if retired runtime state returns to `.agents`; @@ -378,12 +380,14 @@ template. - never overwrites; - creates missing seed files; +- refuses to create a missing skill manifest when lock imports or non-seed skill directories already exist; - prints compact review diffs for conflicting local files. `update`: - requires clean target worktree unless `--force`; - creates missing seed files; +- refuses to create a missing skill manifest when lock imports or non-seed skill directories already exist; - keeps local doctrine by default; - supports `--overwrite` only for non-doctrine files; - prints compact review diffs for local doctrine. diff --git a/package.json b/package.json index 99cf1c2..522379a 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,7 @@ "README.md" ], "scripts": { - "verify": "node scripts/test-verify-template.mjs && node scripts/verify-template.mjs", + "verify": "node scripts/test-installer.mjs && node scripts/test-verify-template.mjs && node scripts/verify-template.mjs", "check": "npm run verify" } } diff --git a/scripts/test-installer.mjs b/scripts/test-installer.mjs new file mode 100644 index 0000000..7db20ff --- /dev/null +++ b/scripts/test-installer.mjs @@ -0,0 +1,72 @@ +#!/usr/bin/env node +import assert from "node:assert/strict" +import { spawnSync } from "node:child_process" +import fs from "node:fs/promises" +import os from "node:os" +import path from "node:path" +import { fileURLToPath } from "node:url" + +const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..") +const installer = path.join(repoRoot, "bin", "agents-kit.mjs") +const manifest = ".agents/skills/manifest.json" + +async function withTarget(run) { + const root = await fs.mkdtemp(path.join(os.tmpdir(), "agents-kit-installer-")) + try { + await run(root) + } finally { + await fs.rm(root, { recursive: true, force: true }) + } +} + +async function write(root, relative, content) { + const target = path.join(root, relative) + await fs.mkdir(path.dirname(target), { recursive: true }) + await fs.writeFile(target, content) +} + +function run(command, root) { + return spawnSync(process.execPath, [installer, command, "--target", root], { + cwd: repoRoot, + encoding: "utf8", + }) +} + +function output(result) { + return `${result.stdout}${result.stderr}` +} + +async function assertRefused(result, root) { + assert.notEqual(result.status, 0, "legacy inventory should be refused") + assert.match(output(result), /installer will not infer ownership or materialization hashes/i) + await assert.rejects(fs.access(path.join(root, manifest))) +} + +await withTarget(async (root) => { + await write(root, "skills-lock.json", JSON.stringify({ + skills: { + imported: { computedHash: "a".repeat(64) }, + }, + })) + await assertRefused(run("adopt", root), root) +}) + +await withTarget(async (root) => { + await write(root, "skills-lock.json", JSON.stringify({ skills: {} })) + await write(root, ".agents/skills/imported/SKILL.md", "fixture\n") + await assertRefused(run("update", root), root) +}) + +await withTarget(async (root) => { + const result = run("adopt", root) + assert.equal(result.status, 0, output(result)) + await fs.access(path.join(root, manifest)) +}) + +await withTarget(async (root) => { + const result = run("update", root) + assert.equal(result.status, 0, output(result)) + await fs.access(path.join(root, manifest)) +}) + +console.log("installer self-test: PASS") diff --git a/scripts/test-verify-template.mjs b/scripts/test-verify-template.mjs index e7ffb2a..b4ac4d6 100644 --- a/scripts/test-verify-template.mjs +++ b/scripts/test-verify-template.mjs @@ -9,6 +9,8 @@ import { fileURLToPath } from "node:url" const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..") const sourceRoot = path.join(repoRoot, "templates", "default") const verifier = path.join(repoRoot, "scripts", "verify-template.mjs") +const importedSkillBody = "---\nname: imported\ndescription: fixture\n---\n" +const importedMaterializedHash = "b7e73bd6ee8643f059062b27771430221472c1ede175fae683b176434b038122" async function withTemplate(run) { const parent = await fs.mkdtemp(path.join(os.tmpdir(), "agents-kit-verify-")) @@ -28,6 +30,15 @@ function verify(root) { }) } +function health(root) { + return spawnSync("python3", [ + path.join(root, ".agents", "skills", "agents-kit", "scripts", "check-agents-kit-health.py"), + ], { + cwd: root, + encoding: "utf8", + }) +} + function output(result) { return `${result.stdout}${result.stderr}` } @@ -68,9 +79,11 @@ await withTemplate(async (root) => { const { manifestPath, manifest } = await readManifest(root) manifest.repoSkills["agents-kit"].ownership = "repo" await writeJson(manifestPath, manifest) + const installedResult = health(root) + assert.equal(installedResult.status, 0, output(installedResult)) const result = verify(root) - assert.notEqual(result.status, 0, "agents-kit must remain seed-owned") - assert.match(output(result), /repoSkills\.agents-kit ownership must be seed/) + assert.notEqual(result.status, 0, "the shipped seed must keep agents-kit seed-owned") + assert.match(output(result), /Shipped template manifest must declare agents-kit ownership seed/) }) await withTemplate(async (root) => { @@ -103,7 +116,7 @@ await withTemplate(async (root) => { await withTemplate(async (root) => { const skillPath = path.join(root, ".agents", "skills", "imported", "SKILL.md") await fs.mkdir(path.dirname(skillPath), { recursive: true }) - await fs.writeFile(skillPath, "---\nname: imported\ndescription: test fixture\n---\n") + await fs.writeFile(skillPath, importedSkillBody) await writeJson(path.join(root, "skills-lock.json"), { skills: { imported: { @@ -120,7 +133,7 @@ await withTemplate(async (root) => { await withTemplate(async (root) => { const skillPath = path.join(root, ".agents", "skills", "imported", "SKILL.md") await fs.mkdir(path.dirname(skillPath), { recursive: true }) - await fs.writeFile(skillPath, "---\nname: imported\ndescription: test fixture\n---\n") + await fs.writeFile(skillPath, importedSkillBody) await writeJson(path.join(root, "skills-lock.json"), { skills: { imported: { @@ -137,6 +150,50 @@ await withTemplate(async (root) => { assert.match(output(result), /computedHash is not 64 lowercase hex: imported/) }) +await withTemplate(async (root) => { + const skillPath = path.join(root, ".agents", "skills", "imported", "SKILL.md") + await fs.mkdir(path.dirname(skillPath), { recursive: true }) + await fs.writeFile(skillPath, importedSkillBody) + await writeJson(path.join(root, "skills-lock.json"), { + skills: { + imported: { + localPath: ".agents/skills/imported/SKILL.md", + computedHash: "e".repeat(64), + }, + }, + }) + const { manifestPath, manifest } = await readManifest(root) + manifest.materializedImports.hashes.imported = importedMaterializedHash + await writeJson(manifestPath, manifest) + const result = verify(root) + assert.equal(result.status, 0, output(result)) + + await fs.appendFile(skillPath, "# drift\n") + const drift = verify(root) + assert.notEqual(drift.status, 0, "materialized import drift should fail") + assert.match(output(drift), /materialized skill hash drift: imported/) +}) + +await withTemplate(async (root) => { + const skillPath = path.join(root, ".agents", "skills", "imported", "SKILL.md") + await fs.mkdir(path.dirname(skillPath), { recursive: true }) + await fs.writeFile(skillPath, importedSkillBody) + await writeJson(path.join(root, "skills-lock.json"), { + skills: { + imported: { + localPath: "../outside/SKILL.md", + computedHash: "f".repeat(64), + }, + }, + }) + const { manifestPath, manifest } = await readManifest(root) + manifest.materializedImports.hashes.imported = importedMaterializedHash + await writeJson(manifestPath, manifest) + const result = verify(root) + assert.notEqual(result.status, 0, "a localPath outside the repo should fail") + assert.match(output(result), /localPath escapes repo root: imported -> \.\.\/outside\/SKILL\.md/) +}) + await withTemplate(async (root) => { const { manifestPath, manifest } = await readManifest(root) manifest.repoSkills.missing = { ownership: "repo" } diff --git a/scripts/verify-template.mjs b/scripts/verify-template.mjs index 1ddc1e8..482ad3b 100755 --- a/scripts/verify-template.mjs +++ b/scripts/verify-template.mjs @@ -35,6 +35,7 @@ const requiredFiles = [ ".agents/skills/agents-kit/SKILL.md", ".agents/skills/agents-kit/scripts/check-agents-kit-health.py", ".agents/skills/agents-kit/scripts/check-skill-frontmatter.py", + ".agents/skills/agents-kit/scripts/hash-materialized-skill.mjs", ".agents/logs/README.md", ".scratch/README.md", "history/README.md", @@ -93,6 +94,13 @@ if (conflicts.length > 0) { process.exit(1) } +const manifestPath = path.join(templateRoot, ".agents/skills/manifest.json") +const manifest = JSON.parse(await fs.readFile(manifestPath, "utf8")) +if (manifest.repoSkills?.["agents-kit"]?.ownership !== "seed") { + console.error("Shipped template manifest must declare agents-kit ownership seed") + process.exit(1) +} + const health = spawnSync("python3", [ path.join(templateRoot, ".agents/skills/agents-kit/scripts/check-agents-kit-health.py"), ], { diff --git a/templates/default/.agents/AGENT-CONTROL-PLANE.md b/templates/default/.agents/AGENT-CONTROL-PLANE.md index 203cb9e..f031847 100644 --- a/templates/default/.agents/AGENT-CONTROL-PLANE.md +++ b/templates/default/.agents/AGENT-CONTROL-PLANE.md @@ -189,7 +189,7 @@ materially shaped the response. Skill provenance is explicit: `.agents/skills/manifest.json` owns repo and seed skills plus versioned local materialization hashes; `skills-lock.json` owns installer import metadata, whose `computedHash` remains opaque. The owner sets are disjoint, repo declarations win over same-name global skills, and global fallback applies only to undeclared skills. Before changing a skill, inspect both ownership files and its directory. A materialization hash proves local snapshot integrity, not upstream provenance. -Imported bodies remain tracked until an immutable upstream reference can restore the same materialized bytes. If a skill has an upstream installer, use it. Hand-authored skills are for repo technique and must be declared in `.agents/skills/manifest.json`. +Imported bodies remain tracked until an immutable upstream reference can restore the same materialized bytes. `sha256-path-content-v1` recursively hashes regular files except `.git` and `node_modules`, ordered by POSIX-relative path, feeding each path then its raw bytes with no separator; health recomputes it and rejects drift. If a skill has an upstream installer, use it. Hand-authored skills are for repo technique and must be declared in `.agents/skills/manifest.json`. Generic skill design is a baseline, not the repo-local control-plane rule. `skill-creator` teaches skill packaging. `.agents/skills/agents-kit` teaches safe surgery on this repo's router, resolvers, gates, skills, logs, history, and lessons. diff --git a/templates/default/.agents/gates/agent-tooling.md b/templates/default/.agents/gates/agent-tooling.md index e87fd8b..337ebe2 100644 --- a/templates/default/.agents/gates/agent-tooling.md +++ b/templates/default/.agents/gates/agent-tooling.md @@ -7,6 +7,7 @@ Done means: - `python3 .agents/skills/agents-kit/scripts/check-agents-kit-health.py` passes when router, resolver, gate, command, check, skill, or lesson-artifact structure changes; - captured lesson artifacts live under `history/lessons/`, not `.agents/`; - `.agents/skills/manifest.json`, `skills-lock.json`, and materialized skill directories have disjoint, complete ownership; +- imported skill materialization hashes match `sha256-path-content-v1`, and locked `localPath` values remain inside the repo; - skill inventory changes prove source-of-truth lookup happened before edits: both ownership files, existing `.agents/skills//`, and any user-provided install command or upstream docs were checked; - third-party skills are installed with the repo skill manager, not hand-authored under `.agents/skills/*`; - hand-authored `.agents/skills//` additions are declared as repo-owned skills; diff --git a/templates/default/.agents/resolvers/agent-tooling.md b/templates/default/.agents/resolvers/agent-tooling.md index ba60f5f..46c68f3 100644 --- a/templates/default/.agents/resolvers/agent-tooling.md +++ b/templates/default/.agents/resolvers/agent-tooling.md @@ -43,6 +43,8 @@ Before creating, installing, editing, or routing to a repo skill, identify the s If the skill is third-party or the user provides an install command, use that installer and let it update `.agents/skills/*` and `skills-lock.json`. Do not hand-author a mirror. +When adopting or updating the control plane in an older repo, do not infer a missing manifest from existing lock entries or skill directories. Declare owners and independently derived materialization hashes explicitly before continuing. + Hand-author `.agents/skills//` only when the task is explicitly to create repo technique, and declare its ownership in `.agents/skills/manifest.json` in the same change. When hand-authoring or editing a local-only repo skill, run: diff --git a/templates/default/.agents/skills/agents-kit/scripts/check-agents-kit-health.py b/templates/default/.agents/skills/agents-kit/scripts/check-agents-kit-health.py index defb452..bfa9c71 100755 --- a/templates/default/.agents/skills/agents-kit/scripts/check-agents-kit-health.py +++ b/templates/default/.agents/skills/agents-kit/scripts/check-agents-kit-health.py @@ -4,6 +4,7 @@ import re import sys import json +import subprocess from pathlib import Path @@ -170,6 +171,12 @@ def locked_skill_paths(errors: list[str], lock_path: str) -> dict[str, str]: if not isinstance(local_path, str): fail(errors, f"{lock_path} localPath is not a string: {name}") continue + resolved_path = (ROOT / local_path).resolve() + try: + resolved_path.relative_to(ROOT.resolve()) + except ValueError: + fail(errors, f"{lock_path} localPath escapes repo root: {name} -> {local_path}") + continue computed_hash = metadata.get("computedHash") if not isinstance(computed_hash, str) or re.fullmatch(r"[0-9a-f]{64}", computed_hash) is None: fail(errors, f"{lock_path} computedHash is not 64 lowercase hex: {name}") @@ -233,13 +240,29 @@ def repo_skill_manifest( if metadata.get("ownership") not in {"seed", "repo", "adopted"}: fail(errors, f"{path} repoSkills ownership is invalid: {name}") - agents_kit = repo_skills.get("agents-kit") - if not isinstance(agents_kit, dict) or agents_kit.get("ownership") != "seed": - fail(errors, f"{path} repoSkills.agents-kit ownership must be seed") - return repo_skills, hashes, external_lock +def materialized_skill_hash(errors: list[str], name: str, directory: Path) -> str: + helper = ROOT / ".agents/skills/agents-kit/scripts/hash-materialized-skill.mjs" + try: + result = subprocess.run( + ["node", str(helper), str(directory)], + capture_output=True, + text=True, + check=False, + ) + except OSError as error: + fail(errors, f"could not hash materialized skill {name}: {error}") + return "" + actual = result.stdout.strip() + if result.returncode != 0 or re.fullmatch(r"[0-9a-f]{64}", actual) is None: + detail = result.stderr.strip() or f"invalid helper output: {actual}" + fail(errors, f"could not hash materialized skill {name}: {detail}") + return "" + return actual + + def check_skill_inventory(errors: list[str]) -> None: skills_root = ROOT / ".agents" / "skills" repo_skills, hashes, external_lock = repo_skill_manifest(errors) @@ -271,6 +294,13 @@ def check_skill_inventory(errors: list[str]) -> None: fail(errors, f"materialization hash has no externally locked skill: {name}") elif name not in skill_dirs: fail(errors, f"materialization hash has no local skill directory: {name}") + else: + actual = materialized_skill_hash(errors, name, skill_dirs[name]) + if actual and actual != hashes[name]: + fail( + errors, + f"materialized skill hash drift: {name} expected {hashes[name]} got {actual}", + ) for name in sorted(skill_dirs): if name not in locked and name not in repo_skills: @@ -286,6 +316,7 @@ def check_control_plane(errors: list[str]) -> None: require_file(errors, ".agents/AGENT-CONTROL-PLANE.md") require_file(errors, ".agents/commands/README.md") require_file(errors, ".agents/checks/README.md") + require_file(errors, ".agents/skills/agents-kit/scripts/hash-materialized-skill.mjs") require_file(errors, "history/lessons/README.md") agents_text = read(ROOT / "AGENTS.md") diff --git a/templates/default/.agents/skills/agents-kit/scripts/hash-materialized-skill.mjs b/templates/default/.agents/skills/agents-kit/scripts/hash-materialized-skill.mjs new file mode 100644 index 0000000..8ec6190 --- /dev/null +++ b/templates/default/.agents/skills/agents-kit/scripts/hash-materialized-skill.mjs @@ -0,0 +1,37 @@ +#!/usr/bin/env node +import { createHash } from "node:crypto" +import { readdir, readFile } from "node:fs/promises" +import path from "node:path" +import process from "node:process" + +async function collectFiles(base, current, files) { + for (const entry of await readdir(current, { withFileTypes: true })) { + if (entry.name === ".git" || entry.name === "node_modules") continue + const absolute = path.join(current, entry.name) + if (entry.isDirectory()) { + await collectFiles(base, absolute, files) + } else if (entry.isFile()) { + files.push({ + relative: path.relative(base, absolute).split(path.sep).join("/"), + content: await readFile(absolute), + }) + } + } +} + +const directory = process.argv[2] +if (!directory || process.argv.length !== 3) { + console.error("usage: hash-materialized-skill.mjs ") + process.exit(1) +} + +const files = [] +await collectFiles(directory, directory, files) +files.sort((a, b) => a.relative.localeCompare(b.relative)) + +const hash = createHash("sha256") +for (const file of files) { + hash.update(file.relative) + hash.update(file.content) +} +console.log(hash.digest("hex"))