@cairnkeep/cli 2.13.1 → 2.15.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 +45 -1
- package/README.md +29 -6
- package/claude/commands/cairn-work.md +19 -0
- package/docs/context-packs.md +13 -3
- package/docs/design/team-mode.md +145 -0
- package/docs/harness-compatibility.md +12 -11
- package/docs/learning/CURRICULUM-MAP.md +8 -4
- package/docs/learning/FEATURE-GUIDE.md +13 -5
- package/docs/learning/README.md +3 -1
- package/docs/learning/lessons/L00-why-cairnkeep.md +1 -1
- package/docs/learning/lessons/L01-safe-trial.md +3 -3
- package/docs/learning/lessons/L02-installation.md +2 -2
- 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 +5 -1
- package/docs/learning/lessons/L22-native-windows.md +2 -2
- package/docs/learning/lessons/L23-guided-setup.md +2 -2
- package/docs/learning/lessons/L24-okf-exchange.md +111 -0
- package/docs/learning/lessons/L25-playbooks.md +114 -0
- package/docs/learning/tracks/operator.md +8 -2
- package/docs/learning/tracks/practitioner.md +5 -3
- package/docs/learning/video-scripts/V24-okf-exchange.md +31 -0
- package/docs/learning/video-scripts/V25-playbooks.md +38 -0
- package/docs/native-windows.md +12 -0
- package/docs/open-knowledge-format.md +100 -0
- package/docs/operating.md +68 -0
- package/docs/privacy-and-data-flow.md +25 -0
- package/docs/quickstart.md +17 -1
- package/docs/storage.md +28 -0
- package/examples/eval/bundled-fake.json +1 -1
- package/kimi/skills/cairn-work/SKILL.md +13 -0
- package/mcp-memory-server/dist/context-pack-cli.js +54 -1
- package/mcp-memory-server/dist/context-pack.js +243 -47
- package/mcp-memory-server/dist/index.js +13 -1
- package/mcp-memory-server/dist/mcp-tool-catalog.js +1 -0
- package/mcp-memory-server/dist/note-store.js +44 -2
- package/mcp-memory-server/dist/okf.js +432 -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 +383 -0
- package/mcp-memory-server/package.json +4 -1
- package/opencode/command/cairn-work.md +13 -0
- package/package.json +2 -1
- package/pi/prompts/cairn-work.md +12 -0
- package/schemas/context-pack.schema.json +9 -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 +26 -6
- 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
package/scripts/doctor.sh
CHANGED
|
@@ -230,7 +230,20 @@ try {
|
|
|
230
230
|
[[ $evidence_status -eq 0 || "$evidence_state" == "broken" ]] || true
|
|
231
231
|
fi
|
|
232
232
|
|
|
233
|
-
# 10.
|
|
233
|
+
# 10. Project playbook policy and private receipts. Absence is healthy because
|
|
234
|
+
# the built-in balanced profile is the compatibility default.
|
|
235
|
+
playbook_cli="$CAIRN_ROOT/mcp-memory-server/dist/playbook-cli.js"
|
|
236
|
+
if [[ ! -f "$playbook_cli" ]]; then
|
|
237
|
+
fail "playbook diagnostics unavailable — rebuild mcp-memory-server"
|
|
238
|
+
elif playbook_json=$(node "$playbook_cli" doctor --project "$PWD" --json 2>/dev/null) \
|
|
239
|
+
&& node -e 'const v=JSON.parse(process.argv[1]);if(v.schema_version!==1||v.ok!==true)process.exit(1)' "$playbook_json"; then
|
|
240
|
+
playbook_count=$(node -e 'const v=JSON.parse(process.argv[1]);process.stdout.write(String(v.receipts.count))' "$playbook_json")
|
|
241
|
+
pass "playbook policy and private receipts are valid ($playbook_count receipt(s))"
|
|
242
|
+
else
|
|
243
|
+
fail "playbook policy or receipts failed validation — inspect with: cairn playbook doctor --json"
|
|
244
|
+
fi
|
|
245
|
+
|
|
246
|
+
# 11. Typed metadata overlays and canonical-note transaction state. The internal
|
|
234
247
|
# JSON seam enumerates only contained local databases and the one notes root;
|
|
235
248
|
# raw KV cells and unmarked Markdown bytes are never repair inputs.
|
|
236
249
|
node_cli="$CAIRN_ROOT/mcp-memory-server/dist/node-cli.js"
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { randomBytes } from "node:crypto";
|
|
3
|
+
import { closeSync, constants, existsSync, fstatSync, lstatSync, openSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { dirname, join, resolve } from "node:path";
|
|
5
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
6
|
+
|
|
7
|
+
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
8
|
+
const TEMPLATE = join(ROOT, "templates", "playbook-agent-instructions.md.template");
|
|
9
|
+
const START = "<!-- cairnkeep:playbook:v1:start -->";
|
|
10
|
+
const END = "<!-- cairnkeep:playbook:v1:end -->";
|
|
11
|
+
const MAX_BYTES = 1024 * 1024;
|
|
12
|
+
|
|
13
|
+
function safeRead(path) {
|
|
14
|
+
const info = lstatSync(path);
|
|
15
|
+
if (!info.isFile() || info.isSymbolicLink() || info.size > MAX_BYTES) throw new Error("AGENTS.md is not a safe bounded regular file.");
|
|
16
|
+
let descriptor;
|
|
17
|
+
try {
|
|
18
|
+
descriptor = openSync(path, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
|
|
19
|
+
const opened = fstatSync(descriptor);
|
|
20
|
+
if (!opened.isFile() || opened.dev !== info.dev || opened.ino !== info.ino || opened.size > MAX_BYTES) throw new Error("AGENTS.md changed during validation.");
|
|
21
|
+
return { text: readFileSync(descriptor, "utf8"), mode: info.mode & 0o777 };
|
|
22
|
+
} finally {
|
|
23
|
+
if (descriptor !== undefined) closeSync(descriptor);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function count(text, marker) {
|
|
28
|
+
return text.split(marker).length - 1;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function render(current, block) {
|
|
32
|
+
const starts = count(current, START);
|
|
33
|
+
const ends = count(current, END);
|
|
34
|
+
if (starts === 0 && ends === 0) return `${current.trimEnd()}${current.trim() ? "\n\n" : ""}${block.trim()}\n`;
|
|
35
|
+
if (starts !== 1 || ends !== 1) throw new Error("AGENTS.md contains malformed or duplicate Cairnkeep playbook markers.");
|
|
36
|
+
const start = current.indexOf(START);
|
|
37
|
+
const end = current.indexOf(END, start);
|
|
38
|
+
if (end < start) throw new Error("AGENTS.md contains reversed Cairnkeep playbook markers.");
|
|
39
|
+
return `${current.slice(0, start)}${block.trim()}${current.slice(end + END.length)}`.replace(/\s*$/, "\n");
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function removeBlock(current) {
|
|
43
|
+
const starts = count(current, START);
|
|
44
|
+
const ends = count(current, END);
|
|
45
|
+
if (starts === 0 && ends === 0) return current;
|
|
46
|
+
if (starts !== 1 || ends !== 1) throw new Error("AGENTS.md contains malformed or duplicate Cairnkeep playbook markers.");
|
|
47
|
+
const start = current.indexOf(START);
|
|
48
|
+
const end = current.indexOf(END, start);
|
|
49
|
+
if (end < start) throw new Error("AGENTS.md contains reversed Cairnkeep playbook markers.");
|
|
50
|
+
return `${current.slice(0, start)}${current.slice(end + END.length)}`.replace(/\n{3,}/g, "\n\n").trimEnd();
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function atomicWrite(path, text, mode) {
|
|
54
|
+
const temporary = join(dirname(path), `.AGENTS.md.${process.pid}.${randomBytes(6).toString("hex")}.tmp`);
|
|
55
|
+
try {
|
|
56
|
+
writeFileSync(temporary, text, { encoding: "utf8", mode, flag: "wx" });
|
|
57
|
+
renameSync(temporary, path);
|
|
58
|
+
} finally {
|
|
59
|
+
rmSync(temporary, { force: true });
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function reconcilePlaybookInstructions(projectRoot = process.cwd(), options = {}) {
|
|
64
|
+
const root = resolve(projectRoot);
|
|
65
|
+
const path = join(root, "AGENTS.md");
|
|
66
|
+
if (dirname(path) !== root) throw new Error("AGENTS.md path escapes the project root.");
|
|
67
|
+
const block = readFileSync(TEMPLATE, "utf8");
|
|
68
|
+
const existing = existsSync(path) ? safeRead(path) : { text: "", mode: 0o644 };
|
|
69
|
+
const desired = render(existing.text, block);
|
|
70
|
+
const status = !existsSync(path) ? "created" : desired === existing.text ? "unchanged" : "updated";
|
|
71
|
+
if (!options.check && status !== "unchanged") atomicWrite(path, desired, existing.mode || 0o644);
|
|
72
|
+
return Object.freeze({ schema_version: 1, path, status: options.check && status !== "unchanged" ? `would-${status}` : status });
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function removePlaybookInstructions(projectRoot = process.cwd(), options = {}) {
|
|
76
|
+
const root = resolve(projectRoot);
|
|
77
|
+
const path = join(root, "AGENTS.md");
|
|
78
|
+
if (!existsSync(path)) return Object.freeze({ schema_version: 1, path, status: "unchanged" });
|
|
79
|
+
const existing = safeRead(path);
|
|
80
|
+
const desired = removeBlock(existing.text);
|
|
81
|
+
const status = desired === existing.text ? "unchanged" : "removed";
|
|
82
|
+
if (!options.check && status === "removed") {
|
|
83
|
+
if (desired.trim()) atomicWrite(path, `${desired}\n`, existing.mode || 0o644);
|
|
84
|
+
else rmSync(path);
|
|
85
|
+
}
|
|
86
|
+
return Object.freeze({ schema_version: 1, path, status: options.check && status === "removed" ? "would-remove" : status });
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async function main() {
|
|
90
|
+
const args = process.argv.slice(2);
|
|
91
|
+
const check = args.includes("--check");
|
|
92
|
+
const remove = args.includes("--remove");
|
|
93
|
+
const json = args.includes("--json");
|
|
94
|
+
const values = args.filter((arg) => !["--check", "--remove", "--json"].includes(arg));
|
|
95
|
+
if (values.length > 1) throw new Error("Usage: playbook-instructions.mjs [PROJECT] [--check] [--json]");
|
|
96
|
+
const value = remove
|
|
97
|
+
? removePlaybookInstructions(values[0] ?? process.cwd(), { check })
|
|
98
|
+
: reconcilePlaybookInstructions(values[0] ?? process.cwd(), { check });
|
|
99
|
+
process.stdout.write(`${json ? JSON.stringify(value) : `${value.status}: ${value.path}`}\n`);
|
|
100
|
+
if (check && value.status !== "unchanged") process.exitCode = 1;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (process.argv[1] && pathToFileURL(resolve(process.argv[1])).href === import.meta.url) {
|
|
104
|
+
main().catch((error) => {
|
|
105
|
+
process.stderr.write(`cairn playbook instructions: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
106
|
+
process.exitCode = 2;
|
|
107
|
+
});
|
|
108
|
+
}
|
package/scripts/setup-core.mjs
CHANGED
|
@@ -16,6 +16,7 @@ const COMMON_ASSETS = Object.freeze([
|
|
|
16
16
|
["env.example.template", ".ai/env.example", 0o644],
|
|
17
17
|
["trajectory-redaction.json.template", ".ai/trajectory-redaction.json", 0o644],
|
|
18
18
|
["capabilities.json.template", ".ai/capabilities.json", 0o600],
|
|
19
|
+
["playbooks.json.template", ".ai/playbooks.json", 0o600],
|
|
19
20
|
["agentfs-gitignore.template", ".agentfs/.gitignore", 0o644],
|
|
20
21
|
["planning-config.json.template", ".planning/config.json", 0o644],
|
|
21
22
|
["project-brief.md.template", ".planning/PROJECT-BRIEF.md", 0o644],
|
package/scripts/setup.mjs
CHANGED
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
} from "./setup-core.mjs";
|
|
23
23
|
import { reconcileSetupPlan } from "./setup-reconcile.mjs";
|
|
24
24
|
import { HARNESS_IDS, machineSyncCommand, requiredHarnessAssetPaths } from "./harness-registry.mjs";
|
|
25
|
+
import { reconcilePlaybookInstructions } from "./playbook-instructions.mjs";
|
|
25
26
|
|
|
26
27
|
const HARNESSES = HARNESS_IDS;
|
|
27
28
|
const GIT_MODES = Object.freeze(["init", "existing", "none"]);
|
|
@@ -33,6 +34,7 @@ const COMMON_SETUP_ASSETS = Object.freeze([
|
|
|
33
34
|
".ai/env.example",
|
|
34
35
|
".ai/trajectory-redaction.json",
|
|
35
36
|
".ai/capabilities.json",
|
|
37
|
+
".ai/playbooks.json",
|
|
36
38
|
".agentfs/.gitignore",
|
|
37
39
|
".planning/config.json",
|
|
38
40
|
".planning/PROJECT-BRIEF.md",
|
|
@@ -186,6 +188,11 @@ async function executeSetup(args, options) {
|
|
|
186
188
|
});
|
|
187
189
|
let plan = buildSetupPlan({ target, preflight, choices });
|
|
188
190
|
if (options.augmentPlan) plan = await options.augmentPlan(plan);
|
|
191
|
+
try {
|
|
192
|
+
reconcilePlaybookInstructions(plan.target, { check: true });
|
|
193
|
+
} catch (error) {
|
|
194
|
+
throw operational("unsafe-instructions", `Project AGENTS.md cannot be reconciled safely: ${error instanceof Error ? error.message : String(error)}`);
|
|
195
|
+
}
|
|
189
196
|
|
|
190
197
|
if (interactive && !parsed.confirmed) {
|
|
191
198
|
renderPlan(plan, options.output);
|
|
@@ -200,6 +207,7 @@ async function executeSetup(args, options) {
|
|
|
200
207
|
createTarget(plan.target);
|
|
201
208
|
if (plan.git === "init") initializeGit(plan.target);
|
|
202
209
|
const reconciliation = await reconcileSetupPlan(plan);
|
|
210
|
+
reconcilePlaybookInstructions(plan.target);
|
|
203
211
|
return resultFor(plan, reconciliation, options.platform);
|
|
204
212
|
}
|
|
205
213
|
|
|
@@ -5,7 +5,7 @@ usage() {
|
|
|
5
5
|
cat <<'EOF'
|
|
6
6
|
Usage: sync-kimi-assets.sh [--check|--apply] [--live-root PATH]
|
|
7
7
|
|
|
8
|
-
Compare or sync Cairnkeep's thin
|
|
8
|
+
Compare or sync Cairnkeep's thin managed Skills against the live Kimi Code tree.
|
|
9
9
|
|
|
10
10
|
Options:
|
|
11
11
|
--check Verify managed live assets match the repo copies (default)
|
|
@@ -19,7 +19,7 @@ ROOT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
|
|
|
19
19
|
SOURCE_ROOT="$ROOT_DIR/kimi"
|
|
20
20
|
LIVE_ROOT="${KIMI_CODE_HOME:-$HOME/.kimi-code}"
|
|
21
21
|
MODE="check"
|
|
22
|
-
ASSETS=("skills/graphify/SKILL.md")
|
|
22
|
+
ASSETS=("skills/graphify/SKILL.md" "skills/cairn-work/SKILL.md")
|
|
23
23
|
|
|
24
24
|
while [[ $# -gt 0 ]]; do
|
|
25
25
|
case "$1" in
|
|
@@ -74,4 +74,4 @@ if [[ "$MODE" == "apply" ]]; then
|
|
|
74
74
|
fi
|
|
75
75
|
|
|
76
76
|
check_assets
|
|
77
|
-
printf 'Kimi
|
|
77
|
+
printf 'Kimi managed Skills are in sync under %s\n' "$LIVE_ROOT"
|
|
@@ -16,7 +16,7 @@ Options:
|
|
|
16
16
|
|
|
17
17
|
Notes:
|
|
18
18
|
- The repo-managed source of truth lives under ./opencode/
|
|
19
|
-
- This script manages the memory-sync / memory-review / code-review commands,
|
|
19
|
+
- This script manages the memory-sync / memory-review / code-review / cairn-work commands,
|
|
20
20
|
the code-reviewer agent, and the memory-review workflow. The memory-wakeup
|
|
21
21
|
plugin is handled by sync-opencode-plugin-assets.sh.
|
|
22
22
|
EOF
|
|
@@ -33,6 +33,7 @@ ASSETS=(
|
|
|
33
33
|
"command/code-review.md"
|
|
34
34
|
"command/remember.md"
|
|
35
35
|
"command/recall.md"
|
|
36
|
+
"command/cairn-work.md"
|
|
36
37
|
"agents/code-reviewer.md"
|
|
37
38
|
"workflows/memory-review-workflow.md"
|
|
38
39
|
)
|
|
@@ -24,6 +24,7 @@ ASSETS=(
|
|
|
24
24
|
"extensions/cairnkeep-memory.ts"
|
|
25
25
|
"extensions/cairnkeep-trajectory.ts"
|
|
26
26
|
"prompts/graphify.md"
|
|
27
|
+
"prompts/cairn-work.md"
|
|
27
28
|
)
|
|
28
29
|
|
|
29
30
|
while [[ $# -gt 0 ]]; do
|
|
@@ -105,4 +106,4 @@ if ! check_assets; then
|
|
|
105
106
|
echo "Recovery: sync-pi-assets.sh --apply --live-root PATH" >&2
|
|
106
107
|
exit 1
|
|
107
108
|
fi
|
|
108
|
-
printf 'Pi
|
|
109
|
+
printf 'Pi extensions and managed prompts are in sync under %s\n' "$LIVE_ROOT"
|
|
@@ -16,6 +16,7 @@ tmp=$(mktemp -d); trap 'rm -rf "$tmp"' EXIT
|
|
|
16
16
|
"$cairn" help | grep -q "cairn notes" || fail "help missing notes"
|
|
17
17
|
"$cairn" help | grep -q "cairn artifact <list|show|delete|prune>" || fail "help missing artifact"
|
|
18
18
|
"$cairn" help | grep -q "cairn evidence <list|show|delete|prune|doctor>" || fail "help missing evidence"
|
|
19
|
+
"$cairn" help | grep -q "cairn playbook <list|status|init|set|enable|disable|reset|check|record|receipts|instructions|doctor>" || fail "help missing playbook"
|
|
19
20
|
"$cairn" help | grep -q "cairn capabilities <list|status|enable|disable|reset|logging>" || fail "help missing capabilities"
|
|
20
21
|
"$cairn" help | grep -q "cairn mcp-tools <list|status|set|reset>" || fail "help missing mcp-tools"
|
|
21
22
|
"$cairn" help | grep -q "cairn pack <init|lock|validate|install|list|show|remove|enable|disable|update|skills|approve-skill|revoke-skill>" || fail "help missing pack"
|
|
@@ -26,6 +27,7 @@ tmp=$(mktemp -d); trap 'rm -rf "$tmp"' EXIT
|
|
|
26
27
|
env -u CAIRN_NOTE_DISTILLATION "$cairn" notes --help >/dev/null 2>&1 || fail "cairn notes dispatch"
|
|
27
28
|
"$cairn" artifact --help | grep -q "cairn artifact list" || fail "cairn artifact dispatch"
|
|
28
29
|
"$cairn" evidence --help | grep -q "cairn evidence list" || fail "cairn evidence dispatch"
|
|
30
|
+
"$cairn" playbook --help | grep -q "cairn playbook list" || fail "cairn playbook dispatch"
|
|
29
31
|
"$cairn" capabilities --help >"$tmp/capabilities-help" 2>/dev/null || fail "cairn capabilities dispatch"
|
|
30
32
|
"$cairn" mcp-tools list --json >"$tmp/mcp-tools.json" || fail "cairn mcp-tools dispatch"
|
|
31
33
|
CAIRN_PACK_BASE_DIR="$tmp/pack-store" "$cairn" pack list --json >"$tmp/packs.json" || fail "cairn pack dispatch"
|
|
@@ -22,7 +22,7 @@ for shell in bash zsh fish; do
|
|
|
22
22
|
for capability_id in $capability_ids; do
|
|
23
23
|
grep -qF "$capability_id" "$tmp/$shell"
|
|
24
24
|
done
|
|
25
|
-
if grep -Eq '(^|[[:space:]"'"'"'])
|
|
25
|
+
if grep -Eq '(^|[[:space:]"'"'"'])guard([[:space:]"'"'"']|$)' "$tmp/$shell"; then
|
|
26
26
|
echo "completion exposed a private capability operation for $shell" >&2
|
|
27
27
|
exit 1
|
|
28
28
|
fi
|
|
@@ -34,7 +34,7 @@ for shell in bash zsh fish; do
|
|
|
34
34
|
for capability_id in $capability_ids; do
|
|
35
35
|
grep -qF "$capability_id" "$tmp/$shell"
|
|
36
36
|
done
|
|
37
|
-
if grep -Eq '(^|[[:space:]"'"'"'])(doctor-diagnosis|guard
|
|
37
|
+
if grep -Eq '(^|[[:space:]"'"'"'])(doctor-diagnosis|guard)([[:space:]"'"'"']|$)' "$tmp/$shell"; then
|
|
38
38
|
echo "completion exposed a private eval lifecycle operation for $shell" >&2
|
|
39
39
|
exit 1
|
|
40
40
|
fi
|
|
@@ -51,6 +51,14 @@ for shell in bash zsh fish; do
|
|
|
51
51
|
grep -q 'evidence' "$tmp/$shell"
|
|
52
52
|
grep -q 'list.*show.*delete.*prune.*doctor\|list show delete prune doctor' "$tmp/$shell"
|
|
53
53
|
grep -q -- '--status\|-l status' "$tmp/$shell"
|
|
54
|
+
grep -q 'playbook' "$tmp/$shell"
|
|
55
|
+
grep -q 'list.*status.*init.*set.*enable.*disable.*reset.*check.*record.*receipts.*instructions.*doctor\|list status init set enable disable reset check record receipts instructions doctor' "$tmp/$shell"
|
|
56
|
+
for playbook_value in minimal balanced strict context.recall verify.tests review.security learning.capture; do
|
|
57
|
+
grep -qF "$playbook_value" "$tmp/$shell"
|
|
58
|
+
done
|
|
59
|
+
for playbook_flag in --project --json --enforce --changed --complexity --familiarity --risk --public-change --completed --skipped --failed; do
|
|
60
|
+
grep -q -- "$playbook_flag\|-l ${playbook_flag#--}" "$tmp/$shell"
|
|
61
|
+
done
|
|
54
62
|
done
|
|
55
63
|
|
|
56
64
|
"$ROOT/bin/cairn" help >"$tmp/root-help"
|
|
@@ -68,6 +76,7 @@ if grep -q 'doctor-diagnosis' "$tmp/eval-help"; then
|
|
|
68
76
|
fi
|
|
69
77
|
grep -qF 'cairn skill <harvest|list|show|review|propose|evaluate|apply|rollback>' "$tmp/root-help"
|
|
70
78
|
grep -qF 'cairn evidence <list|show|delete|prune|doctor>' "$tmp/root-help"
|
|
79
|
+
grep -qF 'cairn playbook <list|status|init|set|enable|disable|reset|check|record|receipts|instructions|doctor>' "$tmp/root-help"
|
|
71
80
|
node "$ROOT/mcp-memory-server/dist/skill-cli.js" --help >"$tmp/skill-help"
|
|
72
81
|
for command in $skill_commands; do
|
|
73
82
|
grep -q "cairn skill $command" "$tmp/skill-help"
|
|
@@ -9,9 +9,13 @@ fail() { echo "FAIL: $1" >&2; exit 1; }
|
|
|
9
9
|
|
|
10
10
|
PI_SOURCE="$ROOT/pi/prompts/graphify.md"
|
|
11
11
|
KIMI_SOURCE="$ROOT/kimi/skills/graphify/SKILL.md"
|
|
12
|
+
PI_PLAYBOOK_SOURCE="$ROOT/pi/prompts/cairn-work.md"
|
|
13
|
+
KIMI_PLAYBOOK_SOURCE="$ROOT/kimi/skills/cairn-work/SKILL.md"
|
|
12
14
|
|
|
13
15
|
[[ -f "$PI_SOURCE" ]] || fail "Pi /graphify prompt source is missing"
|
|
14
16
|
[[ -f "$KIMI_SOURCE" ]] || fail "Kimi graphify skill source is missing"
|
|
17
|
+
[[ -f "$PI_PLAYBOOK_SOURCE" ]] || fail "Pi cairn-work prompt source is missing"
|
|
18
|
+
[[ -f "$KIMI_PLAYBOOK_SOURCE" ]] || fail "Kimi cairn-work Skill source is missing"
|
|
15
19
|
|
|
16
20
|
for adapter in "$PI_SOURCE" "$KIMI_SOURCE"; do
|
|
17
21
|
grep -qF 'cairn graph' "$adapter" || fail "adapter does not delegate to cairn graph: $adapter"
|
|
@@ -31,16 +35,18 @@ PI_LIVE="$tmp/pi"
|
|
|
31
35
|
"$ROOT/scripts/sync-pi-assets.sh" --apply --live-root "$PI_LIVE" >"$tmp/pi-first"
|
|
32
36
|
[[ -f "$PI_LIVE/extensions/cairnkeep-trajectory.ts" ]] || fail "Pi sync omitted trajectory extension"
|
|
33
37
|
cmp -s "$PI_SOURCE" "$PI_LIVE/prompts/graphify.md" || fail "Pi sync changed graphify prompt bytes"
|
|
38
|
+
cmp -s "$PI_PLAYBOOK_SOURCE" "$PI_LIVE/prompts/cairn-work.md" || fail "Pi sync changed cairn-work prompt bytes"
|
|
34
39
|
"$ROOT/scripts/sync-pi-assets.sh" --apply --live-root "$PI_LIVE" >"$tmp/pi-second"
|
|
35
|
-
grep -qF 'Applied 0 Pi asset(s);
|
|
40
|
+
grep -qF 'Applied 0 Pi asset(s); 3 already matched.' "$tmp/pi-second" || fail "Pi sync is not idempotent"
|
|
36
41
|
|
|
37
42
|
KIMI_LIVE="$tmp/kimi"
|
|
38
43
|
"$ROOT/scripts/sync-kimi-assets.sh" --apply --live-root "$KIMI_LIVE" >"$tmp/kimi-first"
|
|
39
44
|
cmp -s "$KIMI_SOURCE" "$KIMI_LIVE/skills/graphify/SKILL.md" || fail "Kimi sync changed graphify skill bytes"
|
|
45
|
+
cmp -s "$KIMI_PLAYBOOK_SOURCE" "$KIMI_LIVE/skills/cairn-work/SKILL.md" || fail "Kimi sync changed cairn-work Skill bytes"
|
|
40
46
|
"$ROOT/scripts/sync-kimi-assets.sh" --apply --live-root "$KIMI_LIVE" >"$tmp/kimi-second"
|
|
41
|
-
grep -qF 'Applied 0 Kimi asset(s);
|
|
47
|
+
grep -qF 'Applied 0 Kimi asset(s); 2 already matched.' "$tmp/kimi-second" || fail "Kimi sync is not idempotent"
|
|
42
48
|
|
|
43
49
|
"$ROOT/bin/cairn" help | grep -qF 'cairn sync-kimi' || fail "CLI help omits sync-kimi"
|
|
44
50
|
"$ROOT/bin/cairn" sync-kimi --help >/dev/null || fail "CLI does not dispatch sync-kimi"
|
|
45
51
|
|
|
46
|
-
echo "PASS: thin Pi and Kimi graph adapters
|
|
52
|
+
echo "PASS: thin Pi and Kimi graph/playbook adapters are bounded and idempotent"
|
|
@@ -13,6 +13,17 @@ import { spawnSync } from "node:child_process";
|
|
|
13
13
|
import { pathToFileURL } from "node:url";
|
|
14
14
|
|
|
15
15
|
const [root, sandbox] = process.argv.slice(2);
|
|
16
|
+
for (const path of [
|
|
17
|
+
"claude/commands/cairn-work.md",
|
|
18
|
+
"opencode/command/cairn-work.md",
|
|
19
|
+
"pi/prompts/cairn-work.md",
|
|
20
|
+
"kimi/skills/cairn-work/SKILL.md",
|
|
21
|
+
]) {
|
|
22
|
+
const content = readFileSync(join(root, path), "utf8");
|
|
23
|
+
assert.match(content, /cairn playbook check/);
|
|
24
|
+
assert.match(content, /must/);
|
|
25
|
+
assert.match(content, /approval/);
|
|
26
|
+
}
|
|
16
27
|
const registry = await import(pathToFileURL(join(root, "scripts", "harness-registry.mjs")).href);
|
|
17
28
|
assert.deepEqual(registry.HARNESS_IDS, ["claude", "opencode", "pi", "kimi", "qwen", "codex"]);
|
|
18
29
|
assert.equal(new Set(registry.HARNESS_IDS).size, registry.HARNESS_IDS.length);
|
|
@@ -22,7 +22,7 @@ done
|
|
|
22
22
|
|
|
23
23
|
ready=0
|
|
24
24
|
brief=0
|
|
25
|
-
for number in $(seq -w 0
|
|
25
|
+
for number in $(seq -w 0 25); do
|
|
26
26
|
matches=(docs/learning/lessons/L"$number"-*.md)
|
|
27
27
|
[[ ${#matches[@]} -eq 1 && -f ${matches[0]} ]] || {
|
|
28
28
|
echo "learning path must contain exactly one L$number lesson" >&2
|
|
@@ -53,7 +53,7 @@ grep -qF '`cairn sync-kimi --apply`' "$graph_lesson"
|
|
|
53
53
|
grep -qF '`cairn sync-pi --apply`' "$graph_lesson"
|
|
54
54
|
grep -qF '`graphify-out/`' "$graph_lesson"
|
|
55
55
|
|
|
56
|
-
[[ $ready -eq
|
|
56
|
+
[[ $ready -eq 11 && $brief -eq 15 ]] || {
|
|
57
57
|
echo "unexpected learning status totals: ready=$ready brief=$brief" >&2
|
|
58
58
|
exit 1
|
|
59
59
|
}
|
|
@@ -143,4 +143,25 @@ grep -Eqi 'cancel|shutdown|child process' "$l23"
|
|
|
143
143
|
grep -Eqi '0\.84\.1' "$l23"
|
|
144
144
|
grep -Eqi 'credentials|secrets|private state' "$l23"
|
|
145
145
|
|
|
146
|
+
l25=docs/learning/lessons/L25-playbooks.md
|
|
147
|
+
video25=docs/learning/video-scripts/V25-playbooks.md
|
|
148
|
+
for file in "$l25" "$video25"; do
|
|
149
|
+
[[ -f "$file" ]]
|
|
150
|
+
done
|
|
151
|
+
for phrase in \
|
|
152
|
+
'cairn playbook check start' \
|
|
153
|
+
'cairn playbook check finish' \
|
|
154
|
+
'--enforce' \
|
|
155
|
+
'cairn playbook record' \
|
|
156
|
+
'unauthenticated' \
|
|
157
|
+
'does not run'; do
|
|
158
|
+
grep -qF -- "$phrase" "$l25" || {
|
|
159
|
+
echo "L25 missing playbook teaching boundary: $phrase" >&2
|
|
160
|
+
exit 1
|
|
161
|
+
}
|
|
162
|
+
done
|
|
163
|
+
grep -qF 'course-13-playbooks' "$coverage"
|
|
164
|
+
grep -qF 'L25-playbooks.md' docs/learning/tracks/practitioner.md
|
|
165
|
+
grep -qF 'L25-playbooks.md' docs/learning/tracks/operator.md
|
|
166
|
+
|
|
146
167
|
echo "PASS: public learning path structure, readiness, links, and version alignment"
|
|
@@ -48,6 +48,8 @@ bootstrap_output=$(cairn bootstrap "$tmp/project")
|
|
|
48
48
|
[[ -x "$tmp/project/.ai/start-qwen.sh" ]] || fail "bootstrap did not install an executable Qwen launcher"
|
|
49
49
|
[[ -x "$tmp/project/.ai/start-codex.sh" ]] || fail "bootstrap did not install an executable Codex launcher"
|
|
50
50
|
[[ -f "$tmp/project/.planning/config.json" ]] || fail "bootstrap did not install the planning scaffold"
|
|
51
|
+
[[ -f "$tmp/project/.ai/playbooks.json" ]] || fail "bootstrap did not install the playbook policy"
|
|
52
|
+
grep -qF '<!-- cairnkeep:playbook:v1:start -->' "$tmp/project/AGENTS.md" || fail "bootstrap did not install playbook agent guidance"
|
|
51
53
|
|
|
52
54
|
(cd "$tmp/project" && cairn doctor) >/dev/null || fail "installed package failed cairn doctor"
|
|
53
55
|
env -u CAIRN_NOTE_DISTILLATION cairn notes --help >/dev/null \
|
|
@@ -104,6 +106,30 @@ CAIRN_PACK_BASE_DIR="$tmp/installed-packs" cairn pack list --json >"$tmp/install
|
|
|
104
106
|
cairn mcp-tools list --json >"$tmp/installed-mcp-tools.json" || \
|
|
105
107
|
fail "installed package mcp-tools CLI failed"
|
|
106
108
|
|
|
109
|
+
for required in \
|
|
110
|
+
schemas/playbook.schema.json \
|
|
111
|
+
templates/playbooks.json.template \
|
|
112
|
+
templates/playbook-agent-instructions.md.template \
|
|
113
|
+
scripts/playbook-instructions.mjs \
|
|
114
|
+
mcp-memory-server/dist/playbook-schema.js \
|
|
115
|
+
mcp-memory-server/dist/playbook.js \
|
|
116
|
+
mcp-memory-server/dist/playbook-receipt.js \
|
|
117
|
+
mcp-memory-server/dist/playbook-cli.js \
|
|
118
|
+
claude/commands/cairn-work.md \
|
|
119
|
+
opencode/command/cairn-work.md \
|
|
120
|
+
pi/prompts/cairn-work.md \
|
|
121
|
+
kimi/skills/cairn-work/SKILL.md \
|
|
122
|
+
docs/design/team-mode.md \
|
|
123
|
+
docs/learning/lessons/L25-playbooks.md \
|
|
124
|
+
docs/learning/video-scripts/V25-playbooks.md
|
|
125
|
+
do
|
|
126
|
+
[[ -f "$installed_root/$required" ]] || fail "npm tarball omitted $required"
|
|
127
|
+
done
|
|
128
|
+
cairn playbook status --project "$tmp/project" --json >"$tmp/installed-playbook-status.json" || \
|
|
129
|
+
fail "installed package playbook status failed"
|
|
130
|
+
cairn playbook check start --project "$tmp/project" --session installed-playbook --json >"$tmp/installed-playbook-check.json" || \
|
|
131
|
+
fail "installed package playbook check failed"
|
|
132
|
+
|
|
107
133
|
for required in \
|
|
108
134
|
schemas/skill-adapter.schema.json \
|
|
109
135
|
schemas/skill-proposal-protocol.schema.json \
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
ROOT=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
|
|
5
|
+
tmp=$(mktemp -d)
|
|
6
|
+
trap 'rm -rf "$tmp"' EXIT
|
|
7
|
+
project="$tmp/project"
|
|
8
|
+
mkdir -p "$project"
|
|
9
|
+
printf '# User rules\n\nKeep this line.\n' >"$project/AGENTS.md"
|
|
10
|
+
chmod 0640 "$project/AGENTS.md"
|
|
11
|
+
|
|
12
|
+
node "$ROOT/scripts/playbook-instructions.mjs" "$project" >/dev/null
|
|
13
|
+
grep -qF 'Keep this line.' "$project/AGENTS.md"
|
|
14
|
+
[[ $(grep -cF '<!-- cairnkeep:playbook:v1:start -->' "$project/AGENTS.md") -eq 1 ]]
|
|
15
|
+
[[ $(stat -c '%a' "$project/AGENTS.md") == 640 ]]
|
|
16
|
+
before=$(sha256sum "$project/AGENTS.md" | cut -d' ' -f1)
|
|
17
|
+
node "$ROOT/scripts/playbook-instructions.mjs" "$project" >/dev/null
|
|
18
|
+
after=$(sha256sum "$project/AGENTS.md" | cut -d' ' -f1)
|
|
19
|
+
[[ "$before" == "$after" ]]
|
|
20
|
+
"$ROOT/bin/cairn" playbook instructions check --project "$project" --json >/dev/null
|
|
21
|
+
|
|
22
|
+
node "$ROOT/scripts/playbook-instructions.mjs" "$project" --remove >/dev/null
|
|
23
|
+
grep -qF 'Keep this line.' "$project/AGENTS.md"
|
|
24
|
+
! grep -qF 'cairnkeep:playbook' "$project/AGENTS.md"
|
|
25
|
+
|
|
26
|
+
printf '%s\n%s\n%s\n' \
|
|
27
|
+
'<!-- cairnkeep:playbook:v1:start -->' \
|
|
28
|
+
'<!-- cairnkeep:playbook:v1:start -->' \
|
|
29
|
+
'<!-- cairnkeep:playbook:v1:end -->' >"$project/AGENTS.md"
|
|
30
|
+
if node "$ROOT/scripts/playbook-instructions.mjs" "$project" >/dev/null 2>&1; then
|
|
31
|
+
echo 'duplicate managed markers were accepted' >&2
|
|
32
|
+
exit 1
|
|
33
|
+
fi
|
|
34
|
+
|
|
35
|
+
rm "$project/AGENTS.md"
|
|
36
|
+
printf 'neighbor\n' >"$tmp/neighbor"
|
|
37
|
+
ln -s "$tmp/neighbor" "$project/AGENTS.md"
|
|
38
|
+
if node "$ROOT/scripts/playbook-instructions.mjs" "$project" >/dev/null 2>&1; then
|
|
39
|
+
echo 'symlink AGENTS.md was accepted' >&2
|
|
40
|
+
exit 1
|
|
41
|
+
fi
|
|
42
|
+
[[ $(cat "$tmp/neighbor") == neighbor ]]
|
|
43
|
+
|
|
44
|
+
for command in bootstrap setup; do
|
|
45
|
+
unsafe="$tmp/unsafe-$command"
|
|
46
|
+
mkdir -p "$unsafe"
|
|
47
|
+
printf '%s\n%s\n' '<!-- cairnkeep:playbook:v1:start -->' '<!-- cairnkeep:playbook:v1:start -->' >"$unsafe/AGENTS.md"
|
|
48
|
+
if [[ "$command" == bootstrap ]]; then
|
|
49
|
+
if "$ROOT/bin/cairn" bootstrap "$unsafe" >/dev/null 2>&1; then
|
|
50
|
+
echo 'bootstrap accepted malformed managed markers' >&2
|
|
51
|
+
exit 1
|
|
52
|
+
fi
|
|
53
|
+
elif "$ROOT/bin/cairn" setup "$unsafe" --git init --harness codex --memory local --yes >/dev/null 2>&1; then
|
|
54
|
+
echo 'setup accepted malformed managed markers' >&2
|
|
55
|
+
exit 1
|
|
56
|
+
fi
|
|
57
|
+
[[ ! -e "$unsafe/.ai" ]]
|
|
58
|
+
done
|
|
59
|
+
|
|
60
|
+
echo 'PASS: ownership-safe playbook AGENTS.md reconciliation'
|
|
@@ -28,6 +28,7 @@ try {
|
|
|
28
28
|
process.env.USERPROFILE = sandbox;
|
|
29
29
|
const project = join(sandbox, "Project with spaces – Unicode");
|
|
30
30
|
mkdirSync(project, { recursive: true });
|
|
31
|
+
writeFileSync(join(project, "AGENTS.md"), "# User rules\n\nKeep this line.\n");
|
|
31
32
|
bootstrapWindows(root, [project]);
|
|
32
33
|
for (const harness of HARNESS_IDS) {
|
|
33
34
|
assert.ok(existsSync(join(project, ".ai", `start-${harness}.cmd`)), `${harness} cmd launcher`);
|
|
@@ -38,6 +39,8 @@ try {
|
|
|
38
39
|
assert.match(launcherPowerShell(), /post-exit\.ps1/);
|
|
39
40
|
assert.match(launcherPowerShell(), /evidence run --harness/);
|
|
40
41
|
assert.match(launcherPowerShell(), /CAIRN_WORK_EVIDENCE/);
|
|
42
|
+
assert.match(readFileSync(join(project, "AGENTS.md"), "utf8"), /cairnkeep:playbook:v1:start/);
|
|
43
|
+
assert.match(readFileSync(join(project, "AGENTS.md"), "utf8"), /Keep this line\./);
|
|
41
44
|
|
|
42
45
|
const claudeRoot = join(sandbox, "Claude Config");
|
|
43
46
|
await runWindowsCommand({ command: "sync", args: ["--apply", "--live-root", claudeRoot], root });
|
|
@@ -59,6 +62,9 @@ try {
|
|
|
59
62
|
assert.equal(readFileSync(join(memory, "project.db"), "utf8"), "sqlite-a");
|
|
60
63
|
assert.match(powershellCompletion(), /Register-ArgumentCompleter/);
|
|
61
64
|
assert.match(powershellCompletion(), /evidence/);
|
|
65
|
+
assert.match(powershellCompletion(), /playbook/);
|
|
66
|
+
assert.match(powershellCompletion(), /review\.security/);
|
|
67
|
+
assert.match(powershellCompletion(), /--enforce/);
|
|
62
68
|
|
|
63
69
|
let containerArgs = [];
|
|
64
70
|
runNativeContainer(["stdio", "--image", "example/windows:1", "--volume", "windows-data"], root, (_engine, args) => { containerArgs = args; });
|
|
@@ -68,6 +74,7 @@ try {
|
|
|
68
74
|
|
|
69
75
|
await runWindowsCommand({ command: "uninstall", args: ["--yes", "--live-root", claudeRoot, project], root });
|
|
70
76
|
assert.ok(!existsSync(join(project, ".ai", "start-claude.cmd")));
|
|
77
|
+
assert.equal(readFileSync(join(project, "AGENTS.md"), "utf8"), "# User rules\n\nKeep this line.\n");
|
|
71
78
|
assert.ok(!existsSync(join(claudeRoot, "commands", "remember.md")));
|
|
72
79
|
const cleanedSettings = JSON.parse(readFileSync(join(claudeRoot, "settings.json"), "utf8"));
|
|
73
80
|
assert.ok(!JSON.stringify(cleanedSettings).includes("memory-wakeup"));
|
package/scripts/uninstall.sh
CHANGED
|
@@ -49,6 +49,7 @@ PROJECT_AI_FILES=(
|
|
|
49
49
|
env.example
|
|
50
50
|
trajectory-redaction.json
|
|
51
51
|
capabilities.json
|
|
52
|
+
playbooks.json
|
|
52
53
|
)
|
|
53
54
|
|
|
54
55
|
usage() {
|
|
@@ -150,7 +151,7 @@ if (!exact(state, ["schema_version", "cairnkeep_version", "git", "memory", "harn
|
|
|
150
151
|
|| typeof state.assets !== "object"
|
|
151
152
|
|| Array.isArray(state.assets)) process.exit(1);
|
|
152
153
|
const allowed = new Set([
|
|
153
|
-
".ai/env.example", ".ai/trajectory-redaction.json", ".ai/capabilities.json",
|
|
154
|
+
".ai/env.example", ".ai/trajectory-redaction.json", ".ai/capabilities.json", ".ai/playbooks.json",
|
|
154
155
|
".agentfs/.gitignore", ".planning/config.json", ".planning/PROJECT-BRIEF.md",
|
|
155
156
|
".planning/wiki/index.md", ".planning/wiki/policy.md", ".planning/wiki/CONTRADICTIONS.md",
|
|
156
157
|
".planning/wiki/LOG.md", ".planning/alignment/policy.md", ".planning/alignment/gap-register.yaml",
|
|
@@ -213,8 +214,10 @@ echo "Pi adapters:"
|
|
|
213
214
|
remove_path "$PI_LIVE_ROOT/extensions/cairnkeep-memory.ts"
|
|
214
215
|
remove_path "$PI_LIVE_ROOT/extensions/cairnkeep-trajectory.ts"
|
|
215
216
|
remove_path "$PI_LIVE_ROOT/prompts/graphify.md"
|
|
217
|
+
remove_path "$PI_LIVE_ROOT/prompts/cairn-work.md"
|
|
216
218
|
echo "Kimi adapter:"
|
|
217
219
|
remove_path "$KIMI_LIVE_ROOT/skills/graphify/SKILL.md"
|
|
220
|
+
remove_path "$KIMI_LIVE_ROOT/skills/cairn-work/SKILL.md"
|
|
218
221
|
|
|
219
222
|
# 2. settings.json hook registrations (back up whole file, then de-register).
|
|
220
223
|
SETTINGS="$LIVE_ROOT/settings.json"
|
|
@@ -310,6 +313,18 @@ for proj in "${PROJECTS[@]:-}"; do
|
|
|
310
313
|
[[ -n "$proj" && -d "$proj" ]] || continue
|
|
311
314
|
proj=$(cd "$proj" && pwd)
|
|
312
315
|
echo "Project scaffold: $proj"
|
|
316
|
+
if [[ -f "$proj/AGENTS.md" && ! -L "$proj/AGENTS.md" ]] && grep -qF '<!-- cairnkeep:playbook:v1:start -->' "$proj/AGENTS.md" 2>/dev/null; then
|
|
317
|
+
if [[ $DRY_RUN -eq 1 ]]; then
|
|
318
|
+
echo " would remove Cairnkeep playbook block from $proj/AGENTS.md"
|
|
319
|
+
else
|
|
320
|
+
back_up "$proj/AGENTS.md"
|
|
321
|
+
if node "$CAIRN_ROOT/scripts/playbook-instructions.mjs" "$proj" --remove >/dev/null; then
|
|
322
|
+
echo " removed Cairnkeep playbook block from $proj/AGENTS.md"
|
|
323
|
+
else
|
|
324
|
+
echo " kept AGENTS.md: playbook markers could not be removed safely"
|
|
325
|
+
fi
|
|
326
|
+
fi
|
|
327
|
+
fi
|
|
313
328
|
# Truthy launchers create this project-local root only for the explicit
|
|
314
329
|
# capability overlay. Back up the complete managed unit so its native Claude
|
|
315
330
|
# hook registrations and OpenCode plugin bytes restore together exactly.
|