@cairnkeep/cli 2.6.0 → 2.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (80) hide show
  1. package/CHANGELOG.md +35 -1
  2. package/README.md +45 -8
  3. package/bin/cairn +21 -3
  4. package/claude/commands/graphify.md +46 -55
  5. package/docs/design/meta-agent.md +39 -35
  6. package/docs/harness-compatibility.md +13 -5
  7. package/docs/learning/CURRICULUM-MAP.md +10 -3
  8. package/docs/learning/FEATURE-GUIDE.md +8 -5
  9. package/docs/learning/README.md +16 -14
  10. package/docs/learning/lessons/L00-why-cairnkeep.md +1 -5
  11. package/docs/learning/lessons/L01-safe-trial.md +3 -7
  12. package/docs/learning/lessons/L02-installation.md +6 -8
  13. package/docs/learning/lessons/L03-first-project.md +1 -5
  14. package/docs/learning/lessons/L04-memory-fundamentals.md +0 -6
  15. package/docs/learning/lessons/L05-daily-workflow.md +0 -5
  16. package/docs/learning/lessons/L06-repository-quality.md +0 -6
  17. package/docs/learning/lessons/L07-privacy-and-storage.md +0 -5
  18. package/docs/learning/lessons/L08-multiple-machines.md +0 -5
  19. package/docs/learning/lessons/L09-domain-knowledge.md +0 -5
  20. package/docs/learning/lessons/L10-context-exploration.md +0 -5
  21. package/docs/learning/lessons/L11-containers.md +0 -5
  22. package/docs/learning/lessons/L12-managed-overlays.md +0 -6
  23. package/docs/learning/lessons/L13-session-evidence.md +0 -6
  24. package/docs/learning/lessons/L14-typed-memory.md +0 -6
  25. package/docs/learning/lessons/L15-capability-governance.md +0 -6
  26. package/docs/learning/lessons/L16-evaluation.md +0 -7
  27. package/docs/learning/lessons/L17-agent-boundary.md +12 -14
  28. package/docs/learning/lessons/L18-local-code-graph.md +97 -0
  29. package/docs/learning/lessons/L19-validated-skill-improvement.md +87 -0
  30. package/docs/learning/templates/lesson-template.md +0 -4
  31. package/docs/learning/tracks/evidence-and-evaluation.md +5 -3
  32. package/docs/learning/tracks/practitioner.md +5 -4
  33. package/docs/operating.md +102 -9
  34. package/docs/privacy-and-data-flow.md +42 -0
  35. package/docs/skill-improvement.md +124 -0
  36. package/docs/storage.md +25 -0
  37. package/examples/eval/bundled-fake.json +1 -1
  38. package/kimi/skills/graphify/SKILL.md +33 -0
  39. package/mcp-memory-server/dist/capability-registry.js +2 -2
  40. package/mcp-memory-server/dist/eval-plan.js +22 -5
  41. package/mcp-memory-server/dist/eval-process.js +31 -0
  42. package/mcp-memory-server/dist/eval-runner.js +10 -4
  43. package/mcp-memory-server/dist/eval-schema.js +1 -1
  44. package/mcp-memory-server/dist/eval-workspace.js +50 -1
  45. package/mcp-memory-server/dist/graph-cli.js +386 -0
  46. package/mcp-memory-server/dist/skill-cli.js +236 -0
  47. package/mcp-memory-server/dist/skill-evaluation.js +244 -0
  48. package/mcp-memory-server/dist/skill-schema.js +164 -0
  49. package/mcp-memory-server/dist/skill-store.js +583 -0
  50. package/mcp-memory-server/package.json +3 -1
  51. package/opencode/command/graphify.md +48 -58
  52. package/package.json +2 -1
  53. package/pi/prompts/graphify.md +32 -0
  54. package/schemas/eval-report.schema.json +1 -1
  55. package/schemas/skill-adapter.schema.json +39 -0
  56. package/schemas/skill-proposal-protocol.schema.json +97 -0
  57. package/scripts/bootstrap.sh +2 -1
  58. package/scripts/completion.sh +55 -6
  59. package/scripts/smoke-bash32.sh +1 -0
  60. package/scripts/sync-kimi-assets.sh +77 -0
  61. package/scripts/sync-pi-assets.sh +52 -31
  62. package/scripts/test-bootstrap-untracked.sh +21 -0
  63. package/scripts/test-cli-dispatch.sh +4 -0
  64. package/scripts/test-completion.sh +16 -0
  65. package/scripts/test-graph-harness-adapters.sh +46 -0
  66. package/scripts/test-launcher-hooks.sh +4 -2
  67. package/scripts/test-learning-docs.sh +13 -11
  68. package/scripts/test-package-install.sh +24 -0
  69. package/scripts/test-skill-cli.sh +30 -0
  70. package/scripts/test-trajectory-hooks.sh +5 -1
  71. package/scripts/test-uninstall.sh +12 -2
  72. package/scripts/uninstall.sh +11 -4
  73. package/templates/graph-policy.md.template +26 -5
  74. package/docs/learning/PRODUCTION-PLAN.md +0 -153
  75. package/docs/learning/instructor-guide.md +0 -66
  76. package/docs/learning/templates/video-script-template.md +0 -50
  77. package/docs/learning/video-scripts/L00-why-cairnkeep.md +0 -80
  78. package/docs/learning/video-scripts/L01-safe-trial.md +0 -78
  79. package/docs/learning/video-scripts/L02-installation.md +0 -97
  80. package/docs/learning/video-scripts/L03-first-project.md +0 -99
