@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.
Files changed (60) hide show
  1. package/CHANGELOG.md +41 -1
  2. package/README.md +22 -2
  3. package/claude/commands/cairn-work.md +19 -0
  4. package/docs/design/team-mode.md +145 -0
  5. package/docs/harness-compatibility.md +11 -10
  6. package/docs/learning/CURRICULUM-MAP.md +4 -1
  7. package/docs/learning/FEATURE-GUIDE.md +8 -4
  8. package/docs/learning/README.md +2 -1
  9. package/docs/learning/lessons/L00-why-cairnkeep.md +1 -1
  10. package/docs/learning/lessons/L01-safe-trial.md +1 -1
  11. package/docs/learning/lessons/L02-installation.md +1 -1
  12. package/docs/learning/lessons/L03-first-project.md +1 -1
  13. package/docs/learning/lessons/L19-validated-skill-improvement.md +1 -1
  14. package/docs/learning/lessons/L20-mcp-tool-profiles.md +1 -1
  15. package/docs/learning/lessons/L21-context-packs.md +1 -1
  16. package/docs/learning/lessons/L22-native-windows.md +1 -1
  17. package/docs/learning/lessons/L23-guided-setup.md +1 -1
  18. package/docs/learning/lessons/L24-okf-exchange.md +1 -1
  19. package/docs/learning/lessons/L25-playbooks.md +114 -0
  20. package/docs/learning/tracks/operator.md +5 -2
  21. package/docs/learning/tracks/practitioner.md +5 -3
  22. package/docs/learning/video-scripts/V25-playbooks.md +38 -0
  23. package/docs/native-windows.md +12 -0
  24. package/docs/operating.md +54 -0
  25. package/docs/privacy-and-data-flow.md +23 -0
  26. package/docs/quickstart.md +17 -1
  27. package/docs/storage.md +22 -0
  28. package/examples/eval/bundled-fake.json +1 -1
  29. package/kimi/skills/cairn-work/SKILL.md +13 -0
  30. package/mcp-memory-server/dist/playbook-cli.js +321 -0
  31. package/mcp-memory-server/dist/playbook-receipt.js +245 -0
  32. package/mcp-memory-server/dist/playbook-schema.js +56 -0
  33. package/mcp-memory-server/dist/playbook.js +396 -0
  34. package/mcp-memory-server/package.json +2 -1
  35. package/opencode/command/cairn-work.md +13 -0
  36. package/package.json +1 -1
  37. package/pi/prompts/cairn-work.md +12 -0
  38. package/schemas/playbook.schema.json +29 -0
  39. package/scripts/bootstrap.sh +17 -0
  40. package/scripts/cairn-cli.mjs +2 -0
  41. package/scripts/completion.sh +23 -3
  42. package/scripts/doctor.sh +14 -1
  43. package/scripts/playbook-instructions.mjs +108 -0
  44. package/scripts/setup-core.mjs +1 -0
  45. package/scripts/setup.mjs +8 -0
  46. package/scripts/sync-kimi-assets.sh +3 -3
  47. package/scripts/sync-opencode-memory-assets.sh +2 -1
  48. package/scripts/sync-pi-assets.sh +2 -1
  49. package/scripts/test-cli-dispatch.sh +2 -0
  50. package/scripts/test-completion.sh +11 -2
  51. package/scripts/test-graph-harness-adapters.sh +9 -3
  52. package/scripts/test-harness-registry.sh +11 -0
  53. package/scripts/test-learning-docs.sh +23 -2
  54. package/scripts/test-package-install.sh +26 -0
  55. package/scripts/test-playbook-instructions.sh +60 -0
  56. package/scripts/test-windows-native.mjs +7 -0
  57. package/scripts/uninstall.sh +16 -1
  58. package/scripts/windows-platform.mjs +24 -3
  59. package/templates/playbook-agent-instructions.md.template +14 -0
  60. 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 10 minutes
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.
@@ -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
@@ -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 initializes Git only with explicit
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.14.0","schema_version":1,"task_set_digest":"13e95379f35839593047ec34521a65a8fa3ab92bd3993bca9dc8bf5c4a9ff36f"}
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
+ });