@cairnkeep/cli 2.3.1 → 2.5.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 +85 -0
- package/README.md +232 -6
- package/bin/cairn +43 -2
- package/claude/capability-contract/hooks/capability-command-finish.sh +81 -0
- package/claude/capability-contract/hooks/capability-command-start.sh +76 -0
- package/claude/hooks/compaction-capture.sh +34 -0
- package/claude/hooks/memory-capture.sh +47 -5
- package/claude/hooks/memory-wakeup.sh +55 -2
- package/docs/building-an-overlay.md +4 -3
- package/docs/design/meta-agent.md +439 -0
- package/docs/harness-compatibility.md +117 -0
- package/docs/learning/CURRICULUM-MAP.md +91 -0
- package/docs/learning/FEATURE-GUIDE.md +86 -0
- package/docs/learning/PRODUCTION-PLAN.md +45 -14
- package/docs/learning/README.md +20 -1
- package/docs/learning/instructor-guide.md +6 -0
- package/docs/learning/lessons/L00-why-cairnkeep.md +20 -8
- package/docs/learning/lessons/L01-safe-trial.md +3 -3
- package/docs/learning/lessons/L02-installation.md +23 -5
- package/docs/learning/lessons/L03-first-project.md +7 -2
- package/docs/learning/lessons/L04-memory-fundamentals.md +4 -0
- package/docs/learning/lessons/L05-daily-workflow.md +4 -0
- package/docs/learning/lessons/L07-privacy-and-storage.md +3 -0
- package/docs/learning/lessons/L10-context-exploration.md +4 -2
- package/docs/learning/lessons/L12-managed-overlays.md +2 -0
- package/docs/learning/lessons/L13-session-evidence.md +71 -0
- package/docs/learning/lessons/L14-typed-memory.md +51 -0
- package/docs/learning/lessons/L15-capability-governance.md +56 -0
- package/docs/learning/lessons/L16-evaluation.md +67 -0
- package/docs/learning/lessons/L17-agent-boundary.md +55 -0
- package/docs/learning/templates/lesson-template.md +4 -1
- package/docs/learning/tracks/evidence-and-evaluation.md +20 -0
- package/docs/learning/tracks/operator.md +8 -2
- package/docs/learning/tracks/practitioner.md +6 -1
- package/docs/learning/tracks/quickstart.md +3 -0
- package/docs/learning/video-scripts/L00-why-cairnkeep.md +21 -17
- package/docs/learning/video-scripts/L02-installation.md +9 -2
- package/docs/learning/video-scripts/L03-first-project.md +3 -0
- package/docs/operating.md +567 -7
- package/docs/privacy-and-data-flow.md +417 -0
- package/docs/storage.md +252 -0
- package/examples/eval/README.md +42 -0
- package/examples/eval/adapter.json +12 -0
- package/examples/eval/bundled-fake.json +1 -0
- package/examples/eval/task-set.json +1 -0
- package/mcp-memory-server/dist/artifact-cli.js +184 -0
- package/mcp-memory-server/dist/artifact-schema.js +218 -0
- package/mcp-memory-server/dist/artifact-store.js +812 -0
- package/mcp-memory-server/dist/capability-adapter.js +286 -0
- package/mcp-memory-server/dist/capability-cli.js +361 -0
- package/mcp-memory-server/dist/capability-config.js +310 -0
- package/mcp-memory-server/dist/capability-harness.js +496 -0
- package/mcp-memory-server/dist/capability-registry.js +123 -0
- package/mcp-memory-server/dist/capability-schema.js +65 -0
- package/mcp-memory-server/dist/capability-store.js +431 -0
- package/mcp-memory-server/dist/compaction-normalize.js +267 -0
- package/mcp-memory-server/dist/eval-cli.js +523 -0
- package/mcp-memory-server/dist/eval-plan.js +346 -0
- package/mcp-memory-server/dist/eval-process.js +273 -0
- package/mcp-memory-server/dist/eval-report.js +609 -0
- package/mcp-memory-server/dist/eval-runner.js +827 -0
- package/mcp-memory-server/dist/eval-schema.js +368 -0
- package/mcp-memory-server/dist/eval-statistics.js +161 -0
- package/mcp-memory-server/dist/eval-workspace.js +313 -0
- package/mcp-memory-server/dist/failure-signature.js +141 -0
- package/mcp-memory-server/dist/index.js +600 -55
- package/mcp-memory-server/dist/node-cli.js +84 -0
- package/mcp-memory-server/dist/node-schema.js +170 -0
- package/mcp-memory-server/dist/node-store.js +455 -0
- package/mcp-memory-server/dist/note-cli.js +235 -0
- package/mcp-memory-server/dist/note-distiller.js +177 -0
- package/mcp-memory-server/dist/note-enrichment.js +93 -0
- package/mcp-memory-server/dist/note-schema.js +4 -0
- package/mcp-memory-server/dist/note-store.js +985 -0
- package/mcp-memory-server/dist/trajectory-cli.js +141 -0
- package/mcp-memory-server/dist/trajectory-normalize.js +448 -0
- package/mcp-memory-server/dist/trajectory-redaction.js +155 -0
- package/mcp-memory-server/dist/trajectory-schema.js +73 -0
- package/mcp-memory-server/dist/trajectory-store.js +339 -0
- package/mcp-memory-server/package.json +24 -2
- package/mcp-memory-server/scripts/fixtures/compaction-claude-code-2.1.219.json +9 -0
- package/mcp-memory-server/scripts/fixtures/compaction-claude-code-2.1.220.json +9 -0
- package/mcp-memory-server/scripts/fixtures/compaction-opencode-1.17.20-event.json +6 -0
- package/mcp-memory-server/scripts/fixtures/compaction-opencode-1.17.20-messages.json +119 -0
- package/mcp-memory-server/scripts/fixtures/compaction-unknown-version.json +17 -0
- package/opencode/capability-contract/plugins/capability-command.ts +319 -0
- package/opencode/command/graphify.md +25 -14
- package/opencode/plugins/memory-capture.ts +76 -10
- package/opencode/plugins/memory-wakeup.ts +58 -6
- package/package.json +8 -4
- package/pi/extensions/cairnkeep-trajectory.ts +55 -0
- package/schemas/artifact.schema.json +133 -0
- package/schemas/capability-callback.schema.json +68 -0
- package/schemas/capability-contract.schema.json +126 -0
- package/schemas/eval-adapter.schema.json +43 -0
- package/schemas/eval-protocol.schema.json +114 -0
- package/schemas/eval-report.schema.json +423 -0
- package/schemas/eval-task-set.schema.json +114 -0
- package/schemas/memory-node.schema.json +90 -0
- package/schemas/note.schema.json +112 -0
- package/schemas/trajectory-redaction.schema.json +25 -0
- package/schemas/trajectory.schema.json +82 -0
- package/scripts/bootstrap.sh +6 -0
- package/scripts/completion.sh +119 -5
- package/scripts/doctor.sh +284 -0
- package/scripts/fake-eval-adapter.mjs +120 -0
- package/scripts/fixtures/capability-harness-contracts.json +408 -0
- package/scripts/install-audit-timer.sh +3 -1
- package/scripts/lib/capability-opencode-plugin-harness.mjs +291 -0
- package/scripts/lib/trajectory-opencode-plugin-harness.mjs +25 -0
- package/scripts/lib/trajectory-pi-extension-harness.mjs +42 -0
- package/scripts/memory-wiki-audit.sh +49 -0
- package/scripts/smoke-bash32.sh +9 -0
- package/scripts/sync-claude-assets.sh +99 -6
- package/scripts/sync-opencode-graphify-assets.sh +24 -3
- package/scripts/sync-opencode-plugin-assets.sh +31 -5
- package/scripts/sync-opencode-security-assets.sh +24 -3
- package/scripts/sync-opencode-wiki-assets.sh +24 -3
- package/scripts/sync-pi-assets.sh +72 -0
- package/scripts/test-audit-timer.sh +98 -0
- package/scripts/test-bootstrap-untracked.sh +49 -0
- package/scripts/test-cli-dispatch.sh +41 -2
- package/scripts/test-compaction-hooks.sh +495 -0
- package/scripts/test-completion.sh +77 -0
- package/scripts/test-doctor.sh +348 -1
- package/scripts/test-eval-cli.sh +175 -0
- package/scripts/test-launcher-hooks.sh +630 -3
- package/scripts/test-learning-docs.sh +20 -4
- package/scripts/test-notes-cli.sh +114 -0
- package/scripts/test-package-install.sh +254 -0
- package/scripts/test-phase16-node-lifecycle.sh +50 -0
- package/scripts/test-phase17-artifact-lifecycle.sh +370 -0
- package/scripts/test-phase18-capability-lifecycle.sh +504 -0
- package/scripts/test-phase18-harness-boundary.sh +1060 -0
- package/scripts/test-phase19-eval-lifecycle.sh +1015 -0
- package/scripts/test-trajectory-cli.sh +87 -0
- package/scripts/test-trajectory-hooks.sh +142 -0
- package/scripts/test-uninstall.sh +231 -5
- package/scripts/uninstall.sh +38 -5
- package/scripts/verify-dependency-security.mjs +121 -0
- package/scripts/verify-docs-parity.sh +527 -1
- package/scripts/verify-phase18-runtime-evidence.mjs +895 -0
- package/scripts/verify-phase19-runtime-evidence.mjs +947 -0
- package/templates/capabilities.json.template +7 -0
- package/templates/env.example.template +51 -0
- package/templates/memory-wiki-audit.service.template +1 -1
- package/templates/start-claude.sh.template +36 -0
- package/templates/start-kimi.sh.template +39 -0
- package/templates/start-opencode.sh.template +47 -5
- package/templates/start-pi.sh.template +30 -0
- package/templates/trajectory-redaction.json.template +4 -0
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import assert from "node:assert/strict";
|
|
4
|
+
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
5
|
+
import { setTimeout as delay } from "node:timers/promises";
|
|
6
|
+
import { pathToFileURL } from "node:url";
|
|
7
|
+
|
|
8
|
+
const [pluginPath, repoPath, fixturePath, scenario = "contract"] = process.argv.slice(2);
|
|
9
|
+
const sessionID = process.env.CAIRN_TEST_SESSION ?? "session-fixture";
|
|
10
|
+
|
|
11
|
+
assert.ok(
|
|
12
|
+
pluginPath && repoPath && fixturePath,
|
|
13
|
+
"usage: capability-opencode-plugin-harness.mjs <plugin.ts> <repo> <fixture.json> [scenario]",
|
|
14
|
+
);
|
|
15
|
+
|
|
16
|
+
const fixture = JSON.parse(readFileSync(fixturePath, "utf8"));
|
|
17
|
+
const contract = fixture?.opencode;
|
|
18
|
+
const pluginSource = readFileSync(pluginPath, "utf8");
|
|
19
|
+
assert.match(pluginSource, /spawn\("node", \[COORDINATOR, operation\]/,
|
|
20
|
+
"OpenCode's Bun host must delegate the coordinator to Node");
|
|
21
|
+
assert.doesNotMatch(pluginSource, /spawn\(process\.execPath/,
|
|
22
|
+
"OpenCode's process.execPath is the harness binary, not Node");
|
|
23
|
+
assert.match(pluginSource, /state\.operations === 0\s*\? Promise\.resolve\(operation\(\)\)/,
|
|
24
|
+
"non-awaited OpenCode terminal callbacks must start coordinator I/O synchronously");
|
|
25
|
+
assert.match(pluginSource, /spawnSync\("node", \[COORDINATOR, operation\]/,
|
|
26
|
+
"OpenCode terminal settlement needs a bounded synchronous Bun-host bridge");
|
|
27
|
+
assert.match(pluginSource, /process\.versions as NodeJS\.ProcessVersions & \{ bun\?: string \}/,
|
|
28
|
+
"the synchronous terminal bridge must stay scoped to OpenCode's Bun host");
|
|
29
|
+
assert.equal(contract?.version, "1.17.20", "fixture must pin OpenCode 1.17.20");
|
|
30
|
+
assert.equal(
|
|
31
|
+
contract?.source?.commit,
|
|
32
|
+
"4473fc3c9055046183990a965d68df3db7ea6f62",
|
|
33
|
+
"fixture must pin the official v1.17.20 source commit",
|
|
34
|
+
);
|
|
35
|
+
assert.equal(contract?.hook?.name, "command.execute.before");
|
|
36
|
+
assert.deepEqual(contract?.dispose, {
|
|
37
|
+
name: "dispose",
|
|
38
|
+
phase: "plugin-shutdown",
|
|
39
|
+
input_fields: [],
|
|
40
|
+
additional_properties: false,
|
|
41
|
+
source_file: "packages/plugin/src/index.ts",
|
|
42
|
+
source_commit: "4473fc3c9055046183990a965d68df3db7ea6f62",
|
|
43
|
+
purpose: "Release plugin-owned resources after the host stops using the plugin instance.",
|
|
44
|
+
});
|
|
45
|
+
assert.deepEqual(contract?.hook?.input?.required_fields, ["command", "sessionID", "arguments"]);
|
|
46
|
+
assert.deepEqual(contract?.hook?.output?.required_fields, ["parts"]);
|
|
47
|
+
assert.deepEqual(Object.keys(contract?.events ?? {}).sort(), [
|
|
48
|
+
"session.deleted",
|
|
49
|
+
"session.error",
|
|
50
|
+
"session.idle",
|
|
51
|
+
"session.status",
|
|
52
|
+
]);
|
|
53
|
+
assert.equal(contract?.lifecycle?.event_callback_is_awaited, false);
|
|
54
|
+
|
|
55
|
+
function assertExactKeys(value, required, optional = []) {
|
|
56
|
+
assert.ok(value && typeof value === "object" && !Array.isArray(value));
|
|
57
|
+
const keys = Object.keys(value).sort();
|
|
58
|
+
const allowed = [...required, ...optional].sort();
|
|
59
|
+
for (const key of required) assert.ok(keys.includes(key), `missing required field: ${key}`);
|
|
60
|
+
for (const key of keys) assert.ok(allowed.includes(key), `unexpected fixture field: ${key}`);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
assertExactKeys(
|
|
64
|
+
contract.hook.input.sample,
|
|
65
|
+
contract.hook.input.required_fields,
|
|
66
|
+
contract.hook.input.optional_fields,
|
|
67
|
+
);
|
|
68
|
+
assertExactKeys(
|
|
69
|
+
contract.hook.output.sample,
|
|
70
|
+
contract.hook.output.required_fields,
|
|
71
|
+
contract.hook.output.optional_fields,
|
|
72
|
+
);
|
|
73
|
+
for (const [name, eventContract] of Object.entries(contract.events)) {
|
|
74
|
+
assertExactKeys(
|
|
75
|
+
eventContract.sample,
|
|
76
|
+
eventContract.required_fields,
|
|
77
|
+
eventContract.optional_fields,
|
|
78
|
+
);
|
|
79
|
+
assertExactKeys(
|
|
80
|
+
eventContract.sample.properties,
|
|
81
|
+
eventContract.properties_required_fields,
|
|
82
|
+
eventContract.properties_optional_fields,
|
|
83
|
+
);
|
|
84
|
+
assert.equal(eventContract.sample.type, name);
|
|
85
|
+
if (name === "session.deleted") {
|
|
86
|
+
assertExactKeys(
|
|
87
|
+
eventContract.sample.properties.info,
|
|
88
|
+
eventContract.info_required_fields,
|
|
89
|
+
eventContract.info_optional_fields,
|
|
90
|
+
);
|
|
91
|
+
assertExactKeys(
|
|
92
|
+
eventContract.sample.properties.info.time,
|
|
93
|
+
eventContract.time_required_fields,
|
|
94
|
+
eventContract.time_optional_fields,
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const module = await import(`${pathToFileURL(pluginPath).href}?test=${Date.now()}`);
|
|
100
|
+
for (const [name, value] of Object.entries(module)) {
|
|
101
|
+
assert.equal(typeof value, "function", `OpenCode treats exported ${name} as a plugin entrypoint`);
|
|
102
|
+
}
|
|
103
|
+
assert.equal(typeof module.CapabilityCommandPlugin, "function");
|
|
104
|
+
assert.deepEqual(module.CapabilityCommandPlugin.contract, {
|
|
105
|
+
version: contract.version,
|
|
106
|
+
sourceCommit: contract.source.commit,
|
|
107
|
+
admissionHook: contract.hook.name,
|
|
108
|
+
terminalEvents: contract.lifecycle,
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
const calls = [];
|
|
112
|
+
const client = {
|
|
113
|
+
session: {
|
|
114
|
+
get: async ({ path }) => ({
|
|
115
|
+
data: {
|
|
116
|
+
id: path.id,
|
|
117
|
+
projectID: "project-fixture",
|
|
118
|
+
directory: repoPath,
|
|
119
|
+
title: "fixture",
|
|
120
|
+
version: contract.version,
|
|
121
|
+
time: { created: 1, updated: 2 },
|
|
122
|
+
},
|
|
123
|
+
}),
|
|
124
|
+
},
|
|
125
|
+
};
|
|
126
|
+
const plugin = await module.CapabilityCommandPlugin({
|
|
127
|
+
client,
|
|
128
|
+
project: {
|
|
129
|
+
id: "project-fixture",
|
|
130
|
+
worktree: scenario === "identity-mismatch" ? `${repoPath}-decoy` : repoPath,
|
|
131
|
+
},
|
|
132
|
+
directory: repoPath,
|
|
133
|
+
worktree: repoPath,
|
|
134
|
+
serverUrl: new URL("http://127.0.0.1:4096"),
|
|
135
|
+
$: undefined,
|
|
136
|
+
experimental_workspace: undefined,
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
assert.ok(plugin && typeof plugin === "object");
|
|
140
|
+
assert.equal(typeof plugin[contract.hook.name], "function");
|
|
141
|
+
assert.equal(typeof plugin.event, "function");
|
|
142
|
+
|
|
143
|
+
async function commandBefore(targetSessionID = sessionID) {
|
|
144
|
+
const input = structuredClone(contract.hook.input.sample);
|
|
145
|
+
const output = structuredClone(contract.hook.output.sample);
|
|
146
|
+
input.sessionID = targetSessionID;
|
|
147
|
+
if (scenario === "malformed-admission") input.unexpected = "rejected";
|
|
148
|
+
try {
|
|
149
|
+
await plugin[contract.hook.name](input, output);
|
|
150
|
+
calls.push({ boundary: contract.hook.name, result: "allowed", parts: output.parts.length });
|
|
151
|
+
} catch (error) {
|
|
152
|
+
calls.push({
|
|
153
|
+
boundary: contract.hook.name,
|
|
154
|
+
result: "blocked",
|
|
155
|
+
error: error instanceof Error ? error.message : String(error),
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
async function deliver(eventName, targetSessionID = sessionID) {
|
|
161
|
+
const event = structuredClone(contract.events[eventName].sample);
|
|
162
|
+
if (eventName === "session.deleted") event.properties.info.id = targetSessionID;
|
|
163
|
+
else event.properties.sessionID = targetSessionID;
|
|
164
|
+
await plugin.event({ event });
|
|
165
|
+
calls.push({ boundary: "event", event: eventName });
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
async function disposePlugin() {
|
|
169
|
+
if (typeof plugin.dispose !== "function") {
|
|
170
|
+
calls.push({ boundary: contract.dispose.name, result: "missing" });
|
|
171
|
+
return false;
|
|
172
|
+
}
|
|
173
|
+
await plugin.dispose();
|
|
174
|
+
calls.push({ boundary: contract.dispose.name, result: "completed" });
|
|
175
|
+
return true;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function traceRows() {
|
|
179
|
+
const trace = process.env.CAIRN_BOUNDARY_TRACE;
|
|
180
|
+
if (!trace || !existsSync(trace)) return [];
|
|
181
|
+
const raw = readFileSync(trace, "utf8").trim();
|
|
182
|
+
return raw ? raw.split(/\n/).map(JSON.parse) : [];
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
async function waitForOperation(operation, count) {
|
|
186
|
+
for (let attempt = 0; attempt < 200; attempt += 1) {
|
|
187
|
+
if (traceRows().filter((row) => row.operation === operation).length >= count) return;
|
|
188
|
+
await delay(10);
|
|
189
|
+
}
|
|
190
|
+
throw new Error(`coordinator operation did not arrive: ${operation} #${count}`);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function releaseCoordinator() {
|
|
194
|
+
const release = process.env.CAIRN_TEST_RELEASE_FILE;
|
|
195
|
+
assert.ok(release, "CAIRN_TEST_RELEASE_FILE is required for ordering scenarios");
|
|
196
|
+
writeFileSync(release, "release\n");
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const scenarios = new Set([
|
|
200
|
+
"contract",
|
|
201
|
+
"admission",
|
|
202
|
+
"success",
|
|
203
|
+
"status-success",
|
|
204
|
+
"error",
|
|
205
|
+
"abandonment",
|
|
206
|
+
"duplicate-success",
|
|
207
|
+
"duplicate-error",
|
|
208
|
+
"settled-then-delete",
|
|
209
|
+
"cwd-drift-success",
|
|
210
|
+
"malformed-admission",
|
|
211
|
+
"identity-mismatch",
|
|
212
|
+
"dispose-contract",
|
|
213
|
+
"dispose-unfinished",
|
|
214
|
+
"dispose-multiple",
|
|
215
|
+
"dispose-settled-success",
|
|
216
|
+
"dispose-settled-error",
|
|
217
|
+
"dispose-terminal-race",
|
|
218
|
+
"terminal-next-admission",
|
|
219
|
+
"dispose-master-off",
|
|
220
|
+
]);
|
|
221
|
+
assert.ok(scenarios.has(scenario), `unsupported scenario: ${scenario}`);
|
|
222
|
+
|
|
223
|
+
if (!new Set(["contract", "dispose-contract", "dispose-multiple"]).has(scenario)) await commandBefore();
|
|
224
|
+
if (scenario === "success") await deliver("session.idle");
|
|
225
|
+
if (scenario === "status-success") await deliver("session.status");
|
|
226
|
+
if (scenario === "error") await deliver("session.error");
|
|
227
|
+
if (scenario === "abandonment") await deliver("session.deleted");
|
|
228
|
+
if (scenario === "duplicate-success") {
|
|
229
|
+
await deliver("session.idle");
|
|
230
|
+
await deliver("session.status");
|
|
231
|
+
await deliver("session.idle");
|
|
232
|
+
}
|
|
233
|
+
if (scenario === "duplicate-error") {
|
|
234
|
+
await deliver("session.error");
|
|
235
|
+
await deliver("session.error");
|
|
236
|
+
}
|
|
237
|
+
if (scenario === "settled-then-delete") {
|
|
238
|
+
await deliver("session.idle");
|
|
239
|
+
await deliver("session.deleted");
|
|
240
|
+
}
|
|
241
|
+
if (scenario === "cwd-drift-success") {
|
|
242
|
+
process.chdir(`${repoPath}-decoy`);
|
|
243
|
+
await deliver("session.idle");
|
|
244
|
+
}
|
|
245
|
+
if (scenario === "dispose-contract") await disposePlugin();
|
|
246
|
+
if (scenario === "dispose-unfinished" || scenario === "dispose-master-off") await disposePlugin();
|
|
247
|
+
if (scenario === "dispose-multiple") {
|
|
248
|
+
await commandBefore(`${sessionID}-one`);
|
|
249
|
+
await commandBefore(`${sessionID}-two`);
|
|
250
|
+
await disposePlugin();
|
|
251
|
+
}
|
|
252
|
+
if (scenario === "dispose-settled-success") {
|
|
253
|
+
await deliver("session.idle");
|
|
254
|
+
await disposePlugin();
|
|
255
|
+
}
|
|
256
|
+
if (scenario === "dispose-settled-error") {
|
|
257
|
+
await deliver("session.error");
|
|
258
|
+
await disposePlugin();
|
|
259
|
+
}
|
|
260
|
+
if (scenario === "dispose-terminal-race") {
|
|
261
|
+
assert.equal(typeof plugin.dispose, "function", "native dispose hook is missing");
|
|
262
|
+
const terminal = deliver("session.idle");
|
|
263
|
+
await waitForOperation("harness-terminal", 1);
|
|
264
|
+
const disposal = plugin.dispose();
|
|
265
|
+
await delay(50);
|
|
266
|
+
releaseCoordinator();
|
|
267
|
+
await terminal;
|
|
268
|
+
await disposal;
|
|
269
|
+
calls.push({ boundary: contract.dispose.name, result: "completed" });
|
|
270
|
+
}
|
|
271
|
+
if (scenario === "terminal-next-admission") {
|
|
272
|
+
const terminal = deliver("session.error");
|
|
273
|
+
await waitForOperation("harness-terminal", 1);
|
|
274
|
+
const nextAdmission = commandBefore();
|
|
275
|
+
await delay(50);
|
|
276
|
+
assert.equal(traceRows().filter((row) => row.operation === "harness-before").length, 1,
|
|
277
|
+
"next same-session admission reached the coordinator before the accepted terminal finished");
|
|
278
|
+
releaseCoordinator();
|
|
279
|
+
await terminal;
|
|
280
|
+
await nextAdmission;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
process.stdout.write(`${JSON.stringify({
|
|
284
|
+
fixture: {
|
|
285
|
+
version: contract.version,
|
|
286
|
+
commit: contract.source.commit,
|
|
287
|
+
},
|
|
288
|
+
dispose_present: typeof plugin.dispose === "function",
|
|
289
|
+
scenario,
|
|
290
|
+
calls,
|
|
291
|
+
})}\n`);
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
import { pathToFileURL } from "node:url";
|
|
4
|
+
|
|
5
|
+
const [pluginPath, repoPath, fixturePath] = process.argv.slice(2);
|
|
6
|
+
assert.ok(pluginPath && repoPath && fixturePath, "usage: harness <plugin.ts> <repo> <fixture.json>");
|
|
7
|
+
|
|
8
|
+
const fixture = JSON.parse(readFileSync(fixturePath, "utf8"));
|
|
9
|
+
const module = await import(`${pathToFileURL(pluginPath).href}?test=${Date.now()}`);
|
|
10
|
+
assert.equal(typeof module.MemoryCapturePlugin, "function");
|
|
11
|
+
|
|
12
|
+
const client = {
|
|
13
|
+
session: {
|
|
14
|
+
get: async () => ({ data: { id: fixture.session.id } }),
|
|
15
|
+
messages: async () => ({ data: fixture.messages }),
|
|
16
|
+
},
|
|
17
|
+
};
|
|
18
|
+
const plugin = await module.MemoryCapturePlugin({ client, directory: repoPath });
|
|
19
|
+
assert.equal(typeof plugin.event, "function");
|
|
20
|
+
await plugin.event({
|
|
21
|
+
event: {
|
|
22
|
+
type: "session.idle",
|
|
23
|
+
properties: { sessionID: fixture.session.id },
|
|
24
|
+
},
|
|
25
|
+
});
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
import { pathToFileURL } from "node:url";
|
|
4
|
+
|
|
5
|
+
const [extensionPath, repoPath, fixturePath] = process.argv.slice(2);
|
|
6
|
+
assert.ok(extensionPath && repoPath && fixturePath, "usage: harness <extension.ts> <repo> <fixture.json>");
|
|
7
|
+
|
|
8
|
+
const fixture = JSON.parse(readFileSync(fixturePath, "utf8"));
|
|
9
|
+
const module = await import(`${pathToFileURL(extensionPath).href}?test=${Date.now()}`);
|
|
10
|
+
assert.equal(typeof module.default, "function");
|
|
11
|
+
|
|
12
|
+
let shutdownHandler;
|
|
13
|
+
const pi = {
|
|
14
|
+
on(event, handler) {
|
|
15
|
+
if (event === "session_shutdown") shutdownHandler = handler;
|
|
16
|
+
},
|
|
17
|
+
};
|
|
18
|
+
await module.default(pi);
|
|
19
|
+
assert.equal(typeof shutdownHandler, "function");
|
|
20
|
+
|
|
21
|
+
let sessionReads = 0;
|
|
22
|
+
const sessionManager = {
|
|
23
|
+
getSessionId() {
|
|
24
|
+
sessionReads += 1;
|
|
25
|
+
return fixture.session.id;
|
|
26
|
+
},
|
|
27
|
+
getBranch() {
|
|
28
|
+
sessionReads += 1;
|
|
29
|
+
return fixture.session.entries;
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
await shutdownHandler(
|
|
34
|
+
{ type: "session_shutdown", reason: "quit" },
|
|
35
|
+
{ cwd: repoPath, sessionManager },
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
if (/^(?:1|true|yes|on)$/i.test(process.env.CAIRN_TRAJECTORY_CAPTURE ?? "")) {
|
|
39
|
+
assert.equal(sessionReads, 2, "enabled adapter must read the session ID and active branch exactly once");
|
|
40
|
+
} else {
|
|
41
|
+
assert.equal(sessionReads, 0, "disabled adapter must not touch the SessionManager");
|
|
42
|
+
}
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
# 2. open hard contradictions in CONTRADICTIONS.md
|
|
7
7
|
# 3. orphan source pages not linked from index.md
|
|
8
8
|
# 4. unreviewed memory candidates staged longer than STALE_DAYS
|
|
9
|
+
# 5. optional closed-trajectory note distillation in a separate process
|
|
9
10
|
#
|
|
10
11
|
# Output: a consolidated markdown report on stdout (and optionally to REPORT_PATH).
|
|
11
12
|
# Designed for a systemd timer or cron. Pure bash/grep — no model round-trip, so
|
|
@@ -19,6 +20,8 @@
|
|
|
19
20
|
# --report PATH also write the report to this file
|
|
20
21
|
set -euo pipefail
|
|
21
22
|
|
|
23
|
+
CAIRN_ROOT=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
|
|
24
|
+
|
|
22
25
|
PARA_ROOT="${HOME}/PARA"
|
|
23
26
|
STALE_DAYS=30
|
|
24
27
|
REPORT_PATH=""
|
|
@@ -41,6 +44,8 @@ hard_open_total=0
|
|
|
41
44
|
orphan_total=0
|
|
42
45
|
staged_stale_total=0
|
|
43
46
|
sections=()
|
|
47
|
+
note_distillation_enabled=0
|
|
48
|
+
note_distillation_status=""
|
|
44
49
|
|
|
45
50
|
# Find every project with a wiki.
|
|
46
51
|
while IFS= read -r wiki_dir; do
|
|
@@ -118,6 +123,47 @@ while IFS= read -r wiki_dir; do
|
|
|
118
123
|
fi
|
|
119
124
|
done < <(find "$PARA_ROOT" -maxdepth 4 -type d -name wiki -path '*/.planning/wiki' 2>/dev/null)
|
|
120
125
|
|
|
126
|
+
# Keep note taking outside every harness callback. The audit timer starts the
|
|
127
|
+
# same public one-shot command an operator would run. Master-off retains the
|
|
128
|
+
# compatibility flag; master-on consumes the central effective status. A note
|
|
129
|
+
# failure is reported but never hides or changes wiki findings or their exit.
|
|
130
|
+
note_distillation_eligible=0
|
|
131
|
+
case "${CAIRN_CAPABILITY_CONTRACT:-}" in
|
|
132
|
+
1|true|TRUE|yes|YES|on|ON)
|
|
133
|
+
capability_status="$("$CAIRN_ROOT/bin/cairn" capabilities status --json 2>/dev/null || true)"
|
|
134
|
+
if printf '%s' "$capability_status" | node -e '
|
|
135
|
+
let input = "";
|
|
136
|
+
process.stdin.setEncoding("utf8");
|
|
137
|
+
process.stdin.on("data", (chunk) => { input += chunk; });
|
|
138
|
+
process.stdin.on("end", () => {
|
|
139
|
+
try {
|
|
140
|
+
const status = JSON.parse(input);
|
|
141
|
+
const notes = status.capabilities?.find((row) => row.id === "notes.distill");
|
|
142
|
+
process.exit(notes?.enabled === true ? 0 : 1);
|
|
143
|
+
} catch {
|
|
144
|
+
process.exit(1);
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
'; then
|
|
148
|
+
note_distillation_eligible=1
|
|
149
|
+
fi
|
|
150
|
+
;;
|
|
151
|
+
*)
|
|
152
|
+
case "${CAIRN_NOTE_DISTILLATION:-}" in
|
|
153
|
+
1|true|TRUE|yes|YES|on|ON) note_distillation_eligible=1 ;;
|
|
154
|
+
esac
|
|
155
|
+
;;
|
|
156
|
+
esac
|
|
157
|
+
|
|
158
|
+
if (( note_distillation_eligible == 1 )); then
|
|
159
|
+
note_distillation_enabled=1
|
|
160
|
+
if "$CAIRN_ROOT/bin/cairn" notes distill --all-projects --para-root "$PARA_ROOT" --json >/dev/null 2>&1; then
|
|
161
|
+
note_distillation_status="completed"
|
|
162
|
+
else
|
|
163
|
+
note_distillation_status="failed (deterministic wiki findings remain valid)"
|
|
164
|
+
fi
|
|
165
|
+
fi
|
|
166
|
+
|
|
121
167
|
# Build report once, output to stdout (and REPORT_PATH if set).
|
|
122
168
|
report_body="$(
|
|
123
169
|
{
|
|
@@ -129,6 +175,9 @@ report_body="$(
|
|
|
129
175
|
echo "- Open hard contradictions: $hard_open_total"
|
|
130
176
|
echo "- Orphan source pages: $orphan_total"
|
|
131
177
|
echo "- Unreviewed staged candidates: $staged_stale_total"
|
|
178
|
+
if (( note_distillation_enabled == 1 )); then
|
|
179
|
+
echo "- Note distillation: $note_distillation_status"
|
|
180
|
+
fi
|
|
132
181
|
echo
|
|
133
182
|
if (( ${#sections[@]} > 0 )); then
|
|
134
183
|
echo "## Findings by project"
|
package/scripts/smoke-bash32.sh
CHANGED
|
@@ -30,6 +30,7 @@ SCRIPTS=(
|
|
|
30
30
|
scripts/sync-claude-assets.sh
|
|
31
31
|
scripts/sync-opencode-explore-assets.sh
|
|
32
32
|
scripts/sync-opencode-wiki-assets.sh
|
|
33
|
+
scripts/sync-pi-assets.sh
|
|
33
34
|
)
|
|
34
35
|
|
|
35
36
|
fails=0
|
|
@@ -49,6 +50,14 @@ for s in "${SCRIPTS[@]}"; do
|
|
|
49
50
|
fi
|
|
50
51
|
done
|
|
51
52
|
|
|
53
|
+
help_out=$("$RUNTIME" run --rm -v "$ROOT_DIR":/w:ro -w /w "$IMG" bash scripts/doctor.sh --help 2>&1) || true
|
|
54
|
+
if grep -q 'typed metadata.*note transactions' <<<"$help_out"; then
|
|
55
|
+
echo " [PASS] doctor help runs on bash 3.2"
|
|
56
|
+
else
|
|
57
|
+
echo " [FAIL] doctor help did not expose Phase 16 repair scope"
|
|
58
|
+
fails=$((fails + 1))
|
|
59
|
+
fi
|
|
60
|
+
|
|
52
61
|
if [[ $fails -eq 0 ]]; then
|
|
53
62
|
echo "smoke-bash32: OK"
|
|
54
63
|
exit 0
|
|
@@ -4,7 +4,7 @@ shopt -s nullglob
|
|
|
4
4
|
|
|
5
5
|
usage() {
|
|
6
6
|
cat <<'EOF'
|
|
7
|
-
Usage: sync-claude-assets.sh [--check|--apply] [--live-root PATH]
|
|
7
|
+
Usage: sync-claude-assets.sh [--check|--apply] [--capability-overlay] [--live-root PATH]
|
|
8
8
|
|
|
9
9
|
Compare or sync the repo-managed Claude Code assets (commands, agents) and the
|
|
10
10
|
security/wiki scaffold templates against the live Claude Code config tree.
|
|
@@ -12,12 +12,17 @@ security/wiki scaffold templates against the live Claude Code config tree.
|
|
|
12
12
|
Options:
|
|
13
13
|
--check Verify that the live assets match the repo copy (default)
|
|
14
14
|
--apply Copy the repo-managed assets into the live Claude tree, then verify
|
|
15
|
+
--capability-overlay
|
|
16
|
+
Select guarded commands and native capability hooks only when
|
|
17
|
+
CAIRN_CAPABILITY_CONTRACT is also enabled
|
|
15
18
|
--live-root PATH Override the live Claude root (default: $CLAUDE_CONFIG_DIR or $HOME/.claude)
|
|
16
19
|
-h, --help Show this help text
|
|
17
20
|
|
|
18
21
|
Notes:
|
|
19
22
|
- Claude assets source of truth lives under ./claude/ (commands/, agents/).
|
|
20
23
|
- Installs claude/commands/*.md -> <live>/commands/ and claude/agents/*.md -> <live>/agents/.
|
|
24
|
+
- --capability-overlay substitutes matching files from claude/capability-contract/
|
|
25
|
+
only with the master switch enabled; all other modes ignore that tree.
|
|
21
26
|
- Also installs templates/{security,wiki}-*.template -> <live>/templates/ so the
|
|
22
27
|
/security-audit and /wiki-* commands can scaffold from $HOME/.claude/templates/.
|
|
23
28
|
- Renders claude/hooks/*.sh -> <live>/hooks/ (substituting @@INFRA_ROOT@@) and
|
|
@@ -32,17 +37,34 @@ CLAUDE_SOURCE="$ROOT_DIR/claude"
|
|
|
32
37
|
TEMPLATE_SOURCE="$ROOT_DIR/templates"
|
|
33
38
|
LIVE_ROOT="${CLAUDE_CONFIG_DIR:-$HOME/.claude}"
|
|
34
39
|
MODE="check"
|
|
40
|
+
CAPABILITY_OVERLAY=0
|
|
35
41
|
|
|
36
42
|
while [[ $# -gt 0 ]]; do
|
|
37
43
|
case "$1" in
|
|
38
44
|
--check) MODE="check"; shift ;;
|
|
39
45
|
--apply) MODE="apply"; shift ;;
|
|
46
|
+
--capability-overlay) CAPABILITY_OVERLAY=1; shift ;;
|
|
40
47
|
--live-root) LIVE_ROOT="$2"; shift 2 ;;
|
|
41
48
|
-h|--help) usage; exit 0 ;;
|
|
42
49
|
*) echo "Unknown argument: $1" >&2; usage >&2; exit 2 ;;
|
|
43
50
|
esac
|
|
44
51
|
done
|
|
45
52
|
|
|
53
|
+
capability_contract_enabled() {
|
|
54
|
+
local value="${CAIRN_CAPABILITY_CONTRACT:-}"
|
|
55
|
+
value="${value#"${value%%[![:space:]]*}"}"
|
|
56
|
+
value="${value%"${value##*[![:space:]]}"}"
|
|
57
|
+
case "$value" in
|
|
58
|
+
1|[Tt][Rr][Uu][Ee]|[Yy][Ee][Ss]|[Oo][Nn]) return 0 ;;
|
|
59
|
+
*) return 1 ;;
|
|
60
|
+
esac
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
CAPABILITY_OVERLAY_ACTIVE=0
|
|
64
|
+
if [[ "$CAPABILITY_OVERLAY" -eq 1 ]] && capability_contract_enabled; then
|
|
65
|
+
CAPABILITY_OVERLAY_ACTIVE=1
|
|
66
|
+
fi
|
|
67
|
+
|
|
46
68
|
if [[ ! -d "$CLAUDE_SOURCE" ]]; then
|
|
47
69
|
echo "No repo-managed Claude assets found at $CLAUDE_SOURCE" >&2
|
|
48
70
|
exit 1
|
|
@@ -54,9 +76,13 @@ DSTS=()
|
|
|
54
76
|
|
|
55
77
|
# 1. claude/ tree -> <live>/...
|
|
56
78
|
while IFS= read -r rel; do
|
|
57
|
-
|
|
79
|
+
src="$CLAUDE_SOURCE/$rel"
|
|
80
|
+
if [[ "$CAPABILITY_OVERLAY_ACTIVE" -eq 1 && -f "$CLAUDE_SOURCE/capability-contract/$rel" ]]; then
|
|
81
|
+
src="$CLAUDE_SOURCE/capability-contract/$rel"
|
|
82
|
+
fi
|
|
83
|
+
SRCS+=("$src")
|
|
58
84
|
DSTS+=("$LIVE_ROOT/$rel")
|
|
59
|
-
done < <(cd "$CLAUDE_SOURCE" && find . -type f -name '*.md' | sed 's|^\./||' | sort)
|
|
85
|
+
done < <(cd "$CLAUDE_SOURCE" && find . -path './capability-contract' -prune -o -type f -name '*.md' -print | sed 's|^\./||' | sort)
|
|
60
86
|
|
|
61
87
|
# 2. security/wiki scaffold templates -> <live>/templates/
|
|
62
88
|
for tpl in "$TEMPLATE_SOURCE"/security-*.template "$TEMPLATE_SOURCE"/wiki-*.template; do
|
|
@@ -71,6 +97,8 @@ if ((${#SRCS[@]} == 0)); then
|
|
|
71
97
|
fi
|
|
72
98
|
|
|
73
99
|
status=0
|
|
100
|
+
HOOK_LIVE_DIR="$LIVE_ROOT/hooks"
|
|
101
|
+
SETTINGS_FILE="$LIVE_ROOT/settings.json"
|
|
74
102
|
for i in "${!SRCS[@]}"; do
|
|
75
103
|
src="${SRCS[$i]}"
|
|
76
104
|
dst="${DSTS[$i]}"
|
|
@@ -89,12 +117,76 @@ for i in "${!SRCS[@]}"; do
|
|
|
89
117
|
fi
|
|
90
118
|
done
|
|
91
119
|
|
|
120
|
+
# Capability hooks are intentionally outside claude/hooks so the legacy loop
|
|
121
|
+
# above cannot discover, render, register, or execute them. They enter an
|
|
122
|
+
# isolated root only through the effective overlay branch.
|
|
123
|
+
if [[ "$CAPABILITY_OVERLAY_ACTIVE" -eq 1 ]]; then
|
|
124
|
+
for hook_src in "$CLAUDE_SOURCE"/capability-contract/hooks/*.sh; do
|
|
125
|
+
[[ -f "$hook_src" ]] || continue
|
|
126
|
+
hook_name="$(basename "$hook_src")"
|
|
127
|
+
hook_dst="$HOOK_LIVE_DIR/$hook_name"
|
|
128
|
+
hook_cmd="bash \"$hook_dst\""
|
|
129
|
+
case "$hook_name" in
|
|
130
|
+
capability-command-start.sh)
|
|
131
|
+
hook_event_specs='UserPromptExpansion:wiki-ingest|wiki-query|wiki-lint|graphify|security-audit'
|
|
132
|
+
;;
|
|
133
|
+
capability-command-finish.sh)
|
|
134
|
+
hook_event_specs=$'Stop\nStopFailure\nCwdChanged\nSessionEnd'
|
|
135
|
+
;;
|
|
136
|
+
*)
|
|
137
|
+
continue
|
|
138
|
+
;;
|
|
139
|
+
esac
|
|
140
|
+
|
|
141
|
+
if [[ "$MODE" == "apply" ]]; then
|
|
142
|
+
mkdir -p "$HOOK_LIVE_DIR"
|
|
143
|
+
sed "s|@@INFRA_ROOT@@|$ROOT_DIR|g" "$hook_src" > "$hook_dst"
|
|
144
|
+
chmod +x "$hook_dst"
|
|
145
|
+
echo "installed: hooks/$hook_name"
|
|
146
|
+
fi
|
|
147
|
+
|
|
148
|
+
while IFS= read -r event_spec; do
|
|
149
|
+
[[ -n "$event_spec" ]] || continue
|
|
150
|
+
event="${event_spec%%:*}"
|
|
151
|
+
matcher="${event_spec#*:}"
|
|
152
|
+
[[ "$matcher" == "$event_spec" ]] && matcher=""
|
|
153
|
+
if [[ "$MODE" == "apply" ]]; then
|
|
154
|
+
node -e '
|
|
155
|
+
const fs = require("fs");
|
|
156
|
+
const [p, cmd, token, event, matcher] = process.argv.slice(1);
|
|
157
|
+
let s = {};
|
|
158
|
+
if (fs.existsSync(p)) {
|
|
159
|
+
try { s = JSON.parse(fs.readFileSync(p, "utf8")); }
|
|
160
|
+
catch (e) { console.error("settings.json parse failed, leaving untouched"); process.exit(1); }
|
|
161
|
+
}
|
|
162
|
+
s.hooks = s.hooks || {};
|
|
163
|
+
s.hooks[event] = Array.isArray(s.hooks[event]) ? s.hooks[event] : [];
|
|
164
|
+
const already = s.hooks[event].some((entry) =>
|
|
165
|
+
Array.isArray(entry.hooks) && entry.hooks.some((h) => typeof h.command === "string" && h.command.includes(token)));
|
|
166
|
+
if (already) { console.log(`ok: ${event} hook already registered`); process.exit(0); }
|
|
167
|
+
if (fs.existsSync(p)) fs.copyFileSync(p, p + ".bak");
|
|
168
|
+
const entry = matcher
|
|
169
|
+
? { matcher, hooks: [{ type: "command", command: cmd }] }
|
|
170
|
+
: { hooks: [{ type: "command", command: cmd }] };
|
|
171
|
+
s.hooks[event].push(entry);
|
|
172
|
+
fs.writeFileSync(p, JSON.stringify(s, null, 2) + "\n");
|
|
173
|
+
console.log(`registered: ${event} hook${matcher ? " (" + matcher + ")" : ""}`);
|
|
174
|
+
' "$SETTINGS_FILE" "$hook_cmd" "$hook_dst" "$event" "$matcher" || status=1
|
|
175
|
+
else
|
|
176
|
+
if [[ -f "$hook_dst" ]] && grep -qF "$hook_name" "$SETTINGS_FILE" 2>/dev/null; then
|
|
177
|
+
echo "ok: hooks/$hook_name ($event)"
|
|
178
|
+
else
|
|
179
|
+
echo "DRIFT: hooks/$hook_name ($event)"
|
|
180
|
+
status=1
|
|
181
|
+
fi
|
|
182
|
+
fi
|
|
183
|
+
done <<< "$hook_event_specs"
|
|
184
|
+
done
|
|
185
|
+
fi
|
|
186
|
+
|
|
92
187
|
# Claude hooks: render @@INFRA_ROOT@@ and register each hook on its correct
|
|
93
188
|
# event in <live>/settings.json. This is the one place that touches the global
|
|
94
189
|
# user settings; it backs up first and matches by command so re-runs are no-ops.
|
|
95
|
-
HOOK_LIVE_DIR="$LIVE_ROOT/hooks"
|
|
96
|
-
SETTINGS_FILE="$LIVE_ROOT/settings.json"
|
|
97
|
-
|
|
98
190
|
# Map hook filename -> "event[:matcher][@timeout]". matcher only for
|
|
99
191
|
# PreToolUse-style events; an optional trailing "@timeout" (seconds) threads
|
|
100
192
|
# an explicit per-hook timeout into the settings.json registration entry so
|
|
@@ -108,6 +200,7 @@ hook_event_for() {
|
|
|
108
200
|
case "$1" in
|
|
109
201
|
memory-wakeup.sh) echo "SessionStart" ;;
|
|
110
202
|
memory-capture.sh) echo "SessionEnd" ;;
|
|
203
|
+
compaction-capture.sh) echo "PostCompact" ;;
|
|
111
204
|
memory-recall.sh) echo "PreToolUse:Edit|Write|MultiEdit" ;;
|
|
112
205
|
context-explore-pretask.sh) echo "UserPromptSubmit@25" ;;
|
|
113
206
|
*) echo "" ;;
|