@@ -2,7 +2,7 @@
2
2
 
3
3
  **Audience:** maintainers and advanced practitioners evaluating optional layers
4
4
  **Prerequisite:** Practitioner track
5
- **Total time:** about 3.25 hours
5
+ **Total time:** about 4 hours
6
6
  **Infrastructure:** local fixtures first; a live harness is operator-provided
7
7
 
8
8
  1. [L13 - Session evidence and hindsight](../lessons/L13-session-evidence.md)
@@ -10,11 +10,13 @@
10
10
  3. [L15 - Capability governance](../lessons/L15-capability-governance.md)
11
11
  4. [L16 - Evaluation and ablation](../lessons/L16-evaluation.md)
12
12
  5. [L17 - The agent boundary](../lessons/L17-agent-boundary.md)
13
+ 6. [L19 - Validated skill improvement](../lessons/L19-validated-skill-improvement.md)
13
14
 
14
15
  The track is complete when the learner can explain each consent boundary,
15
16
  inspect and manage every local evidence store, compare baseline and ablation
16
- reports without making unsupported claims, and distinguish current runtime
17
- features from the design-only meta-agent contract.
17
+ reports without making unsupported claims, apply or roll back an exact eligible
18
+ skill proposal, and distinguish that narrow runtime from the design-only
19
+ general meta-agent contract.
18
20
 
19
21
  Hands-on spine: `course-05-evidence` through `course-07-evaluation` in the
