@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.
- package/CHANGELOG.md +35 -1
- package/README.md +45 -8
- package/bin/cairn +21 -3
- package/claude/commands/graphify.md +46 -55
- package/docs/design/meta-agent.md +39 -35
- package/docs/harness-compatibility.md +13 -5
- package/docs/learning/CURRICULUM-MAP.md +10 -3
- package/docs/learning/FEATURE-GUIDE.md +8 -5
- package/docs/learning/README.md +16 -14
- package/docs/learning/lessons/L00-why-cairnkeep.md +1 -5
- package/docs/learning/lessons/L01-safe-trial.md +3 -7
- package/docs/learning/lessons/L02-installation.md +6 -8
- package/docs/learning/lessons/L03-first-project.md +1 -5
- package/docs/learning/lessons/L04-memory-fundamentals.md +0 -6
- package/docs/learning/lessons/L05-daily-workflow.md +0 -5
- package/docs/learning/lessons/L06-repository-quality.md +0 -6
- package/docs/learning/lessons/L07-privacy-and-storage.md +0 -5
- package/docs/learning/lessons/L08-multiple-machines.md +0 -5
- package/docs/learning/lessons/L09-domain-knowledge.md +0 -5
- package/docs/learning/lessons/L10-context-exploration.md +0 -5
- package/docs/learning/lessons/L11-containers.md +0 -5
- package/docs/learning/lessons/L12-managed-overlays.md +0 -6
- package/docs/learning/lessons/L13-session-evidence.md +0 -6
- package/docs/learning/lessons/L14-typed-memory.md +0 -6
- package/docs/learning/lessons/L15-capability-governance.md +0 -6
- package/docs/learning/lessons/L16-evaluation.md +0 -7
- package/docs/learning/lessons/L17-agent-boundary.md +12 -14
- package/docs/learning/lessons/L18-local-code-graph.md +97 -0
- package/docs/learning/lessons/L19-validated-skill-improvement.md +87 -0
- package/docs/learning/templates/lesson-template.md +0 -4
- package/docs/learning/tracks/evidence-and-evaluation.md +5 -3
- package/docs/learning/tracks/practitioner.md +5 -4
- package/docs/operating.md +102 -9
- package/docs/privacy-and-data-flow.md +42 -0
- package/docs/skill-improvement.md +124 -0
- package/docs/storage.md +25 -0
- package/examples/eval/bundled-fake.json +1 -1
- package/kimi/skills/graphify/SKILL.md +33 -0
- package/mcp-memory-server/dist/capability-registry.js +2 -2
- package/mcp-memory-server/dist/eval-plan.js +22 -5
- package/mcp-memory-server/dist/eval-process.js +31 -0
- package/mcp-memory-server/dist/eval-runner.js +10 -4
- package/mcp-memory-server/dist/eval-schema.js +1 -1
- package/mcp-memory-server/dist/eval-workspace.js +50 -1
- package/mcp-memory-server/dist/graph-cli.js +386 -0
- package/mcp-memory-server/dist/skill-cli.js +236 -0
- package/mcp-memory-server/dist/skill-evaluation.js +244 -0
- package/mcp-memory-server/dist/skill-schema.js +164 -0
- package/mcp-memory-server/dist/skill-store.js +583 -0
- package/mcp-memory-server/package.json +3 -1
- package/opencode/command/graphify.md +48 -58
- package/package.json +2 -1
- package/pi/prompts/graphify.md +32 -0
- package/schemas/eval-report.schema.json +1 -1
- package/schemas/skill-adapter.schema.json +39 -0
- package/schemas/skill-proposal-protocol.schema.json +97 -0
- package/scripts/bootstrap.sh +2 -1
- package/scripts/completion.sh +55 -6
- package/scripts/smoke-bash32.sh +1 -0
- package/scripts/sync-kimi-assets.sh +77 -0
- package/scripts/sync-pi-assets.sh +52 -31
- package/scripts/test-bootstrap-untracked.sh +21 -0
- package/scripts/test-cli-dispatch.sh +4 -0
- package/scripts/test-completion.sh +16 -0
- package/scripts/test-graph-harness-adapters.sh +46 -0
- package/scripts/test-launcher-hooks.sh +4 -2
- package/scripts/test-learning-docs.sh +13 -11
- package/scripts/test-package-install.sh +24 -0
- package/scripts/test-skill-cli.sh +30 -0
- package/scripts/test-trajectory-hooks.sh +5 -1
- package/scripts/test-uninstall.sh +12 -2
- package/scripts/uninstall.sh +11 -4
- package/templates/graph-policy.md.template +26 -5
- package/docs/learning/PRODUCTION-PLAN.md +0 -153
- package/docs/learning/instructor-guide.md +0 -66
- package/docs/learning/templates/video-script-template.md +0 -50
- package/docs/learning/video-scripts/L00-why-cairnkeep.md +0 -80
- package/docs/learning/video-scripts/L01-safe-trial.md +0 -78
- package/docs/learning/video-scripts/L02-installation.md +0 -97
- 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
|
|
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,
|
|
17
|
-
|
|
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. [
|
|
10
|
-
4. [
|
|
11
|
-
5. [
|
|
12
|
-
6. [
|
|
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
|
|
21
|
-
|
|
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
|
|
165
|
-
|
|
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
|
|
227
|
-
|
|
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
|
|
244
|
-
removes
|
|
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.
|
|
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
|
|
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 ??
|
|
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("
|
|
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
|
|
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
|
|
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.
|
|
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;
|