@cairnkeep/cli 2.14.0 → 2.15.1
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 +41 -1
- package/README.md +22 -2
- package/claude/commands/cairn-work.md +19 -0
- package/docs/design/team-mode.md +145 -0
- package/docs/harness-compatibility.md +11 -10
- package/docs/learning/CURRICULUM-MAP.md +4 -1
- package/docs/learning/FEATURE-GUIDE.md +8 -4
- package/docs/learning/README.md +2 -1
- package/docs/learning/lessons/L00-why-cairnkeep.md +1 -1
- package/docs/learning/lessons/L01-safe-trial.md +1 -1
- package/docs/learning/lessons/L02-installation.md +1 -1
- package/docs/learning/lessons/L03-first-project.md +1 -1
- package/docs/learning/lessons/L19-validated-skill-improvement.md +1 -1
- package/docs/learning/lessons/L20-mcp-tool-profiles.md +1 -1
- package/docs/learning/lessons/L21-context-packs.md +1 -1
- package/docs/learning/lessons/L22-native-windows.md +1 -1
- package/docs/learning/lessons/L23-guided-setup.md +1 -1
- package/docs/learning/lessons/L24-okf-exchange.md +1 -1
- package/docs/learning/lessons/L25-playbooks.md +114 -0
- package/docs/learning/tracks/operator.md +5 -2
- package/docs/learning/tracks/practitioner.md +5 -3
- package/docs/learning/video-scripts/V25-playbooks.md +38 -0
- package/docs/native-windows.md +12 -0
- package/docs/operating.md +54 -0
- package/docs/privacy-and-data-flow.md +23 -0
- package/docs/quickstart.md +17 -1
- package/docs/storage.md +22 -0
- package/examples/eval/bundled-fake.json +1 -1
- package/kimi/skills/cairn-work/SKILL.md +13 -0
- package/mcp-memory-server/dist/playbook-cli.js +321 -0
- package/mcp-memory-server/dist/playbook-receipt.js +245 -0
- package/mcp-memory-server/dist/playbook-schema.js +56 -0
- package/mcp-memory-server/dist/playbook.js +396 -0
- package/mcp-memory-server/package.json +2 -1
- package/opencode/command/cairn-work.md +13 -0
- package/package.json +1 -1
- package/pi/prompts/cairn-work.md +12 -0
- package/schemas/playbook.schema.json +29 -0
- package/scripts/bootstrap.sh +17 -0
- package/scripts/cairn-cli.mjs +2 -0
- package/scripts/completion.sh +23 -3
- package/scripts/doctor.sh +14 -1
- package/scripts/playbook-instructions.mjs +108 -0
- package/scripts/setup-core.mjs +1 -0
- package/scripts/setup.mjs +8 -0
- package/scripts/sync-kimi-assets.sh +3 -3
- package/scripts/sync-opencode-memory-assets.sh +2 -1
- package/scripts/sync-pi-assets.sh +2 -1
- package/scripts/test-cli-dispatch.sh +2 -0
- package/scripts/test-completion.sh +11 -2
- package/scripts/test-graph-harness-adapters.sh +9 -3
- package/scripts/test-harness-registry.sh +11 -0
- package/scripts/test-learning-docs.sh +23 -2
- package/scripts/test-package-install.sh +26 -0
- package/scripts/test-playbook-instructions.sh +60 -0
- package/scripts/test-windows-native.mjs +7 -0
- package/scripts/uninstall.sh +16 -1
- package/scripts/windows-platform.mjs +24 -3
- package/templates/playbook-agent-instructions.md.template +14 -0
- package/templates/playbooks.json.template +5 -0
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
**Audience:** advanced users and platform maintainers
|
|
4
4
|
**Prerequisite:** Practitioner track; L13 before operating captured evidence
|
|
5
|
-
**Total time:** about 6 hours
|
|
5
|
+
**Total time:** about 6 hours 45 minutes
|
|
6
6
|
|
|
7
7
|
1. [L09 - Domain knowledge with RAG](../lessons/L09-domain-knowledge.md)
|
|
8
8
|
2. [L10 - Faster context exploration](../lessons/L10-context-exploration.md)
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
9. [L22 - Operate Cairnkeep on native Windows](../lessons/L22-native-windows.md)
|
|
16
16
|
10. [L23 - Guided setup and Pi memory](../lessons/L23-guided-setup.md)
|
|
17
17
|
11. [L24 - Reviewed knowledge exchange](../lessons/L24-okf-exchange.md)
|
|
18
|
+
12. [L25 - Bounded workflow playbooks](../lessons/L25-playbooks.md)
|
|
18
19
|
|
|
19
20
|
All modules are optional. The track is complete when the operator can add and
|
|
20
21
|
remove each integration without breaking the standalone local workflow and can
|
|
@@ -23,8 +24,10 @@ v2.11 setup/Pi lifecycle only after the earlier storage, governance, trust, and
|
|
|
23
24
|
native-platform boundaries.
|
|
24
25
|
L24 adds portable exchange only after the immutable-pack and privacy boundaries
|
|
25
26
|
are understood.
|
|
27
|
+
L25 adds policy enforcement and the unauthenticated local actor boundary before
|
|
28
|
+
any future team deployment.
|
|
26
29
|
|
|
27
30
|
Hands-on spine: `course-04-operation`, `course-06-governance`, and
|
|
28
31
|
`course-07-evaluation`, `course-10-trust-context`, and
|
|
29
|
-
`course-12-guided-setup` in the public
|
|
32
|
+
`course-12-guided-setup`, and `course-13-playbooks` in the public
|
|
30
33
|
[course labs](https://github.com/cairnkeep/cairnkeep-course-labs).
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
**Audience:** developers adopting Cairnkeep in daily work
|
|
4
4
|
**Prerequisite:** Quickstart track
|
|
5
|
-
**Total time:** about 4 hours
|
|
5
|
+
**Total time:** about 4 hours 35 minutes
|
|
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)
|
|
@@ -11,11 +11,13 @@
|
|
|
11
11
|
5. [L07 - Privacy and storage](../lessons/L07-privacy-and-storage.md)
|
|
12
12
|
6. [L08 - Multiple machines](../lessons/L08-multiple-machines.md)
|
|
13
13
|
7. [L13 - Session evidence and hindsight](../lessons/L13-session-evidence.md)
|
|
14
|
+
8. [L25 - Bounded workflow playbooks](../lessons/L25-playbooks.md)
|
|
14
15
|
|
|
15
16
|
The track is complete when the learner can decide what should become memory,
|
|
16
17
|
review derived knowledge, run governed repository checks, locate the store, and
|
|
17
|
-
perform a reversible backup or migration
|
|
18
|
+
perform a reversible backup or migration, and distinguish playbook selection
|
|
19
|
+
from execution and approval.
|
|
18
20
|
|
|
19
21
|
Hands-on spine: `course-02-memory`, `course-03-quality`, and
|
|
20
|
-
`course-05-evidence` in the public
|
|
22
|
+
`course-05-evidence`, and `course-13-playbooks` in the public
|
|
21
23
|
[course labs](https://github.com/cairnkeep/cairnkeep-course-labs).
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# V25 - Bounded workflow playbooks
|
|
2
|
+
|
|
3
|
+
**Companion lesson:** [L25](../lessons/L25-playbooks.md)
|
|
4
|
+
**Target length:** 9 minutes
|
|
5
|
+
|
|
6
|
+
## Recording outline
|
|
7
|
+
|
|
8
|
+
1. Open with the boundary: Cairnkeep selects existing workflow actions; the
|
|
9
|
+
harness remains the agent runtime and every approval remains in force.
|
|
10
|
+
2. Run guided setup in a synthetic project. Show `.ai/playbooks.json` mode and
|
|
11
|
+
the delimited Cairnkeep block inside an `AGENTS.md` that already has user
|
|
12
|
+
content.
|
|
13
|
+
3. Compare `minimal`, `balanced`, and `strict` with `cairn playbook status`.
|
|
14
|
+
Customize one canonical action and show that an arbitrary command or unknown
|
|
15
|
+
field is rejected.
|
|
16
|
+
4. Run `check start` for a complex unfamiliar task. Explain deterministic
|
|
17
|
+
signals versus the model-supplied complexity/familiarity assertion.
|
|
18
|
+
5. Run a security-sensitive `check finish --enforce` without evidence and show
|
|
19
|
+
exit 3. Perform synthetic verification, then re-run with completed and
|
|
20
|
+
reasoned skipped evidence.
|
|
21
|
+
6. Record one outcome with exact policy and decision digests. Inspect the
|
|
22
|
+
private receipt and show that changed paths, prompts, source, and output are
|
|
23
|
+
absent.
|
|
24
|
+
7. Show `/cairn-work` in Claude or OpenCode and the portable `AGENTS.md`
|
|
25
|
+
guidance for Codex/Qwen. State that adapters route to the same CLI and do not
|
|
26
|
+
automatically activate skills.
|
|
27
|
+
8. Remove the managed instruction block, prove user instructions remain, and
|
|
28
|
+
close on the design-only team boundary.
|
|
29
|
+
|
|
30
|
+
## Recording cautions
|
|
31
|
+
|
|
32
|
+
- Use only synthetic paths, actors, reasons, and project data.
|
|
33
|
+
- Do not imply that an agent's `completed` assertion proves an action ran;
|
|
34
|
+
demonstrate the evidence boundary explicitly.
|
|
35
|
+
- Do not describe local actor labels as authenticated identities.
|
|
36
|
+
- Do not claim productivity or quality improvement without a separate measured
|
|
37
|
+
evaluation.
|
|
38
|
+
- Keep all network-capable optional integrations disabled during the demo.
|
package/docs/native-windows.md
CHANGED
|
@@ -32,6 +32,11 @@ recovery contract as POSIX. It writes a restricted `.ai/cairnkeep.json` setup
|
|
|
32
32
|
record and never syncs machine assets automatically. `cairn bootstrap` remains
|
|
33
33
|
the deterministic compatibility primitive for existing scripts.
|
|
34
34
|
|
|
35
|
+
Setup also writes private `.ai/playbooks.json` and reconciles Cairnkeep's
|
|
36
|
+
delimited workflow block in `AGENTS.md` without replacing surrounding rules.
|
|
37
|
+
The Node-native `cairn playbook` lifecycle, deterministic enforcement, private
|
|
38
|
+
receipts, and instruction management are identical on Windows and POSIX.
|
|
39
|
+
|
|
35
40
|
For Codex memory without a harness-wide sync, select only Codex:
|
|
36
41
|
|
|
37
42
|
```powershell
|
|
@@ -89,6 +94,9 @@ Load completion for the current session:
|
|
|
89
94
|
Invoke-Expression (& cairn completion powershell | Out-String)
|
|
90
95
|
```
|
|
91
96
|
|
|
97
|
+
The completer includes playbook profiles, lifecycle events, canonical actions,
|
|
98
|
+
and check/record options as well as the top-level command.
|
|
99
|
+
|
|
92
100
|
Place the same expression in `$PROFILE` to load it in future sessions.
|
|
93
101
|
|
|
94
102
|
## Storage and permissions
|
|
@@ -135,6 +143,10 @@ mutation it creates a timestamped bundle under
|
|
|
135
143
|
`revert.ps1`. Durable memory and context packs remain unless their separate
|
|
136
144
|
purge flags are supplied.
|
|
137
145
|
|
|
146
|
+
Uninstall removes only Cairnkeep's managed `AGENTS.md` block and preserves
|
|
147
|
+
surrounding project instructions. Pi and Kimi playbook adapters are handled as
|
|
148
|
+
precisely owned paths alongside their graph adapters.
|
|
149
|
+
|
|
138
150
|
## Verification boundary
|
|
139
151
|
|
|
140
152
|
The release gate runs from `windows-latest` using PowerShell for Node 22, 24,
|
package/docs/operating.md
CHANGED
|
@@ -830,6 +830,60 @@ is not an authorization boundary. See [Memory storage and deployment](storage.md
|
|
|
830
830
|
for the placement rules, client registration, TLS requirements, project headers,
|
|
831
831
|
and backup boundaries.
|
|
832
832
|
|
|
833
|
+
## Project playbooks
|
|
834
|
+
|
|
835
|
+
Guided setup and tracked bootstrap create private `.ai/playbooks.json` mode
|
|
836
|
+
`0600` with the `balanced` profile and reconcile a delimited block in the
|
|
837
|
+
project's `AGENTS.md`. `--untracked` bootstrap leaves the shared `AGENTS.md`
|
|
838
|
+
untouched. The policy can select only eight canonical Cairnkeep actions; it
|
|
839
|
+
cannot contain commands, prompts, URLs, or extensions.
|
|
840
|
+
|
|
841
|
+
```bash
|
|
842
|
+
cairn playbook list
|
|
843
|
+
cairn playbook status --project .
|
|
844
|
+
cairn playbook set strict
|
|
845
|
+
cairn playbook enable review.security must
|
|
846
|
+
cairn playbook disable learning.capture
|
|
847
|
+
cairn playbook reset learning.capture
|
|
848
|
+
```
|
|
849
|
+
|
|
850
|
+
At task start, use `check start`; after a material risk or scope change, use
|
|
851
|
+
`check check`; before completion, use `check finish`. Supply actual changed
|
|
852
|
+
paths and evidence rather than trusting a model's unsupported claim:
|
|
853
|
+
|
|
854
|
+
```bash
|
|
855
|
+
cairn playbook check finish --changed src/auth.ts docs/security.md \
|
|
856
|
+
--risk security --public-change \
|
|
857
|
+
--completed verify.tests review.repository review.security docs.update \
|
|
858
|
+
--enforce
|
|
859
|
+
```
|
|
860
|
+
|
|
861
|
+
Exit 3 means applicable `must` evidence is missing. Exit 2 means the policy has
|
|
862
|
+
diagnostic issues. Otherwise the check succeeded. `should` actions remain
|
|
863
|
+
advisory and should carry a concrete `--skipped ACTION=REASON` when omitted.
|
|
864
|
+
Checking never executes the displayed commands, contacts a service, enables a
|
|
865
|
+
capability, or grants approval.
|
|
866
|
+
|
|
867
|
+
Material outcomes can be stored with `cairn playbook record` using the exact
|
|
868
|
+
policy and decision digests from the check. Inspect them with `cairn playbook
|
|
869
|
+
receipts list|show`; validate state with `cairn playbook doctor`. Actor and
|
|
870
|
+
session options are provenance labels only: v2.15 does not authenticate them
|
|
871
|
+
and does not provide team ACLs. See [team mode](design/team-mode.md) for the
|
|
872
|
+
future admission contract.
|
|
873
|
+
|
|
874
|
+
The managed instruction lifecycle is explicit and ownership-safe:
|
|
875
|
+
|
|
876
|
+
```bash
|
|
877
|
+
cairn playbook instructions check
|
|
878
|
+
cairn playbook instructions install
|
|
879
|
+
cairn playbook instructions remove
|
|
880
|
+
```
|
|
881
|
+
|
|
882
|
+
Claude and OpenCode receive `/cairn-work`; Pi and Kimi receive corresponding
|
|
883
|
+
thin prompt/Skill adapters when their explicit machine sync runs. Codex and
|
|
884
|
+
Qwen follow the project `AGENTS.md` block. These surfaces route to the same CLI
|
|
885
|
+
and do not create a Cairnkeep-owned agent loop.
|
|
886
|
+
|
|
833
887
|
## The workflow
|
|
834
888
|
|
|
835
889
|
Once installed, the operating layer gives you:
|
|
@@ -43,12 +43,35 @@ request unless the corresponding endpoint and credential are configured.
|
|
|
43
43
|
| Guided project setup | None | Selected scaffold assets and private `.ai/cairnkeep.json` state in the target; machine sync is never automatic |
|
|
44
44
|
| Codex project setup and launcher | None | Local `.codex/config.toml` starts `cairn memory-server` over stdio only after the operator trusts the project |
|
|
45
45
|
| Pi memory extension | None | Local `cairn memory-server` stdio child; no HTTP transport is inherited |
|
|
46
|
+
| Playbook policy check | None | Reads local `.ai/playbooks.json`; returns bounded action IDs, rationales, commands, and digests without executing them |
|
|
47
|
+
| Explicit playbook receipt | None | Private project-local `.agentfs/playbooks/receipts/`; no prompts, source bodies, diffs, credentials, or environment snapshot |
|
|
46
48
|
|
|
47
49
|
Model endpoints may be local or remote. Cairnkeep cannot determine a provider's
|
|
48
50
|
retention, training, or logging policy; verify it before sending confidential
|
|
49
51
|
material. Disabling `CAIRN_LLM_API_KEY`, `ANYTHINGLLM_API_KEY`, remote HTTP
|
|
50
52
|
registration, and delegated tools keeps the core memory workflow local.
|
|
51
53
|
|
|
54
|
+
## Playbook decision and receipt flow
|
|
55
|
+
|
|
56
|
+
Playbooks are local workflow policy, not model inference. A check reads the
|
|
57
|
+
strict project policy, bounded caller signals, changed-path labels, and explicit
|
|
58
|
+
evidence. It deterministically returns applicable canonical actions and policy
|
|
59
|
+
and decision digests. It performs no network request, tool action, capability
|
|
60
|
+
change, approval, or durable-memory promotion.
|
|
61
|
+
|
|
62
|
+
Only an explicit `cairn playbook record` writes a receipt. The receipt stores
|
|
63
|
+
project identity, digests, event, canonical action, outcome, bounded reason,
|
|
64
|
+
timestamp, and the supplied actor/session labels. It excludes prompt content,
|
|
65
|
+
file content, patches, command output, secrets, endpoint values, and environment
|
|
66
|
+
snapshots. Changed paths influence a decision but are not copied into receipts.
|
|
67
|
+
|
|
68
|
+
In v2.15, actor identity is an unauthenticated local assertion even when a
|
|
69
|
+
harness supplies it. Do not use receipts for employee monitoring,
|
|
70
|
+
authorization, non-repudiation, or team access decisions. The future
|
|
71
|
+
[team-mode contract](design/team-mode.md) requires authenticated subjects,
|
|
72
|
+
deny-by-default ACLs, tenant isolation, retention, and append-only auditing
|
|
73
|
+
before shared operation can ship.
|
|
74
|
+
|
|
52
75
|
## Data at rest
|
|
53
76
|
|
|
54
77
|
Memories are stored in SQLite databases. They are not encrypted by Cairnkeep at
|
package/docs/quickstart.md
CHANGED
|
@@ -69,7 +69,9 @@ result should survive the restart.
|
|
|
69
69
|
## What setup owns
|
|
70
70
|
|
|
71
71
|
`cairn setup` creates only the selected harness assets plus the common `.ai`,
|
|
72
|
-
`.planning`, and `.agentfs` scaffold. It
|
|
72
|
+
`.planning`, and `.agentfs` scaffold. It also reconciles Cairnkeep's delimited
|
|
73
|
+
playbook block in `AGENTS.md` while preserving every surrounding user byte and
|
|
74
|
+
file mode. It initializes Git only with explicit
|
|
73
75
|
`--git init`, preserves unrelated files, does not overwrite modified files it
|
|
74
76
|
does not own, and records managed digests in private `.ai/cairnkeep.json`.
|
|
75
77
|
|
|
@@ -81,6 +83,20 @@ cairn doctor # diagnose
|
|
|
81
83
|
cairn uninstall --dry-run . # preview removal
|
|
82
84
|
```
|
|
83
85
|
|
|
86
|
+
The generated balanced policy helps compatible agents decide which existing
|
|
87
|
+
workflow checks apply. Inspect it directly before relying on agent routing:
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
cairn playbook status
|
|
91
|
+
cairn playbook check start --complexity standard --familiarity mixed
|
|
92
|
+
cairn playbook check finish --changed README.md --completed docs.update --enforce
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
`must` can produce a non-zero enforcement result; `should` is advisory with a
|
|
96
|
+
recorded reason when skipped; `may` is optional. A decision executes nothing
|
|
97
|
+
and grants no permission. `cairn playbook instructions remove` removes only the
|
|
98
|
+
managed `AGENTS.md` block.
|
|
99
|
+
|
|
84
100
|
For all harnesses, remote HTTP, storage, profiles, context packs, overlays, and
|
|
85
101
|
recovery details, continue with [Operating Cairnkeep](operating.md) or the
|
|
86
102
|
[learning path](learning/README.md).
|
package/docs/storage.md
CHANGED
|
@@ -57,6 +57,28 @@ indexes. Defaults are 5 MiB per serialized session, 256 MiB logical total and
|
|
|
57
57
|
and then the oldest records needed to meet the logical budget. `prune
|
|
58
58
|
--dry-run` reports the same decision without changing the database.
|
|
59
59
|
|
|
60
|
+
## Playbook policy and receipt storage
|
|
61
|
+
|
|
62
|
+
The project policy is `.ai/playbooks.json`, a strict schema-versioned file with
|
|
63
|
+
private permissions. It contains a built-in profile name and bounded overrides
|
|
64
|
+
for canonical actions only. The effective policy digest is derived from its
|
|
65
|
+
canonical meaning, so formatting changes do not create a different policy.
|
|
66
|
+
|
|
67
|
+
Recorded outcomes are immutable JSON files under
|
|
68
|
+
`.agentfs/playbooks/receipts/`. Each receipt is at most 8 KiB and binds the
|
|
69
|
+
canonical project identity, policy and decision digests, local actor assertion,
|
|
70
|
+
session, event, action, outcome, bounded reason, and timestamp. The store is
|
|
71
|
+
private, capped at 10,000 receipts, and rejects symlinks, unsafe file kinds,
|
|
72
|
+
unsafe permissions, and malformed bytes. It deliberately contains no prompts,
|
|
73
|
+
source bodies, diffs, credentials, or environment snapshots.
|
|
74
|
+
|
|
75
|
+
Back up `.ai/playbooks.json` with project configuration and
|
|
76
|
+
`.agentfs/playbooks/` with other project-local evidence. Copying receipts does
|
|
77
|
+
not make their actor authenticated. Run `cairn playbook doctor --project PATH`
|
|
78
|
+
after a restore. Ordinary uninstall removes the setup-owned policy and managed
|
|
79
|
+
instruction block backup-first, while retained `.agentfs` evidence follows the
|
|
80
|
+
normal project-data retention choice.
|
|
81
|
+
|
|
60
82
|
## Capability callback storage
|
|
61
83
|
|
|
62
84
|
Managed operating callbacks use two separate versioned KV namespaces inside
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"identifier":"cairn-offline-fake-v1","package_version":"2.
|
|
1
|
+
{"identifier":"cairn-offline-fake-v1","package_version":"2.15.1","schema_version":1,"task_set_digest":"13e95379f35839593047ec34521a65a8fa3ab92bd3993bca9dc8bf5c4a9ff36f"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: cairn-work
|
|
3
|
+
description: Evaluate Cairnkeep's bounded workflow playbook at task lifecycle boundaries
|
|
4
|
+
type: flow
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
<!-- managed-by:cairnkeep -->
|
|
8
|
+
# Cairnkeep playbook check
|
|
9
|
+
|
|
10
|
+
Invoke Bash exactly once with `cairn playbook check $ARGUMENTS`. Accept only
|
|
11
|
+
`start`, `check`, or `finish` plus documented check options. Return the decision
|
|
12
|
+
verbatim, then state how you will handle its `must`, `should`, and `may`
|
|
13
|
+
actions. Policy evaluation never executes an action or grants approval.
|
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { PLAYBOOK_ACTIONS, evaluatePlaybook, inferChangeTypes, initializePlaybook, resetPlaybook, resetPlaybookOverride, resolvePlaybookStatus, setPlaybookOverride, setPlaybookProfile, } from "./playbook.js";
|
|
3
|
+
import { doctorPlaybooks, listPlaybookReceipts, readPlaybookReceipt, recordPlaybookReceipt } from "./playbook-receipt.js";
|
|
4
|
+
import { PLAYBOOK_ACTION_IDS, PLAYBOOK_CHANGE_TYPES, PLAYBOOK_PROFILES, playbookActionIdSchema, playbookActorKindSchema, playbookComplexitySchema, playbookEventSchema, playbookFamiliaritySchema, playbookModeSchema, playbookOutcomeSchema, playbookProfileSchema, playbookRiskSchema, } from "./playbook-schema.js";
|
|
5
|
+
process.stdout.on("error", (error) => {
|
|
6
|
+
if (error.code === "EPIPE")
|
|
7
|
+
process.exit(0);
|
|
8
|
+
throw error;
|
|
9
|
+
});
|
|
10
|
+
function usage() {
|
|
11
|
+
return `cairn playbook — bounded workflow policy for coding agents
|
|
12
|
+
|
|
13
|
+
Usage:
|
|
14
|
+
cairn playbook list [--json]
|
|
15
|
+
cairn playbook status [--project PATH] [--json]
|
|
16
|
+
cairn playbook init [minimal|balanced|strict] [--project PATH] [--json]
|
|
17
|
+
cairn playbook set minimal|balanced|strict [--project PATH] [--json]
|
|
18
|
+
cairn playbook enable ACTION must|should|may [--project PATH] [--json]
|
|
19
|
+
cairn playbook disable ACTION [--project PATH] [--json]
|
|
20
|
+
cairn playbook reset [ACTION] [--project PATH] [--json]
|
|
21
|
+
cairn playbook check start|check|finish [signals] [evidence] [--enforce] [--json]
|
|
22
|
+
cairn playbook record --policy DIGEST --decision DIGEST --event EVENT --action ACTION --outcome OUTCOME [identity] [--json]
|
|
23
|
+
cairn playbook receipts list|show [RECEIPT] [--project PATH] [--json]
|
|
24
|
+
cairn playbook instructions install|check|remove [--project PATH] [--json]
|
|
25
|
+
cairn playbook doctor [--project PATH] [--json]
|
|
26
|
+
|
|
27
|
+
Check signals:
|
|
28
|
+
--complexity trivial|standard|complex --familiarity known|mixed|unfamiliar
|
|
29
|
+
--risk low|normal|high|security --public-change
|
|
30
|
+
--changed PATH... --change-type TYPE...
|
|
31
|
+
--completed ACTION... --skipped ACTION=REASON...
|
|
32
|
+
--failed ACTION=REASON... --actor ID --actor-kind user|agent|service --session ID
|
|
33
|
+
|
|
34
|
+
Policies select only Cairnkeep-owned actions. Checking never executes an action,
|
|
35
|
+
contacts a network service, or grants approval. Actor identity is caller-supplied
|
|
36
|
+
and unauthenticated until a future team deployment binds it to authentication.
|
|
37
|
+
`;
|
|
38
|
+
}
|
|
39
|
+
const VALUE_OPTIONS = new Set([
|
|
40
|
+
"--project", "--complexity", "--familiarity", "--risk", "--changed", "--change-type",
|
|
41
|
+
"--completed", "--skipped", "--failed", "--actor", "--actor-kind", "--session",
|
|
42
|
+
"--policy", "--decision", "--event", "--action", "--outcome", "--reason",
|
|
43
|
+
]);
|
|
44
|
+
const FLAG_OPTIONS = new Set(["--json", "--enforce", "--public-change"]);
|
|
45
|
+
const MULTI_OPTIONS = new Set(["--changed", "--change-type", "--completed", "--skipped", "--failed"]);
|
|
46
|
+
function parse(raw) {
|
|
47
|
+
const positional = [];
|
|
48
|
+
const options = new Map();
|
|
49
|
+
const flags = new Set();
|
|
50
|
+
for (let index = 0; index < raw.length; index += 1) {
|
|
51
|
+
const value = raw[index];
|
|
52
|
+
if (FLAG_OPTIONS.has(value)) {
|
|
53
|
+
if (flags.has(value))
|
|
54
|
+
throw new Error(`Duplicate ${value}.`);
|
|
55
|
+
flags.add(value);
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
if (VALUE_OPTIONS.has(value)) {
|
|
59
|
+
const values = options.get(value) ?? [];
|
|
60
|
+
if (!MULTI_OPTIONS.has(value) && values.length)
|
|
61
|
+
throw new Error(`Duplicate ${value}.`);
|
|
62
|
+
let consumed = 0;
|
|
63
|
+
while (index + 1 < raw.length && !raw[index + 1].startsWith("--")) {
|
|
64
|
+
values.push(raw[index + 1]);
|
|
65
|
+
index += 1;
|
|
66
|
+
consumed += 1;
|
|
67
|
+
if (!MULTI_OPTIONS.has(value))
|
|
68
|
+
break;
|
|
69
|
+
}
|
|
70
|
+
if (consumed === 0)
|
|
71
|
+
throw new Error(`Missing value for ${value}.`);
|
|
72
|
+
options.set(value, values);
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
if (value.startsWith("--"))
|
|
76
|
+
throw new Error(`Unknown option ${value}.`);
|
|
77
|
+
positional.push(value);
|
|
78
|
+
}
|
|
79
|
+
return {
|
|
80
|
+
positional,
|
|
81
|
+
options,
|
|
82
|
+
flags,
|
|
83
|
+
json: flags.has("--json"),
|
|
84
|
+
projectRoot: options.get("--project")?.[0] ?? process.cwd(),
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
function one(parsed, name, fallback) {
|
|
88
|
+
return parsed.options.get(name)?.[0] ?? fallback;
|
|
89
|
+
}
|
|
90
|
+
function many(parsed, name) {
|
|
91
|
+
return parsed.options.get(name) ?? [];
|
|
92
|
+
}
|
|
93
|
+
function ensureAllowed(parsed, allowedOptions, allowedFlags) {
|
|
94
|
+
for (const key of parsed.options.keys())
|
|
95
|
+
if (!allowedOptions.includes(key))
|
|
96
|
+
throw new Error(`${key} is not valid for this command.`);
|
|
97
|
+
for (const key of parsed.flags)
|
|
98
|
+
if (!allowedFlags.includes(key))
|
|
99
|
+
throw new Error(`${key} is not valid for this command.`);
|
|
100
|
+
}
|
|
101
|
+
function output(value, json, human) {
|
|
102
|
+
process.stdout.write(`${json ? JSON.stringify(value) : human}\n`);
|
|
103
|
+
}
|
|
104
|
+
function humanStatus(status) {
|
|
105
|
+
return [
|
|
106
|
+
`Profile: ${status.profile}`,
|
|
107
|
+
`Source: ${status.source}`,
|
|
108
|
+
`Policy digest: ${status.policy_digest}`,
|
|
109
|
+
`Configuration: ${status.config_exists ? ".ai/playbooks.json" : "built-in balanced default"}`,
|
|
110
|
+
`Issues: ${status.issues.length ? status.issues.join(", ") : "none"}`,
|
|
111
|
+
...PLAYBOOK_ACTION_IDS.map((id) => `${id}\t${status.modes[id]}${status.overrides[id] ? "\toverride" : ""}`),
|
|
112
|
+
].join("\n");
|
|
113
|
+
}
|
|
114
|
+
function parseEvidence(values, outcome) {
|
|
115
|
+
return values.map((value) => {
|
|
116
|
+
const separator = value.indexOf("=");
|
|
117
|
+
const action = separator < 0 ? value : value.slice(0, separator);
|
|
118
|
+
const reason = separator < 0 ? "" : value.slice(separator + 1);
|
|
119
|
+
playbookActionIdSchema.parse(action);
|
|
120
|
+
if (outcome !== "completed" && reason.trim().length === 0)
|
|
121
|
+
throw new Error(`${outcome} evidence requires ACTION=REASON.`);
|
|
122
|
+
return { action: action, outcome, reason };
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
function actor(parsed) {
|
|
126
|
+
const id = one(parsed, "--actor", process.env.CAIRN_PLAYBOOK_ACTOR ?? "local-agent");
|
|
127
|
+
const kind = playbookActorKindSchema.parse(one(parsed, "--actor-kind", process.env.CAIRN_PLAYBOOK_ACTOR_KIND ?? "agent"));
|
|
128
|
+
return { id, kind, authenticated: false };
|
|
129
|
+
}
|
|
130
|
+
function session(parsed) {
|
|
131
|
+
return one(parsed, "--session", process.env.CAIRN_PLAYBOOK_SESSION ?? `local-${process.pid}`);
|
|
132
|
+
}
|
|
133
|
+
function humanDecision(value) {
|
|
134
|
+
const lines = [
|
|
135
|
+
`Playbook: ${value.profile} (${value.policy_digest})`,
|
|
136
|
+
`Decision: ${value.decision_digest}`,
|
|
137
|
+
`Event: ${value.event}`,
|
|
138
|
+
];
|
|
139
|
+
if (value.actions.length === 0)
|
|
140
|
+
lines.push("Actions: none");
|
|
141
|
+
else {
|
|
142
|
+
lines.push("Actions:");
|
|
143
|
+
for (const row of value.actions) {
|
|
144
|
+
lines.push(` [${row.outcome === "completed" ? "x" : " "}] ${row.mode.toUpperCase()} ${row.id} — ${row.title}`);
|
|
145
|
+
lines.push(` ${row.rationale}`);
|
|
146
|
+
lines.push(` ${row.command}`);
|
|
147
|
+
if (row.reason)
|
|
148
|
+
lines.push(` evidence: ${row.outcome} — ${row.reason}`);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
lines.push(`Blocking: ${value.blocking_actions.length ? value.blocking_actions.join(", ") : "none"}`);
|
|
152
|
+
lines.push("Actor authentication: unverified local assertion");
|
|
153
|
+
return lines.join("\n");
|
|
154
|
+
}
|
|
155
|
+
async function check(parsed) {
|
|
156
|
+
ensureAllowed(parsed, ["--project", "--complexity", "--familiarity", "--risk", "--changed", "--change-type", "--completed", "--skipped", "--failed", "--actor", "--actor-kind", "--session"], ["--json", "--enforce", "--public-change"]);
|
|
157
|
+
if (parsed.positional.length !== 1)
|
|
158
|
+
throw new Error("check requires one lifecycle event.");
|
|
159
|
+
const event = playbookEventSchema.parse(parsed.positional[0]);
|
|
160
|
+
const explicitTypes = many(parsed, "--change-type").map((value) => {
|
|
161
|
+
if (!PLAYBOOK_CHANGE_TYPES.includes(value))
|
|
162
|
+
throw new Error(`Unknown change type ${value}.`);
|
|
163
|
+
return value;
|
|
164
|
+
});
|
|
165
|
+
const changed = many(parsed, "--changed");
|
|
166
|
+
const evidence = [
|
|
167
|
+
...parseEvidence(many(parsed, "--completed"), "completed"),
|
|
168
|
+
...parseEvidence(many(parsed, "--skipped"), "skipped"),
|
|
169
|
+
...parseEvidence(many(parsed, "--failed"), "failed"),
|
|
170
|
+
];
|
|
171
|
+
const value = await evaluatePlaybook({
|
|
172
|
+
projectRoot: parsed.projectRoot,
|
|
173
|
+
event,
|
|
174
|
+
actor: actor(parsed),
|
|
175
|
+
sessionId: session(parsed),
|
|
176
|
+
signals: {
|
|
177
|
+
complexity: playbookComplexitySchema.parse(one(parsed, "--complexity", "standard")),
|
|
178
|
+
familiarity: playbookFamiliaritySchema.parse(one(parsed, "--familiarity", "mixed")),
|
|
179
|
+
risk: playbookRiskSchema.parse(one(parsed, "--risk", "normal")),
|
|
180
|
+
public_change: parsed.flags.has("--public-change"),
|
|
181
|
+
changed_paths: changed,
|
|
182
|
+
change_types: inferChangeTypes(changed, explicitTypes),
|
|
183
|
+
},
|
|
184
|
+
evidence,
|
|
185
|
+
});
|
|
186
|
+
output(value, parsed.json, humanDecision(value));
|
|
187
|
+
if (value.issues.length)
|
|
188
|
+
process.exitCode = 2;
|
|
189
|
+
else if (parsed.flags.has("--enforce") && value.blocking_actions.length)
|
|
190
|
+
process.exitCode = 3;
|
|
191
|
+
}
|
|
192
|
+
async function record(parsed) {
|
|
193
|
+
ensureAllowed(parsed, ["--project", "--policy", "--decision", "--event", "--action", "--outcome", "--reason", "--actor", "--actor-kind", "--session"], ["--json"]);
|
|
194
|
+
if (parsed.positional.length !== 0)
|
|
195
|
+
throw new Error("record accepts options only.");
|
|
196
|
+
const value = await recordPlaybookReceipt({
|
|
197
|
+
projectRoot: parsed.projectRoot,
|
|
198
|
+
policyDigest: one(parsed, "--policy") ?? "",
|
|
199
|
+
decisionDigest: one(parsed, "--decision") ?? "",
|
|
200
|
+
actor: actor(parsed),
|
|
201
|
+
sessionId: session(parsed),
|
|
202
|
+
event: playbookEventSchema.parse(one(parsed, "--event")),
|
|
203
|
+
action: playbookActionIdSchema.parse(one(parsed, "--action")),
|
|
204
|
+
outcome: playbookOutcomeSchema.parse(one(parsed, "--outcome")),
|
|
205
|
+
reason: one(parsed, "--reason", ""),
|
|
206
|
+
});
|
|
207
|
+
output(value, parsed.json, `Recorded ${value.receipt_id}: ${value.action} ${value.outcome}.`);
|
|
208
|
+
}
|
|
209
|
+
async function main() {
|
|
210
|
+
const [command = "help", ...raw] = process.argv.slice(2);
|
|
211
|
+
if (["help", "--help", "-h"].includes(command)) {
|
|
212
|
+
if (raw.length)
|
|
213
|
+
throw new Error("help accepts no arguments.");
|
|
214
|
+
process.stdout.write(usage());
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
const parsed = parse(raw);
|
|
218
|
+
if (command === "list") {
|
|
219
|
+
ensureAllowed(parsed, [], ["--json"]);
|
|
220
|
+
if (parsed.positional.length)
|
|
221
|
+
throw new Error("list accepts no arguments.");
|
|
222
|
+
const value = { schema_version: 1, profiles: PLAYBOOK_PROFILES, actions: PLAYBOOK_ACTIONS };
|
|
223
|
+
output(value, parsed.json, PLAYBOOK_ACTIONS.map(({ id, title, command: actionCommand }) => `${id}\t${title}\t${actionCommand}`).join("\n"));
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
if (command === "status") {
|
|
227
|
+
ensureAllowed(parsed, ["--project"], ["--json"]);
|
|
228
|
+
if (parsed.positional.length)
|
|
229
|
+
throw new Error("status accepts no positional arguments.");
|
|
230
|
+
const value = await resolvePlaybookStatus({ projectRoot: parsed.projectRoot });
|
|
231
|
+
output(value, parsed.json, humanStatus(value));
|
|
232
|
+
if (value.issues.length)
|
|
233
|
+
process.exitCode = 2;
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
if (command === "init" || command === "set") {
|
|
237
|
+
ensureAllowed(parsed, ["--project"], ["--json"]);
|
|
238
|
+
if (parsed.positional.length > 1 || (command === "set" && parsed.positional.length !== 1))
|
|
239
|
+
throw new Error(`${command} requires one profile.`);
|
|
240
|
+
const profile = playbookProfileSchema.parse(parsed.positional[0] ?? "balanced");
|
|
241
|
+
const value = command === "init"
|
|
242
|
+
? await initializePlaybook(parsed.projectRoot, profile)
|
|
243
|
+
: await setPlaybookProfile(parsed.projectRoot, profile);
|
|
244
|
+
output(value, parsed.json, humanStatus(value));
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
if (command === "enable" || command === "disable") {
|
|
248
|
+
ensureAllowed(parsed, ["--project"], ["--json"]);
|
|
249
|
+
if (parsed.positional.length !== (command === "enable" ? 2 : 1))
|
|
250
|
+
throw new Error(`${command} has invalid arguments.`);
|
|
251
|
+
const action = playbookActionIdSchema.parse(parsed.positional[0]);
|
|
252
|
+
const mode = command === "disable" ? "off" : playbookModeSchema.parse(parsed.positional[1]);
|
|
253
|
+
if (mode === "off" && command === "enable")
|
|
254
|
+
throw new Error("Use disable ACTION to turn an action off.");
|
|
255
|
+
const value = await setPlaybookOverride(parsed.projectRoot, action, mode);
|
|
256
|
+
output(value, parsed.json, humanStatus(value));
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
if (command === "reset") {
|
|
260
|
+
ensureAllowed(parsed, ["--project"], ["--json"]);
|
|
261
|
+
if (parsed.positional.length > 1)
|
|
262
|
+
throw new Error("reset accepts at most one action.");
|
|
263
|
+
const value = parsed.positional[0]
|
|
264
|
+
? await resetPlaybookOverride(parsed.projectRoot, playbookActionIdSchema.parse(parsed.positional[0]))
|
|
265
|
+
: await resetPlaybook(parsed.projectRoot);
|
|
266
|
+
output(value, parsed.json, humanStatus(value));
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
if (command === "check") {
|
|
270
|
+
await check(parsed);
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
if (command === "record") {
|
|
274
|
+
await record(parsed);
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
if (command === "receipts") {
|
|
278
|
+
ensureAllowed(parsed, ["--project"], ["--json"]);
|
|
279
|
+
const operation = parsed.positional[0] ?? "list";
|
|
280
|
+
if (operation === "list" && parsed.positional.length === 1) {
|
|
281
|
+
const value = await listPlaybookReceipts(parsed.projectRoot);
|
|
282
|
+
output(value, parsed.json, value.receipts.length ? value.receipts.map(({ receipt_id, action, outcome, recorded_at }) => `${receipt_id}\t${outcome}\t${action}\t${recorded_at}`).join("\n") : "No playbook receipts found.");
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
if (operation === "show" && parsed.positional.length === 2) {
|
|
286
|
+
const value = await readPlaybookReceipt(parsed.positional[1], parsed.projectRoot);
|
|
287
|
+
output(value, parsed.json, JSON.stringify(value, null, 2));
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
throw new Error("receipts requires list or show RECEIPT.");
|
|
291
|
+
}
|
|
292
|
+
if (command === "doctor") {
|
|
293
|
+
ensureAllowed(parsed, ["--project"], ["--json"]);
|
|
294
|
+
if (parsed.positional.length)
|
|
295
|
+
throw new Error("doctor accepts no positional arguments.");
|
|
296
|
+
const value = await doctorPlaybooks(parsed.projectRoot);
|
|
297
|
+
output(value, parsed.json, JSON.stringify(value, null, 2));
|
|
298
|
+
if (!value.ok)
|
|
299
|
+
process.exitCode = 2;
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
if (command === "instructions") {
|
|
303
|
+
ensureAllowed(parsed, ["--project"], ["--json"]);
|
|
304
|
+
if (parsed.positional.length !== 1 || !["install", "check", "remove"].includes(parsed.positional[0]))
|
|
305
|
+
throw new Error("instructions requires install, check, or remove.");
|
|
306
|
+
const moduleUrl = new URL("../../scripts/playbook-instructions.mjs", import.meta.url).href;
|
|
307
|
+
const instructionModule = await import(moduleUrl);
|
|
308
|
+
const value = parsed.positional[0] === "remove"
|
|
309
|
+
? instructionModule.removePlaybookInstructions(parsed.projectRoot, { check: false })
|
|
310
|
+
: instructionModule.reconcilePlaybookInstructions(parsed.projectRoot, { check: parsed.positional[0] === "check" });
|
|
311
|
+
output(value, parsed.json, `${value.status}: ${value.path}`);
|
|
312
|
+
if (parsed.positional[0] === "check" && value.status !== "unchanged")
|
|
313
|
+
process.exitCode = 1;
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
throw new Error(`unknown playbook command "${command}".`);
|
|
317
|
+
}
|
|
318
|
+
main().catch((error) => {
|
|
319
|
+
process.stderr.write(`cairn playbook: ${error instanceof Error ? error.message : "invalid command or policy"}\n`);
|
|
320
|
+
process.exitCode = 2;
|
|
321
|
+
});
|