@cairnkeep/cli 2.14.0 → 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 +30 -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 +383 -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
|
@@ -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.
|
|
@@ -21,11 +21,13 @@ import { spawnSync } from "node:child_process";
|
|
|
21
21
|
import { basename, dirname, isAbsolute, join, resolve, sep } from "node:path";
|
|
22
22
|
import { homedir, tmpdir } from "node:os";
|
|
23
23
|
import { HARNESS_IDS, harnessProjectAssets } from "./harness-registry.mjs";
|
|
24
|
+
import { reconcilePlaybookInstructions, removePlaybookInstructions } from "./playbook-instructions.mjs";
|
|
24
25
|
|
|
25
26
|
const BOOTSTRAP_FILES = [
|
|
26
27
|
["env.example.template", ".ai/env.example"],
|
|
27
28
|
["trajectory-redaction.json.template", ".ai/trajectory-redaction.json"],
|
|
28
29
|
["capabilities.json.template", ".ai/capabilities.json"],
|
|
30
|
+
["playbooks.json.template", ".ai/playbooks.json"],
|
|
29
31
|
["agentfs-gitignore.template", ".agentfs/.gitignore"],
|
|
30
32
|
["planning-config.json.template", ".planning/config.json"],
|
|
31
33
|
["project-brief.md.template", ".planning/PROJECT-BRIEF.md"],
|
|
@@ -257,9 +259,11 @@ export function bootstrapWindows(root, args) {
|
|
|
257
259
|
prefix = git(["rev-parse", "--show-prefix"], target).replaceAll("\\", "/");
|
|
258
260
|
}
|
|
259
261
|
|
|
262
|
+
if (!options.has("--untracked")) reconcilePlaybookInstructions(target, { check: true });
|
|
263
|
+
|
|
260
264
|
const templates = join(root, "templates");
|
|
261
265
|
for (const [source, destination] of BOOTSTRAP_FILES) {
|
|
262
|
-
installFile(join(templates, source), join(target, ...destination.split("/")),
|
|
266
|
+
installFile(join(templates, source), join(target, ...destination.split("/")), /(?:capabilities|playbooks)\.json$/.test(destination) ? 0o600 : 0o644);
|
|
263
267
|
}
|
|
264
268
|
for (const harness of HARNESSES) {
|
|
265
269
|
const asset = harnessProjectAssets(harness, "local")[0];
|
|
@@ -271,6 +275,9 @@ export function bootstrapWindows(root, args) {
|
|
|
271
275
|
if (!existsSync(modulePath)) atomicWrite(modulePath, launcherModule(), 0o755);
|
|
272
276
|
const powershellPath = join(target, ".ai", "start-harness.ps1");
|
|
273
277
|
if (!existsSync(powershellPath)) atomicWrite(powershellPath, launcherPowerShell(), 0o600);
|
|
278
|
+
if (!options.has("--untracked")) {
|
|
279
|
+
reconcilePlaybookInstructions(target);
|
|
280
|
+
}
|
|
274
281
|
|
|
275
282
|
if (excludeFile) {
|
|
276
283
|
mkdirSync(dirname(excludeFile), { recursive: true });
|
|
@@ -456,6 +463,7 @@ async function doctorWindows(root, args) {
|
|
|
456
463
|
["typed memory", "node-cli.js", ["doctor", "--project-root", process.cwd(), ...(repair ? ["--repair"] : [])]],
|
|
457
464
|
["capability", "capability-cli.js", ["doctor", "--json"]],
|
|
458
465
|
["context pack", "context-pack-cli.js", ["doctor", "--json"]],
|
|
466
|
+
["playbook", "playbook-cli.js", ["doctor", "--json"]],
|
|
459
467
|
]) {
|
|
460
468
|
const entry = join(root, "mcp-memory-server", "dist", file);
|
|
461
469
|
if (!existsSync(entry)) { line("FAIL", `${label} diagnostics are unavailable`); continue; }
|
|
@@ -627,9 +635,11 @@ function uninstallWindows(root, args) {
|
|
|
627
635
|
join(piLive, "extensions", "cairnkeep-memory.ts"),
|
|
628
636
|
join(piLive, "extensions", "cairnkeep-trajectory.ts"),
|
|
629
637
|
join(piLive, "prompts", "graphify.md"),
|
|
638
|
+
join(piLive, "prompts", "cairn-work.md"),
|
|
630
639
|
);
|
|
631
640
|
const kimiLive = resolve(expandHome(options.get("--kimi-live-root") || process.env.KIMI_CODE_HOME || join(homedir(), ".kimi-code")));
|
|
632
641
|
targets.push(join(kimiLive, "skills", "graphify", "SKILL.md"));
|
|
642
|
+
targets.push(join(kimiLive, "skills", "cairn-work", "SKILL.md"));
|
|
633
643
|
for (const project of projects) {
|
|
634
644
|
const base = resolve(project);
|
|
635
645
|
for (const [, rel] of BOOTSTRAP_FILES) targets.push(join(base, ...rel.split("/")));
|
|
@@ -677,6 +687,16 @@ function uninstallWindows(root, args) {
|
|
|
677
687
|
console.log(`updated: ${settingsPath}`);
|
|
678
688
|
}
|
|
679
689
|
}
|
|
690
|
+
for (const project of projects) {
|
|
691
|
+
const agents = join(resolve(project), "AGENTS.md");
|
|
692
|
+
if (!existsSync(agents)) continue;
|
|
693
|
+
const info = lstatSync(agents);
|
|
694
|
+
if (!info.isFile() || info.isSymbolicLink()) continue;
|
|
695
|
+
if (!readFileSync(agents, "utf8").includes("<!-- cairnkeep:playbook:v1:start -->")) continue;
|
|
696
|
+
backupTarget(agents);
|
|
697
|
+
removePlaybookInstructions(resolve(project));
|
|
698
|
+
console.log(`removed Cairnkeep playbook block: ${agents}`);
|
|
699
|
+
}
|
|
680
700
|
for (const target of existing) {
|
|
681
701
|
backupTarget(target);
|
|
682
702
|
rmSync(target, { recursive: true, force: true });
|
|
@@ -708,9 +728,10 @@ export function powershellCompletion() {
|
|
|
708
728
|
const harnesses = HARNESS_IDS.map((id) => `'${id}'`).join(",");
|
|
709
729
|
return `Register-ArgumentCompleter -Native -CommandName cairn -ScriptBlock {
|
|
710
730
|
param($wordToComplete, $commandAst, $cursorPosition)
|
|
711
|
-
$commands = 'bootstrap','setup','memory-server','sync','sync-pi','sync-kimi','doctor','trajectory','artifact','evidence','capabilities','mcp-tools','pack','notes','eval','skill','graph','memory','audit-timer','uninstall','completion','version','help'
|
|
731
|
+
$commands = 'bootstrap','setup','memory-server','sync','sync-pi','sync-kimi','doctor','trajectory','artifact','evidence','playbook','capabilities','mcp-tools','pack','notes','eval','skill','graph','memory','audit-timer','uninstall','completion','version','help'
|
|
712
732
|
$setup = '--git','--harness','--memory','--policy','--yes','--json','init','existing','none',${harnesses},'local'
|
|
713
|
-
$
|
|
733
|
+
$playbook = 'list','status','init','set','enable','disable','reset','check','record','receipts','instructions','doctor','minimal','balanced','strict','must','should','may','off','start','finish','install','remove','context.recall','context.explore','work.plan','verify.tests','review.repository','review.security','docs.update','learning.capture','--project','--json','--enforce','--complexity','--familiarity','--risk','--public-change','--changed','--change-type','--completed','--skipped','--failed','--actor','--actor-kind','--session','--policy','--decision','--event','--action','--outcome','--reason'
|
|
734
|
+
$candidates = if ($commandAst.ToString() -match '^\\s*cairn\\s+setup(?:\\s|$)') { $setup } elseif ($commandAst.ToString() -match '^\\s*cairn\\s+playbook(?:\\s|$)') { $playbook } else { $commands }
|
|
714
735
|
$candidates | Where-Object { $_ -like "$wordToComplete*" } | ForEach-Object {
|
|
715
736
|
[System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_)
|
|
716
737
|
}
|