20
22
  public [course labs](https://github.com/cairnkeep/cairnkeep-course-labs).
@@ -6,10 +6,11 @@
6
6
 
7
7
  1. [L04 - Memory fundamentals](../lessons/L04-memory-fundamentals.md)
8
8
  2. [L05 - The daily workflow](../lessons/L05-daily-workflow.md)
9
- 3. [L06 - Repository review and security](../lessons/L06-repository-quality.md)
10
- 4. [L07 - Privacy and storage](../lessons/L07-privacy-and-storage.md)
11
- 5. [L08 - Multiple machines](../lessons/L08-multiple-machines.md)
12
- 6. [L13 - Session evidence and hindsight](../lessons/L13-session-evidence.md)
9
+ 3. [L18 - Local code graph](../lessons/L18-local-code-graph.md)
10
+ 4. [L06 - Repository review and security](../lessons/L06-repository-quality.md)
11
+ 5. [L07 - Privacy and storage](../lessons/L07-privacy-and-storage.md)
12
+ 6. [L08 - Multiple machines](../lessons/L08-multiple-machines.md)
13
+ 7. [L13 - Session evidence and hindsight](../lessons/L13-session-evidence.md)
13
14
 
14
15
  The track is complete when the learner can decide what should become memory,
15
16
  review derived knowledge, run governed repository checks, locate the store, and
package/docs/operating.md CHANGED
@@ -17,9 +17,10 @@ installs. This guide covers all three in order.
17
17
  ## Prerequisites
18
18
 
19
19
  - Node.js 22 or newer (for the memory server) and a supported harness. Claude
20
- Code and OpenCode receive the full operating layer. Kimi Code and Qwen Code
21
- receive the memory MCP and launcher. Pi receives its native trajectory
22
- adapter.
20
+ Code and OpenCode receive the full operating layer. Kimi Code receives the
21
+ memory MCP, launcher, and opt-in graph Skill; Qwen Code receives the memory
22
+ MCP and launcher. Pi receives its native trajectory adapter, graph prompt,
23
+ and launcher.
23
24
  - Optional: the `sqlite3` CLI for `cairn memory export`. Runtime memory and
24
25
  `cairn memory import` do not require it.
25
26
  - Optional: an OpenAI-compatible LLM endpoint for memory extraction and
@@ -159,10 +160,85 @@ Model selection stays operator-env-driven through the existing
159
160
  variables (see "Configuration" below) — the harness commits no default model
160
161
  and carries no known-good allowlist.
161
162
 
163
+ ## Optional Graphify workflow
164
+
165
+ Graphify is an optional local structural index. Install only its isolated CLI:
166
+
167
+ ```bash
168
+ uv tool install graphifyy
169
+ # or: pipx install graphifyy
170
+ ```
171
+
172
+ Do not run `graphify install`: Cairnkeep owns the harness commands, policies,
173
+ and hooks, so Graphify-owned assets would duplicate that operating layer.
174
+ With the managed capability contract enabled, run
175
+ `cairn capabilities enable graph`; otherwise enable the compatibility setting
176
+ `graphify.enabled` in `.planning/config.json`. Then use the managed command
177
+ surface. Claude Code and OpenCode receive the native wrapper:
178
+
179
+ ```text
180
+ /graphify build
181
+ /graphify status
182
+ /graphify query putTrajectory
183
+ /graphify explain putTrajectory
184
+ /graphify path putTrajectory resolveCapabilityStatus
185
+ /graphify diff
186
+ ```
187
+
188
+ Install the equivalent thin adapters for Kimi Code and Pi:
189
+
190
+ ```bash
191
+ cairn sync-kimi --apply
192
+ cairn sync-pi --apply
193
+ ```
194
+
195
+ Kimi then exposes `/graphify` and `/skill:graphify`; Pi exposes `/graphify`.
196
+ Both adapters delegate exclusively to the same portable CLI, which also remains
197
+ available directly from their project shell:
198
+
199
+ ```bash
200
+ cairn graph build
201
+ cairn graph status
202
+ cairn graph query putTrajectory
203
+ cairn graph explain putTrajectory
204
+ cairn graph path putTrajectory resolveCapabilityStatus
205
+ cairn graph diff
206
+ ```
207
+
208
+ The same direct CLI works with Qwen Code, Codex, and other shell-capable
209
+ clients. This does not add a Pi MCP bridge or imply that those harnesses receive
210
+ the rest of Cairnkeep's operating layer.
211
+
212
+ All six modes delegate to `cairn graph`. Managed builds run Graphify's local
213
+ code-only `update` path with provider credentials removed from the subprocess
214
+ environment, then atomically publish validated artifacts under
215
+ `.planning/graphs/`. Previous published artifacts are isolated during the scan
216
+ so Graphify cannot re-index its own generated HTML/report/JSON. Managed builds
217
+ do not perform Graphify's optional semantic document extraction. Prefer exact
218
+ function, class, or file names; broad natural-language graph queries are less
219
+ precise. Cairnkeep does not install or update Graphify automatically.
220
+
221
+ Graphify also maintains an incremental `graphify-out/` work directory at the
222
+ repository root. It is separate from Cairnkeep's published
223
+ `.planning/graphs/` view and can contain the same sensitive structural data.
224
+ Keep both locations local. Before the first build in a Git repository, add
225
+ `/graphify-out/` to the clone-local exclude file or the repository's ignore
226
+ policy, and verify with `git status --short` after every build.
227
+
162
228
  ## Setup order (Kimi Code)
163
229
 
164
- Kimi Code can use the Cairnkeep memory MCP and the generated project launcher.
165
- It does not yet receive Cairnkeep-specific skills, commands, or hooks.
230
+ Kimi Code can use the Cairnkeep memory MCP, generated project launcher, and one
231
+ thin graph Skill. Install the Skill into `$KIMI_CODE_HOME` (default
232
+ `~/.kimi-code`) with:
233
+
234
+ ```bash
235
+ cairn sync-kimi --apply
236
+ ```
237
+
238
+ Use `cairn sync-kimi --check` to detect drift or `--live-root DIR` for an
239
+ isolated Kimi root. The command owns only `skills/graphify/SKILL.md`; `cairn
240
+ uninstall --kimi-live-root DIR` removes it backup-first. It adds no hooks,
241
+ agents, plugins, or new data flow.
166
242
 
167
243
  For local stdio memory, create `.kimi-code/mcp.json` in the project:
168
244
 
@@ -223,8 +299,8 @@ configuration, approval behavior, and overlay guidance.
223
299
 
224
300
  ## Setup order (Pi)
225
301
 
226
- Pi trajectory capture uses the same project scaffold and store, but its source
227
- is a native TypeScript extension installed into Pi's agent root:
302
+ Pi trajectory capture uses the same project scaffold and store. Its sync also
303
+ installs a thin `/graphify` prompt into Pi's agent root:
228
304
 
229
305
  ```bash
230
306
  npm install -g @cairnkeep/cli
@@ -240,8 +316,9 @@ a user-chosen Pi extension/bridge separately.
240
316
 
241
317
  Use `cairn sync-pi --check` to report drift without writing, or
242
318
  `--live-root DIR` to target an isolated Pi agent root. The command owns exactly
243
- `extensions/cairnkeep-trajectory.ts`; `cairn uninstall --pi-live-root DIR`
244
- removes that file backup-first and leaves every other Pi asset untouched.
319
+ `extensions/cairnkeep-trajectory.ts` and `prompts/graphify.md`; `cairn
320
+ uninstall --pi-live-root DIR` removes those files backup-first and leaves every
321
+ other Pi asset untouched.
245
322
 
246
323
  The extension listens for Pi's native `session_shutdown` event and reads only
247
324
  the active branch from the read-only session manager. It returns before doing
@@ -827,6 +904,22 @@ See [storage](storage.md#evaluation-report-and-note-snapshot-storage) for local
827
904
  retention/removal and [privacy](privacy-and-data-flow.md#evaluation-adapter-and-report-flow)
828
905
  for the exact request, observation, environment, and persistence boundaries.
829
906
 
907
+ ### Validated skill improvement (opt-in)
908
+
909
+ After hindsight-note distillation has recorded the same failure family in at
910
+ least two sessions, `cairn skill harvest` creates a local candidate under
911
+ `.agentfs/skills/`. Review its exact evidence with `show`, approve it explicitly,
912
+ then use a private proposal adapter to generate a bounded edit list for one
913
+ existing skill file. Proposal and evaluation do not modify the live target.
914
+
915
+ Evaluation reuses the default-off evaluation coordinator and therefore requires
916
+ `CAIRN_EVAL=1`, `--yes`, an explicit harness adapter, and two disjoint committed
917
+ task sets. Only a complete, no-regression improvement in exploration opens the
918
+ confirmation set. Only the same result on confirmation makes the proposal
919
+ eligible for exact-digest application. See
920
+ [Validated skill improvement](skill-improvement.md) for the command sequence,
921
+ adapter contract, task constraints, and rollback procedure.
922
+
830
923
  ### Structured session trajectories (opt-in)
831
924
 
832
925
  Trajectory capture is disabled by default. To enable it for a launched Claude
@@ -16,6 +16,7 @@ request unless the corresponding endpoint and credential are configured.
16
16
  | `domain_knowledge_sync` | Files selected by the sync configuration | `ANYTHINGLLM_BASE_URL` |
17
17
  | `route_check` | A health request, with no memory or prompt content | `CAIRN_ROUTE_ENDPOINT` |
18
18
  | `context_explore` | Repository path and query are passed to the configured local executable | `CAIRN_EXPLORE_BINARY`; any further data flow is controlled by that tool |
19
+ | Optional Graphify workflow | The repository path, exact query/symbol names, root `graphify-out/` work directory, and local `.planning/graphs/` publication are passed to or produced by the operator-installed `graphify` executable | Local Graphify subprocess with a minimal environment and no provider credentials; managed build uses code-only `update`, never semantic document extraction |
19
20
  | Remote HTTP memory | MCP requests and responses, including memory content | The explicitly registered Cairnkeep HTTP server |
20
21
  | Opt-in trajectory capture | None | Local `<project>/.agentfs/trajectory.db` only; no model or HTTP path exists |
21
22
  | Opt-in capability callback records | None | Payload-free final metadata in local `<project>/.agentfs/trajectory.db`; HTTP transport is always skipped |
@@ -174,6 +175,47 @@ the full committed population and note-eligible subset with paired counts and
174
175
  seeded uncertainty; they do not convert framework output into a causal,
175
176
  significance, quality, or efficiency claim.
176
177
 
178
+ ## Validated skill improvement flow
179
+
180
+ Harvest and review are local-only operations over project hindsight notes.
181
+ Before approval, no proposal subprocess is started and no evidence crosses a
182
+ process boundary. After explicit approval, the configured proposal adapter
183
+ receives exactly one bounded JSON request containing the approved candidate,
184
+ candidate digest, selected target path, current target content and digest, and
185
+ edit budget.
186
+
187
+ The proposal adapter receives an isolated private HOME and TMPDIR, a minimal
188
+ PATH and locale, and only environment variables named in its strict
189
+ `environment_allowlist`. It does not inherit the rest of the parent
190
+ environment. HOME, PATH, temporary and XDG roots, and Node runtime injection
191
+ variables are isolation-controlled and cannot be allowlisted. Its stdout must
192
+ be one strict bounded response containing status
193
+ and bounded edits; arbitrary stderr is live operator diagnostics and is not
194
+ stored. Cairnkeep records the adapter executable digest and rejects a change
195
+ during the invocation. The adapter may perform network access according to the
196
+ executable and host policy, but Cairnkeep provides no endpoint, credential, or
197
+ network default. Operators must treat approval as consent to send those exact
198
+ evidence and target fields to that adapter.
199
+
200
+ The reduced environment is not an operating-system sandbox. The explicit
201
+ adapter executable retains the invoking user's filesystem and network
202
+ authority, so operators must trust it or wrap it in a separately enforced host
203
+ sandbox.
204
+
205
+ Evaluation sends neither candidate evidence nor proposal rationales to the
206
+ harness adapter. It overlays baseline or candidate skill bytes into separate
207
+ fresh worktrees and otherwise uses the existing evaluation request contract.
208
+ Exploration and confirmation use disjoint task IDs and definitions. Confirmation
209
+ is not opened unless exploration passes, and its results cannot be supplied
210
+ back to proposal generation by this workflow. Both stages bind one evaluation
211
+ adapter executable digest and reject a change during either stage.
212
+
213
+ Apply and rollback invoke no model and make no network request. They operate on
214
+ one contained regular target and private local artifacts, require exact
215
+ digests, reject symlinks, and fail closed on concurrent target changes. See
216
+ [storage](storage.md#validated-skill-storage) for retained bytes and
217
+ [Validated skill improvement](skill-improvement.md) for the operator sequence.
218
+
177
219
  ## Compaction and artifact flows
178
220
 
179
221
  Both `CAIRN_COMPACTION_CAPTURE` and `CAIRN_ARTIFACT_STORE` are default-off and
@@ -0,0 +1,124 @@
1
+ # Validated skill improvement
2
+
3
+ `cairn skill` turns recurring project hindsight into a proposed change to one
4
+ existing skill file, then requires independent evidence before that change can
5
+ be applied. It is an operator-controlled lifecycle, not an autonomous rewrite
6
+ loop.
7
+
8
+ ## Safety contract
9
+
10
+ - Harvest reads only project hindsight notes and requires failures from at
11
+ least two distinct sessions by default.
12
+ - Evidence remains local until `review --approve` records explicit approval.
13
+ - Proposal generation invokes only the configured executable, sends one strict
14
+ bounded JSON request, exposes only allowlisted environment variables, and
15
+ accepts a bounded edit list rather than an arbitrary rewritten tree.
16
+ - Proposal and evaluation artifacts bind the adapter executable digest;
17
+ evaluation rechecks it after exploration and confirmation.
18
+ - Proposal and evaluation use candidate content without modifying the live
19
+ target.
20
+ - Evaluation requires repository-root tasks in two different committed task
21
+ sets with at least two tasks each. Task IDs and exact task definitions cannot
22
+ overlap, and both sets must bind the same immutable source revision.
23
+ - Confirmation runs only after exploration passes. Confirmation results never
24
+ return to proposal generation.
25
+ - Apply requires an eligible exact evaluation, an unchanged target, and the
26
+ full proposal digest. It creates a private backup before atomic replacement.
27
+ - Rollback refuses to overwrite a target changed after application.
28
+
29
+ ## Prepare a target and proposal adapter
30
+
31
+ The target is one existing project-relative regular file named `SKILL.md`, for example
32
+ `.claude/skills/generated-client-review/SKILL.md`. It cannot be a symlink and
33
+ is limited to 256 KiB.
34
+
35
+ The proposal adapter configuration is strict JSON:
36
+
37
+ ```json
38
+ {
39
+ "schema_version": 1,
40
+ "id": "local-skill-proposer",
41
+ "command": {
42
+ "program": "/absolute/path/to/proposal-adapter",
43
+ "args": []
44
+ },
45
+ "environment_allowlist": ["PROPOSAL_PROVIDER_TOKEN"],
46
+ "limits": {
47
+ "elapsed_ms": 120000,
48
+ "stdout_bytes": 1048576
49
+ }
50
+ }
51
+ ```
52
+
53
+ The executable receives the approved candidate, its digest, current target
54
+ content and digest, and edit budget on stdin. It returns `add`, `replace`, or
55
+ `delete` edits with exact anchors and rationales. The executable path must be
56
+ absolute, executable, and not a symlink. Cairnkeep does not provide a model,
57
+ endpoint, credential, or network default. The machine-readable contracts are
58
+ [`schemas/skill-adapter.schema.json`](../schemas/skill-adapter.schema.json) and
59
+ [`schemas/skill-proposal-protocol.schema.json`](../schemas/skill-proposal-protocol.schema.json).
60
+ Process-isolation variables such as `HOME`, `PATH`, temporary directories,
61
+ XDG roots, and Node runtime injection settings cannot be allowlisted.
62
+ This environment boundary is not an operating-system sandbox: the configured
63
+ program runs with the invoking user's filesystem and network authority. Use
64
+ only a trusted adapter or add a separate host sandbox.
65
+
66
+ ## Run the lifecycle
67
+
68
+ ```bash
69
+ # 1. Find recurring hindsight evidence. This performs no model call.
70
+ cairn skill harvest --project . --json
71
+ cairn skill show --kind candidate --id CANDIDATE_ID
72
+
73
+ # 2. Approve the exact evidence that may cross the adapter boundary.
74
+ cairn skill review --candidate CANDIDATE_ID --approve --json
75
+
76
+ # 3. Generate a bounded proposal. The live target remains unchanged.
77
+ cairn skill propose --candidate CANDIDATE_ID \
78
+ --target .claude/skills/generated-client-review/SKILL.md \
79
+ --adapter ./private/proposal-adapter.json --edit-budget 4 --json
80
+
81
+ # 4. Measure baseline versus candidate in fresh isolated worktrees.
82
+ export CAIRN_EVAL=1
83
+ cairn skill evaluate --proposal PROPOSAL_ID \
84
+ --exploration-task-set eval/skill-exploration.json \
85
+ --confirmation-task-set eval/skill-confirmation.json \
86
+ --adapter eval/harness-adapter.json \
87
+ --repetitions 2 --minimum-improvement 1 --yes --json
88
+
89
+ # 5. Inspect artifacts and apply only the exact eligible proposal.
90
+ cairn skill show --kind proposal --id PROPOSAL_ID
91
+ cairn skill show --kind evaluation --id EVALUATION_ID
92
+ cairn skill apply --proposal PROPOSAL_ID --evaluation EVALUATION_ID \
93
+ --confirm FULL_PROPOSAL_DIGEST --json
94
+
95
+ # 6. Revert if later evidence warrants it.
96
+ cairn skill rollback --application APPLICATION_ID --confirm --json
97
+ ```
98
+
99
+ `propose --json` includes `proposal_digest`, which is the value required by
100
+ `apply --confirm`. `list` shows every artifact class; use `--kind candidate`,
101
+ `proposal`, `evaluation`, or `application` to narrow it.
102
+
103
+ ## Evaluation semantics
104
+
105
+ Each task runs twice in each arm. The baseline arm overlays the exact unchanged
106
+ target content; the treatment arm overlays the exact candidate content. Every
107
+ observation must complete, return the expected capability digest, and receive a
108
+ known result from the independent verifier. A task with any candidate-only pass
109
+ counts once as improved; a task with any baseline-only pass counts once as
110
+ regressed and rejects the candidate.
111
+
112
+ Exploration must have no unknown pairs, no regression, and at least
113
+ `--minimum-improvement` improvements. Otherwise evaluation stops without
114
+ opening the confirmation set. Confirmation must independently satisfy the same
115
+ gate. This is deliberately strict and suited to small, high-value task sets;
116
+ it is not a statistical proof that a skill is universally better.
117
+
118
+ ## What this does not do
119
+
120
+ It does not scrape native harness transcripts, schedule periodic self-edits,
121
+ invent task sets, approve evidence, choose an inference provider, or publish a
122
+ changed skill. These remain explicit operator responsibilities. Start with a
123
+ small target and representative tasks; expand only when the stored evaluation
124
+ artifacts make the decision auditable.
package/docs/storage.md CHANGED
@@ -213,6 +213,31 @@ the process stores databases below `/data`, normally backed by the
213
213
  removing the volume removes the databases. See [Containers](containers.md) for
214
214
  the exact paths and backup boundary.
215
215
 
216
+ ## Validated skill storage
217
+
218
+ The skill-improvement lifecycle stores private JSON artifacts below
219
+ `<project>/.agentfs/skills/`: `candidates/`, `proposals/`, `evaluations/`, and
220
+ `applications/`. Pre-application target backups live in `backups/`; isolated
221
+ proposal-adapter homes are created temporarily in `adapter-tmp/` and removed
222
+ after every invocation. Directories are mode `0700`, JSON and backup files are
223
+ mode `0600`, artifacts are bounded to 2 MiB, and candidate target content is
224
+ bounded to 256 KiB.
225
+
226
+ Candidate and proposal artifacts can contain hindsight excerpts and full skill
227
+ content, so the entire directory is sensitive. Evaluation reports and
228
+ experiment-owned snapshots remain in `.agentfs/eval/experiments/` under the
229
+ existing evaluation retention rules. No skill artifact is written to global or
230
+ remote memory, and remote MCP configuration does not redirect this project
231
+ state.
232
+
233
+ `apply` writes the original target to a unique private backup before atomically
234
+ replacing the target. If replacement, digest verification, or ledger storage
235
+ fails, Cairnkeep restores the original and removes the incomplete backup.
236
+ `rollback` restores only when both the current target and backup match the
237
+ application ledger, preventing an unrelated later edit from being overwritten.
238
+ Default uninstall retains `.agentfs/`; explicit purge uses the existing
239
+ backup-first project boundary.
240
+
216
241
  ## Remote HTTP mode
217
242
 
218
243
  Remote storage is explicit. An operator starts Cairnkeep in HTTP mode on a
@@ -1 +1 @@
1
- {"identifier":"cairn-offline-fake-v1","package_version":"2.6.0","schema_version":1,"task_set_digest":"13e95379f35839593047ec34521a65a8fa3ab92bd3993bca9dc8bf5c4a9ff36f"}
1
+ {"identifier":"cairn-offline-fake-v1","package_version":"2.8.0","schema_version":1,"task_set_digest":"13e95379f35839593047ec34521a65a8fa3ab92bd3993bca9dc8bf5c4a9ff36f"}
@@ -0,0 +1,33 @@
1
+ ---
2
+ name: graphify
3
+ description: Build, query, and inspect the local project graph exclusively through Cairnkeep
4
+ type: flow
5
+ ---
6
+
7
+ <!-- managed-by:cairnkeep -->
8
+ # Cairnkeep graph
9
+
10
+ Handle this request through Cairnkeep's public graph owner. The invocation
11
+ arguments are:
12
+
13
+ ```text
14
+ $ARGUMENTS
15
+ ```
16
+
17
+ Accept exactly one of these forms:
18
+
19
+ - `build`
20
+ - `build --force`
21
+ - `query <term>`
22
+ - `status`
23
+ - `diff`
24
+ - `explain <symbol>`
25
+ - `path <from> <to>`
26
+
27
+ Invoke the Bash tool exactly once with the corresponding `cairn graph ...`
28
+ command. Pass each term or symbol as one safely quoted argument. Return the
29
+ command output verbatim and stop.
30
+
31
+ If the arguments do not match a supported form, show the supported forms and
32
+ stop without invoking a tool. Do not call the Graphify executable, edit graph
33
+ artifacts, spawn another agent, or implement graph logic in this Skill.
@@ -6,7 +6,7 @@ const MAX_COMPATIBILITY_CONFIG_BYTES = 64 * 1024;
6
6
  function environmentName(id) {
7
7
  return `CAIRN_CAPABILITY_${id.toUpperCase().replaceAll(".", "_")}`;
8
8
  }
9
- async function readGraphCompatibility(projectRoot) {
9
+ export async function isGraphCapabilityCompatible(projectRoot) {
10
10
  const planningDirectory = join(projectRoot, ".planning");
11
11
  const configPath = join(planningDirectory, "config.json");
12
12
  try {
@@ -84,7 +84,7 @@ export const CAPABILITY_REGISTRY = [
84
84
  compatibility_default: false,
85
85
  restart_required: false,
86
86
  logging_policy: "final-callback",
87
- resolveCompatibility: ({ projectRoot, graphifyEnabled }) => graphifyEnabled ?? readGraphCompatibility(projectRoot),
87
+ resolveCompatibility: ({ projectRoot, graphifyEnabled }) => graphifyEnabled ?? isGraphCapabilityCompatible(projectRoot),
88
88
  },
89
89
  {
90
90
  id: "security.audit",
@@ -186,7 +186,7 @@ function validateBundledBinding(taskSetPath, taskSet, taskSetBytes) {
186
186
  binding_path: realpathSync(bindingPath),
187
187
  };
188
188
  }
189
- function validateArms(arms) {
189
+ function validateArms(arms, experimentKind) {
190
190
  if (arms.length < 1 || arms.length > 2)
191
191
  throw new Error("Evaluation requires one or two arms.");
192
192
  const seen = new Set();
@@ -201,8 +201,12 @@ function validateArms(arms) {
201
201
  }
202
202
  if (arm.id === "baseline" && arm.disabled_capability !== null)
203
203
  throw new Error("Baseline cannot disable a capability.");
204
- if (arm.id === "treatment" && arm.disabled_capability === null)
205
- throw new Error("Treatment must disable exactly one capability.");
204
+ if (arm.id === "treatment" && experimentKind === "ablation" && arm.disabled_capability === null) {
205
+ throw new Error("Ablation treatment must disable exactly one capability.");
206
+ }
207
+ if (experimentKind === "skill_candidate" && arm.disabled_capability !== null) {
208
+ throw new Error("Skill-candidate arms cannot disable capabilities.");
209
+ }
206
210
  return { id: arm.id, disabled_capability: arm.disabled_capability };
207
211
  });
208
212
  }
@@ -214,7 +218,10 @@ export function isEvalEnabled(value = process.env.CAIRN_EVAL) {
214
218
  }
215
219
  export function buildEvalSchedule(options) {
216
220
  const taskSet = evalTaskSetSchema.parse(options.taskSet);
217
- const arms = validateArms(options.arms);
221
+ const inferredKind = options.arms.length === 1 ? "two_pass" : options.arms.some(({ disabled_capability }) => disabled_capability !== null)
222
+ ? "ablation"
223
+ : "skill_candidate";
224
+ const arms = validateArms(options.arms, inferredKind);
218
225
  if (!Number.isSafeInteger(options.repetitions) || options.repetitions < 1 || options.repetitions > MAX_REPETITIONS) {
219
226
  throw new Error(`Evaluation repetitions must be an integer from 1 to ${MAX_REPETITIONS}.`);
220
227
  }
@@ -282,7 +289,15 @@ export function validateEvalInputs(options) {
282
289
  }
283
290
  const repetitions = options.repetitions ?? 1;
284
291
  const seed = options.seed ?? "cairn-eval-v1";
285
- const arms = validateArms(options.arms ?? [{ id: "baseline", disabled_capability: null }]);
292
+ const suppliedArms = options.arms ?? [{ id: "baseline", disabled_capability: null }];
293
+ const experimentKind = options.experimentKind
294
+ ?? (suppliedArms.length === 1 ? "two_pass" : "ablation");
295
+ if (experimentKind === "two_pass" && suppliedArms.length !== 1)
296
+ throw new Error("Two-pass evaluation requires one arm.");
297
+ if ((experimentKind === "ablation" || experimentKind === "skill_candidate") && suppliedArms.length !== 2) {
298
+ throw new Error(`${experimentKind} evaluation requires two arms.`);
299
+ }
300
+ const arms = validateArms(suppliedArms, experimentKind);
286
301
  const passes = ["run1", "run2"];
287
302
  const schedule = buildEvalSchedule({ taskSet, arms, repetitions, passes, seed });
288
303
  const adapterBase = dirname(adapterInput.path);
@@ -304,6 +319,7 @@ export function validateEvalInputs(options) {
304
319
  repetitions,
305
320
  seed,
306
321
  arms,
322
+ experiment_kind: experimentKind,
307
323
  passes,
308
324
  concurrency: 1,
309
325
  task_set_commit: taskSetCommit,
@@ -320,6 +336,7 @@ export function validateEvalInputs(options) {
320
336
  repetitions,
321
337
  seed,
322
338
  arms,
339
+ experiment_kind: experimentKind,
323
340
  passes: ["run1", "run2"],
324
341
  concurrency: 1,
325
342
  invocation_count: schedule.invocation_count,
@@ -246,6 +246,37 @@ function hasTrailingJsonDocument(value) {
246
246
  }
247
247
  return false;
248
248
  }
249
+ export async function runBoundedJsonProcess(options) {
250
+ const request = options.request_schema.safeParse(options.request);
251
+ if (!request.success)
252
+ throw new EvalProcessError("invalid_request");
253
+ const commandResult = await runCommandInternal({
254
+ command: options.command,
255
+ cwd: options.cwd,
256
+ env: options.env,
257
+ timeout_ms: options.timeout_ms,
258
+ max_stdout_bytes: options.max_stdout_bytes,
259
+ kill_grace_ms: options.kill_grace_ms,
260
+ signal: options.signal,
261
+ stdout_mode: "raw",
262
+ stdin_document: Buffer.from(JSON.stringify(request.data), "utf8"),
263
+ });
264
+ if (commandResult.exit_code !== 0 || commandResult.signal !== null || commandResult.cleanup !== "closed") {
265
+ throw new EvalProcessError("adapter_error", commandResult);
266
+ }
267
+ let parsed;
268
+ try {
269
+ parsed = JSON.parse(commandResult.stdout ?? "");
270
+ }
271
+ catch {
272
+ throw new EvalProcessError(hasTrailingJsonDocument(commandResult.stdout ?? "") ? "multiple_json" : "invalid_json", commandResult);
273
+ }
274
+ const result = options.result_schema.safeParse(parsed);
275
+ if (!result.success)
276
+ throw new EvalProcessError("invalid_result", commandResult);
277
+ const { stdout: _stdout, ...processResult } = commandResult;
278
+ return { ...processResult, result: result.data };
279
+ }
249
280
  export async function runBoundedJsonAdapter(options) {
250
281
  const request = evalAdapterRequestSchema.safeParse(options.request);
251
282
  if (!request.success)
@@ -9,7 +9,7 @@ import { CAPABILITY_IDS, capabilityStatusSchema, } from "./capability-schema.js"
9
9
  import { canonicalDigest, evalReportSchema, evalReportRuntimeContractDigest, } from "./eval-schema.js";
10
10
  import { EvalProcessError, runBoundedCommand, runBoundedJsonAdapter, } from "./eval-process.js";
11
11
  import { checkpointEvalReport, createEvalReportStore, } from "./eval-report.js";
12
- import { cleanupEvalWorkspace, createEvalWorkspace, runTaskPreparation, runTaskVerifier, } from "./eval-workspace.js";
12
+ import { applyEvalWorkspaceOverlay, cleanupEvalWorkspace, createEvalWorkspace, runTaskPreparation, runTaskVerifier, } from "./eval-workspace.js";
13
13
  const moduleDirectory = dirname(fileURLToPath(import.meta.url));
14
14
  const publishedReportSchemaPath = resolve(moduleDirectory, "..", "..", "schemas", "eval-report.schema.json");
15
15
  let reportSchemaDigestsCache;
@@ -557,6 +557,8 @@ export async function runEvalObservation(options) {
557
557
  observation.missing_reasons.push("preparation_failed");
558
558
  }
559
559
  else {
560
+ if (options.workspace_overlay)
561
+ await applyEvalWorkspaceOverlay(workspace, options.workspace_overlay);
560
562
  const request = {
561
563
  schema_version: 1,
562
564
  experiment_id: options.report.experiment_id,
@@ -698,7 +700,7 @@ export async function runEvalObservation(options) {
698
700
  await checkpoint(options.report_store, options.report);
699
701
  return { observation, snapshot: distilledSnapshot, cancelled: observation.terminal_state === "cancelled" };
700
702
  }
701
- function initialReport(plan, experimentId) {
703
+ function initialReport(plan, experimentId, fallbackExperimentKind) {
702
704
  const now = new Date().toISOString();
703
705
  const sourceCommit = plan.task_set_commit ?? (plan.source.kind === "git" ? plan.source.revision : plan.task_set_digest);
704
706
  const missingness = { count: 0, reasons: [] };
@@ -706,7 +708,7 @@ function initialReport(plan, experimentId) {
706
708
  schema_version: 1,
707
709
  experiment_id: experimentId,
708
710
  status: "partial",
709
- experiment_kind: plan.arms.length === 1 ? "two_pass" : "ablation",
711
+ experiment_kind: plan.experiment_kind ?? fallbackExperimentKind,
710
712
  task_set_digest: plan.task_set_digest,
711
713
  adapter_config_digest: plan.adapter_config_digest,
712
714
  source_revision: plan.source.kind === "git" ? plan.source.revision : plan.task_set_digest,
@@ -750,7 +752,7 @@ async function runExperiment(options, capabilityArms) {
750
752
  });
751
753
  if (reportStore.experiment_id !== experimentId)
752
754
  throw new Error("report_experiment_mismatch");
753
- const report = initialReport(options.plan, experimentId);
755
+ const report = initialReport(options.plan, experimentId, capabilityArms ? "ablation" : "two_pass");
754
756
  const snapshotsByTask = new Map();
755
757
  const notesByTask = new Map();
756
758
  const snapshots = [];
@@ -782,6 +784,7 @@ async function runExperiment(options, capabilityArms) {
782
784
  distill_command: options.distill_command,
783
785
  distill_timeout_ms: options.distill_timeout_ms,
784
786
  capability_runtime: capabilityRuntime,
787
+ workspace_overlay: options.workspace_overlays?.[row.arm],
785
788
  });
786
789
  executed.push(row.observation_id);
787
790
  if (result.snapshot) {
@@ -813,6 +816,9 @@ export async function runTwoPassExperiment(options) {
813
816
  return runExperiment(options);
814
817
  }
815
818
  export async function runCapabilityAblation(options) {
819
+ if (options.plan.experiment_kind !== undefined && options.plan.experiment_kind !== "ablation") {
820
+ throw new Error("ablation_experiment_kind_required");
821
+ }
816
822
  const arms = buildAblationArms(options.disabled_capability);
817
823
  const intended = arms.map(({ id, disabled_capability }) => ({ id, disabled_capability }));
818
824
  if (canonicalDigest(options.plan.arms) !== canonicalDigest(intended))
@@ -13,7 +13,7 @@ export const EVAL_OBSERVATION_TERMINAL_STATES = [
13
13
  "invalid_result",
14
14
  ];
15
15
  export const EVAL_PASS_STATES = ["passed", "failed", "unknown"];
16
- export const EVAL_EXPERIMENT_KINDS = ["two_pass", "ablation"];
16
+ export const EVAL_EXPERIMENT_KINDS = ["two_pass", "ablation", "skill_candidate"];
17
17
  const MAX_TEXT_BYTES = 1024 * 1024;
18
18
  const MAX_COMMAND_ARGS = 256;
19
19
  const MAX_TASKS = 10_000;