@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,119 @@
|
|
|
1
|
+
{
|
|
2
|
+
"session": {
|
|
3
|
+
"id": "opencode-compaction-session-001",
|
|
4
|
+
"title": "Pinned compaction fixture",
|
|
5
|
+
"version": "1.17.20",
|
|
6
|
+
"projectID": "synthetic-project",
|
|
7
|
+
"directory": "/synthetic/project",
|
|
8
|
+
"time": {
|
|
9
|
+
"created": 1785052800000,
|
|
10
|
+
"updated": 1785052810000
|
|
11
|
+
}
|
|
12
|
+
},
|
|
13
|
+
"messages": [
|
|
14
|
+
{
|
|
15
|
+
"info": {
|
|
16
|
+
"id": "oc-ordinary-user",
|
|
17
|
+
"sessionID": "opencode-compaction-session-001",
|
|
18
|
+
"role": "user",
|
|
19
|
+
"time": { "created": 1785052801000 }
|
|
20
|
+
},
|
|
21
|
+
"parts": [
|
|
22
|
+
{ "id": "oc-ordinary-text", "sessionID": "opencode-compaction-session-001", "messageID": "oc-ordinary-user", "type": "text", "text": "Continue normal work." }
|
|
23
|
+
]
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
"info": {
|
|
27
|
+
"id": "oc-compact-user-old",
|
|
28
|
+
"sessionID": "opencode-compaction-session-001",
|
|
29
|
+
"role": "user",
|
|
30
|
+
"time": { "created": 1785052802000 }
|
|
31
|
+
},
|
|
32
|
+
"parts": [
|
|
33
|
+
{ "id": "oc-compaction-part-old", "sessionID": "opencode-compaction-session-001", "messageID": "oc-compact-user-old", "type": "compaction", "auto": true }
|
|
34
|
+
]
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
"info": {
|
|
38
|
+
"id": "oc-summary-incomplete",
|
|
39
|
+
"sessionID": "opencode-compaction-session-001",
|
|
40
|
+
"role": "assistant",
|
|
41
|
+
"parentID": "oc-compact-user-old",
|
|
42
|
+
"summary": true,
|
|
43
|
+
"time": { "created": 1785052803000 }
|
|
44
|
+
},
|
|
45
|
+
"parts": [
|
|
46
|
+
{ "id": "oc-incomplete-text", "sessionID": "opencode-compaction-session-001", "messageID": "oc-summary-incomplete", "type": "text", "text": "# Objective\nThis incomplete decoy must not win." }
|
|
47
|
+
]
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
"info": {
|
|
51
|
+
"id": "oc-summary-error",
|
|
52
|
+
"sessionID": "opencode-compaction-session-001",
|
|
53
|
+
"role": "assistant",
|
|
54
|
+
"parentID": "oc-compact-user-old",
|
|
55
|
+
"summary": true,
|
|
56
|
+
"finish": "stop",
|
|
57
|
+
"error": { "name": "SyntheticError", "message": "failed summary decoy" },
|
|
58
|
+
"time": { "created": 1785052803500, "completed": 1785052803600 }
|
|
59
|
+
},
|
|
60
|
+
"parts": [
|
|
61
|
+
{ "id": "oc-error-text", "sessionID": "opencode-compaction-session-001", "messageID": "oc-summary-error", "type": "text", "text": "# Objective\nThis errored decoy must not win." }
|
|
62
|
+
]
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
"info": {
|
|
66
|
+
"id": "oc-summary-valid-old",
|
|
67
|
+
"sessionID": "opencode-compaction-session-001",
|
|
68
|
+
"role": "assistant",
|
|
69
|
+
"parentID": "oc-compact-user-old",
|
|
70
|
+
"summary": true,
|
|
71
|
+
"finish": "stop",
|
|
72
|
+
"time": { "created": 1785052804000, "completed": 1785052805000 }
|
|
73
|
+
},
|
|
74
|
+
"parts": [
|
|
75
|
+
{ "id": "oc-old-summary-text", "sessionID": "opencode-compaction-session-001", "messageID": "oc-summary-valid-old", "type": "text", "text": "# Objective\nPreserve the old goal.\n\n# Important Details\n- Decision: Keep this valid summary as a selection fallback.\n\n# Work State\n## Completed\n- Fixture parsing is pinned.\n## Active\n- Older work remains.\n## Blocked\n- Error: OldError at old.ts:1.\n\n# Next Move\n- Replace this summary with a newer valid revision.\n\n# Relevant Files\n- src/old.ts" }
|
|
76
|
+
]
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
"info": {
|
|
80
|
+
"id": "oc-compact-user-new",
|
|
81
|
+
"sessionID": "opencode-compaction-session-001",
|
|
82
|
+
"role": "user",
|
|
83
|
+
"time": { "created": 1785052806000 }
|
|
84
|
+
},
|
|
85
|
+
"parts": [
|
|
86
|
+
{ "id": "oc-compaction-part-new", "sessionID": "opencode-compaction-session-001", "messageID": "oc-compact-user-new", "type": "compaction", "auto": false }
|
|
87
|
+
]
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
"info": {
|
|
91
|
+
"id": "oc-summary-wrong-parent",
|
|
92
|
+
"sessionID": "opencode-compaction-session-001",
|
|
93
|
+
"role": "assistant",
|
|
94
|
+
"parentID": "oc-ordinary-user",
|
|
95
|
+
"summary": true,
|
|
96
|
+
"finish": "stop",
|
|
97
|
+
"time": { "created": 1785052808000, "completed": 1785052809000 }
|
|
98
|
+
},
|
|
99
|
+
"parts": [
|
|
100
|
+
{ "id": "oc-wrong-parent-text", "sessionID": "opencode-compaction-session-001", "messageID": "oc-summary-wrong-parent", "type": "text", "text": "# Objective\nThis newest wrong-parent decoy must not win." }
|
|
101
|
+
]
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
"info": {
|
|
105
|
+
"id": "oc-summary-valid-new",
|
|
106
|
+
"sessionID": "opencode-compaction-session-001",
|
|
107
|
+
"role": "assistant",
|
|
108
|
+
"parentID": "oc-compact-user-new",
|
|
109
|
+
"summary": true,
|
|
110
|
+
"finish": "stop",
|
|
111
|
+
"time": { "created": 1785052807000, "completed": 1785052808000 }
|
|
112
|
+
},
|
|
113
|
+
"parts": [
|
|
114
|
+
{ "id": "oc-new-summary-text", "sessionID": "opencode-compaction-session-001", "messageID": "oc-summary-valid-new", "type": "text", "text": "# Objective\nPreserve supported compaction state across sessions.\n\n# Important Details\n- Decision: Pin adapter behavior to the documented session.compacted family.\n- Nested credentials: {\"authorization\":\"Bearer opencode-compaction-secret-001\",\"nested\":{\"api_key\":\"sk-opencode-compaction-secret-002\"}}\n\n# Work State\n## Completed\n- The exact fixture family is recorded.\n## Active\n- Adapter implementation remains.\n## Blocked\n- Error: TypeError: recovery pointer invalid at selectRecovery (compaction.ts:42).\n\n# Next Move\n- Implement the local immutable compaction adapter.\n\n# Relevant Files\n- mcp-memory-server/src/compaction-normalize.ts" },
|
|
115
|
+
{ "id": "oc-new-summary-ignored", "sessionID": "opencode-compaction-session-001", "messageID": "oc-summary-valid-new", "type": "text", "text": "IGNORED_SUMMARY_SECRET", "ignored": true }
|
|
116
|
+
]
|
|
117
|
+
}
|
|
118
|
+
]
|
|
119
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"harness": "opencode",
|
|
3
|
+
"harness_version": "99.0.0",
|
|
4
|
+
"event": {
|
|
5
|
+
"type": "session.compacted",
|
|
6
|
+
"properties": {
|
|
7
|
+
"sessionID": "unknown-version-session"
|
|
8
|
+
}
|
|
9
|
+
},
|
|
10
|
+
"payload": {
|
|
11
|
+
"authorization": "Bearer unknown-version-secret-001",
|
|
12
|
+
"nested": {
|
|
13
|
+
"api_key": "sk-unknown-version-secret-002",
|
|
14
|
+
"summary": "UNKNOWN_VERSION_PAYLOAD_MUST_NOT_SURVIVE"
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
import type { Plugin } from "@opencode-ai/plugin"
|
|
2
|
+
import { spawn, spawnSync } from "node:child_process"
|
|
3
|
+
import fs from "node:fs"
|
|
4
|
+
import path from "node:path"
|
|
5
|
+
|
|
6
|
+
const COORDINATOR = "@@INFRA_ROOT@@/mcp-memory-server/dist/capability-cli.js"
|
|
7
|
+
const FIXED_BLOCK = "Cairn capability disabled."
|
|
8
|
+
const MAX_COORDINATOR_OUTPUT = 8 * 1024
|
|
9
|
+
const COORDINATOR_TIMEOUT_MS = 3000
|
|
10
|
+
const MAX_TRACKED_SESSIONS = 10_000
|
|
11
|
+
const DISPOSAL_BATCH_SIZE = 32
|
|
12
|
+
const SESSION_ID = /^(?:cairn:)?[A-Za-z0-9][A-Za-z0-9._:-]{0,255}$/
|
|
13
|
+
const COMMANDS = new Set([
|
|
14
|
+
"wiki-ingest",
|
|
15
|
+
"wiki-query",
|
|
16
|
+
"wiki-lint",
|
|
17
|
+
"graphify",
|
|
18
|
+
"security-audit",
|
|
19
|
+
])
|
|
20
|
+
|
|
21
|
+
const OPENCODE_CAPABILITY_CONTRACT = {
|
|
22
|
+
version: "1.17.20",
|
|
23
|
+
sourceCommit: "4473fc3c9055046183990a965d68df3db7ea6f62",
|
|
24
|
+
admissionHook: "command.execute.before",
|
|
25
|
+
terminalEvents: {
|
|
26
|
+
success_terminals: ["session.idle", "session.status:idle"],
|
|
27
|
+
error_terminal: "session.error",
|
|
28
|
+
abandonment_only: "session.deleted",
|
|
29
|
+
event_callback_is_awaited: false,
|
|
30
|
+
},
|
|
31
|
+
} as const
|
|
32
|
+
|
|
33
|
+
type JsonObject = Record<string, unknown>
|
|
34
|
+
type SessionState = {
|
|
35
|
+
unfinished: boolean
|
|
36
|
+
terminalEpoch: number
|
|
37
|
+
operations: number
|
|
38
|
+
tail: Promise<void>
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function isObject(value: unknown): value is JsonObject {
|
|
42
|
+
return value !== null && typeof value === "object" && !Array.isArray(value)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function hasExactKeys(value: JsonObject, required: readonly string[], optional: readonly string[] = []): boolean {
|
|
46
|
+
const keys = Object.keys(value)
|
|
47
|
+
const allowed = new Set([...required, ...optional])
|
|
48
|
+
return required.every((key) => Object.hasOwn(value, key))
|
|
49
|
+
&& keys.every((key) => allowed.has(key))
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function validSessionID(value: unknown): value is string {
|
|
53
|
+
return typeof value === "string" && value !== "unknown" && SESSION_ID.test(value)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function contractEnabled(): boolean {
|
|
57
|
+
return /^(1|true|yes|on)$/i.test(process.env.CAIRN_CAPABILITY_CONTRACT ?? "")
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function validatedProjectRoot(values: unknown[]): string | undefined {
|
|
61
|
+
try {
|
|
62
|
+
if (!values.every((value) => typeof value === "string" && path.isAbsolute(value))) return undefined
|
|
63
|
+
const roots = values.map((value) => fs.realpathSync(value as string))
|
|
64
|
+
if (!roots.every((value) => value === roots[0])) return undefined
|
|
65
|
+
if (!fs.statSync(roots[0]).isDirectory()) return undefined
|
|
66
|
+
return roots[0]
|
|
67
|
+
} catch {
|
|
68
|
+
return undefined
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function coordinator(operation: string, payload: JsonObject): Promise<JsonObject | undefined> {
|
|
73
|
+
return new Promise((resolvePromise) => {
|
|
74
|
+
let stdout = ""
|
|
75
|
+
let settled = false
|
|
76
|
+
let timer: ReturnType<typeof setTimeout> | undefined
|
|
77
|
+
const finish = (value?: JsonObject) => {
|
|
78
|
+
if (settled) return
|
|
79
|
+
settled = true
|
|
80
|
+
if (timer) clearTimeout(timer)
|
|
81
|
+
resolvePromise(value)
|
|
82
|
+
}
|
|
83
|
+
const child = spawn("node", [COORDINATOR, operation], {
|
|
84
|
+
stdio: ["pipe", "pipe", "ignore"],
|
|
85
|
+
})
|
|
86
|
+
timer = setTimeout(() => {
|
|
87
|
+
try {
|
|
88
|
+
child.kill("SIGKILL")
|
|
89
|
+
} catch {
|
|
90
|
+
// best-effort termination only
|
|
91
|
+
}
|
|
92
|
+
finish()
|
|
93
|
+
}, COORDINATOR_TIMEOUT_MS)
|
|
94
|
+
child.stdout.on("data", (chunk: Buffer) => {
|
|
95
|
+
stdout += chunk.toString("utf8")
|
|
96
|
+
if (Buffer.byteLength(stdout, "utf8") > MAX_COORDINATOR_OUTPUT) {
|
|
97
|
+
stdout = ""
|
|
98
|
+
try {
|
|
99
|
+
child.kill("SIGKILL")
|
|
100
|
+
} catch {
|
|
101
|
+
// best-effort termination only
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
})
|
|
105
|
+
child.on("error", () => finish())
|
|
106
|
+
child.on("close", (code) => {
|
|
107
|
+
if (code !== 0 || !stdout) {
|
|
108
|
+
finish()
|
|
109
|
+
return
|
|
110
|
+
}
|
|
111
|
+
try {
|
|
112
|
+
const value = JSON.parse(stdout) as unknown
|
|
113
|
+
finish(isObject(value) ? value : undefined)
|
|
114
|
+
} catch {
|
|
115
|
+
finish()
|
|
116
|
+
}
|
|
117
|
+
})
|
|
118
|
+
child.stdin.on("error", () => finish())
|
|
119
|
+
child.stdin.end(JSON.stringify(payload))
|
|
120
|
+
})
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function coordinatorSync(operation: string, payload: JsonObject): JsonObject | undefined {
|
|
124
|
+
const child = spawnSync("node", [COORDINATOR, operation], {
|
|
125
|
+
input: JSON.stringify(payload),
|
|
126
|
+
encoding: "utf8",
|
|
127
|
+
timeout: COORDINATOR_TIMEOUT_MS,
|
|
128
|
+
maxBuffer: MAX_COORDINATOR_OUTPUT,
|
|
129
|
+
stdio: ["pipe", "pipe", "ignore"],
|
|
130
|
+
})
|
|
131
|
+
if (child.error || child.status !== 0 || !child.stdout) return undefined
|
|
132
|
+
try {
|
|
133
|
+
const value = JSON.parse(child.stdout) as unknown
|
|
134
|
+
return isObject(value) ? value : undefined
|
|
135
|
+
} catch {
|
|
136
|
+
return undefined
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function terminalRequiresSync(): boolean {
|
|
141
|
+
return (process.versions as NodeJS.ProcessVersions & { bun?: string }).bun !== undefined
|
|
142
|
+
|| process.env.CAIRN_TEST_OPENCODE_NON_AWAITED === "1"
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function admission(input: unknown, output: unknown): {
|
|
146
|
+
command: string
|
|
147
|
+
sessionID: string
|
|
148
|
+
} | undefined {
|
|
149
|
+
if (!isObject(input) || !isObject(output)) return undefined
|
|
150
|
+
if (!hasExactKeys(input, ["command", "sessionID", "arguments"])) return undefined
|
|
151
|
+
if (!hasExactKeys(output, ["parts"]) || !Array.isArray(output.parts)) return undefined
|
|
152
|
+
if (typeof input.command !== "string" || !COMMANDS.has(input.command)) return undefined
|
|
153
|
+
if (!validSessionID(input.sessionID) || typeof input.arguments !== "string") return undefined
|
|
154
|
+
return { command: input.command, sessionID: input.sessionID }
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function eventTerminal(event: unknown): { sessionID: string; outcome: "success" | "error" | "abandoned" } | undefined {
|
|
158
|
+
if (!isObject(event) || !hasExactKeys(event, ["type", "properties"], ["id"]) || !isObject(event.properties)) {
|
|
159
|
+
return undefined
|
|
160
|
+
}
|
|
161
|
+
if (Object.hasOwn(event, "id")
|
|
162
|
+
&& (typeof event.id !== "string" || event.id.length === 0 || event.id.length > 256)) return undefined
|
|
163
|
+
const properties = event.properties
|
|
164
|
+
if (event.type === "session.idle") {
|
|
165
|
+
if (!hasExactKeys(properties, ["sessionID"]) || !validSessionID(properties.sessionID)) return undefined
|
|
166
|
+
return { sessionID: properties.sessionID, outcome: "success" }
|
|
167
|
+
}
|
|
168
|
+
if (event.type === "session.error") {
|
|
169
|
+
if (!hasExactKeys(properties, [], ["sessionID", "error"]) || !validSessionID(properties.sessionID)) return undefined
|
|
170
|
+
return { sessionID: properties.sessionID, outcome: "error" }
|
|
171
|
+
}
|
|
172
|
+
if (event.type === "session.status") {
|
|
173
|
+
if (!hasExactKeys(properties, ["sessionID", "status"]) || !validSessionID(properties.sessionID)) return undefined
|
|
174
|
+
if (!isObject(properties.status) || properties.status.type !== "idle" || !hasExactKeys(properties.status, ["type"])) {
|
|
175
|
+
return undefined
|
|
176
|
+
}
|
|
177
|
+
return { sessionID: properties.sessionID, outcome: "success" }
|
|
178
|
+
}
|
|
179
|
+
if (event.type === "session.deleted") {
|
|
180
|
+
if (!hasExactKeys(properties, ["info"]) || !isObject(properties.info)) return undefined
|
|
181
|
+
const info = properties.info
|
|
182
|
+
if (!hasExactKeys(
|
|
183
|
+
info,
|
|
184
|
+
["id", "projectID", "directory", "title", "version", "time"],
|
|
185
|
+
["parentID", "summary", "share", "revert"],
|
|
186
|
+
) || !validSessionID(info.id) || !isObject(info.time)) return undefined
|
|
187
|
+
if (!hasExactKeys(info.time, ["created", "updated"], ["compacting"])) return undefined
|
|
188
|
+
if (typeof info.projectID !== "string" || typeof info.directory !== "string"
|
|
189
|
+
|| typeof info.title !== "string" || typeof info.version !== "string"
|
|
190
|
+
|| typeof info.time.created !== "number" || typeof info.time.updated !== "number") return undefined
|
|
191
|
+
return { sessionID: info.id, outcome: "abandoned" }
|
|
192
|
+
}
|
|
193
|
+
return undefined
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const capabilityCommandPlugin: Plugin = async ({ directory, worktree, project }) => {
|
|
197
|
+
const projectRoot = validatedProjectRoot([directory, worktree, project.worktree])
|
|
198
|
+
const sessions = new Map<string, SessionState>()
|
|
199
|
+
|
|
200
|
+
const sessionState = (sessionID: string): SessionState | undefined => {
|
|
201
|
+
const existing = sessions.get(sessionID)
|
|
202
|
+
if (existing) return existing
|
|
203
|
+
if (sessions.size >= MAX_TRACKED_SESSIONS) return undefined
|
|
204
|
+
const created: SessionState = {
|
|
205
|
+
unfinished: false,
|
|
206
|
+
terminalEpoch: 0,
|
|
207
|
+
operations: 0,
|
|
208
|
+
tail: Promise.resolve(),
|
|
209
|
+
}
|
|
210
|
+
sessions.set(sessionID, created)
|
|
211
|
+
return created
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const forgetFinishedSession = (sessionID: string, state: SessionState) => {
|
|
215
|
+
if (!state.unfinished && state.operations === 0 && sessions.get(sessionID) === state) {
|
|
216
|
+
sessions.delete(sessionID)
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const serialize = async <T>(sessionID: string, state: SessionState, operation: () => Promise<T>): Promise<T> => {
|
|
221
|
+
const result = state.operations === 0
|
|
222
|
+
? Promise.resolve(operation())
|
|
223
|
+
: state.tail.then(operation, operation)
|
|
224
|
+
state.operations += 1
|
|
225
|
+
state.tail = result.then(() => undefined, () => undefined)
|
|
226
|
+
try {
|
|
227
|
+
return await result
|
|
228
|
+
} finally {
|
|
229
|
+
state.operations -= 1
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const callCoordinator = (operation: string, payload: JsonObject) => coordinator(operation, payload)
|
|
234
|
+
.catch(() => undefined)
|
|
235
|
+
|
|
236
|
+
return {
|
|
237
|
+
"command.execute.before": async (input, output) => {
|
|
238
|
+
if (!contractEnabled()) return
|
|
239
|
+
if (!isObject(input) || typeof input.command !== "string" || !COMMANDS.has(input.command)) return
|
|
240
|
+
const parsed = admission(input, output)
|
|
241
|
+
if (!parsed || !projectRoot || !fs.existsSync(COORDINATOR)) throw new Error(FIXED_BLOCK)
|
|
242
|
+
const state = sessionState(parsed.sessionID)
|
|
243
|
+
const terminalEpoch = state?.terminalEpoch
|
|
244
|
+
const invoke = () => callCoordinator("harness-before", {
|
|
245
|
+
schema_version: 1,
|
|
246
|
+
harness: "opencode",
|
|
247
|
+
command: parsed.command,
|
|
248
|
+
session_id: parsed.sessionID,
|
|
249
|
+
project_root: projectRoot,
|
|
250
|
+
})
|
|
251
|
+
let decision: JsonObject | undefined
|
|
252
|
+
try {
|
|
253
|
+
decision = state
|
|
254
|
+
? await serialize(parsed.sessionID, state, invoke)
|
|
255
|
+
: await invoke()
|
|
256
|
+
} catch (error) {
|
|
257
|
+
if (state) forgetFinishedSession(parsed.sessionID, state)
|
|
258
|
+
throw error
|
|
259
|
+
}
|
|
260
|
+
if (decision?.schema_version !== 1 || (decision.decision !== "allow" && decision.decision !== "block")) {
|
|
261
|
+
if (state) forgetFinishedSession(parsed.sessionID, state)
|
|
262
|
+
throw new Error(FIXED_BLOCK)
|
|
263
|
+
}
|
|
264
|
+
if (decision.decision === "block") {
|
|
265
|
+
if (state) forgetFinishedSession(parsed.sessionID, state)
|
|
266
|
+
throw new Error(FIXED_BLOCK)
|
|
267
|
+
}
|
|
268
|
+
if (state && state.terminalEpoch === terminalEpoch) state.unfinished = true
|
|
269
|
+
else if (state) forgetFinishedSession(parsed.sessionID, state)
|
|
270
|
+
},
|
|
271
|
+
event: async ({ event }) => {
|
|
272
|
+
if (!contractEnabled() || !fs.existsSync(COORDINATOR)) return
|
|
273
|
+
const terminal = eventTerminal(event)
|
|
274
|
+
if (!terminal) return
|
|
275
|
+
const state = sessionState(terminal.sessionID)
|
|
276
|
+
if (state) {
|
|
277
|
+
state.unfinished = false
|
|
278
|
+
state.terminalEpoch += 1
|
|
279
|
+
}
|
|
280
|
+
const payload = {
|
|
281
|
+
schema_version: 1,
|
|
282
|
+
harness: "opencode",
|
|
283
|
+
session_id: terminal.sessionID,
|
|
284
|
+
outcome: terminal.outcome,
|
|
285
|
+
} as const
|
|
286
|
+
const invoke = () => terminalRequiresSync()
|
|
287
|
+
? Promise.resolve(coordinatorSync("harness-terminal", payload))
|
|
288
|
+
: callCoordinator("harness-terminal", payload)
|
|
289
|
+
if (state) {
|
|
290
|
+
await serialize(terminal.sessionID, state, invoke)
|
|
291
|
+
forgetFinishedSession(terminal.sessionID, state)
|
|
292
|
+
} else await invoke()
|
|
293
|
+
},
|
|
294
|
+
dispose: async () => {
|
|
295
|
+
if (!contractEnabled() || !fs.existsSync(COORDINATOR)) return
|
|
296
|
+
const unfinished = [...sessions.entries()].filter(([, state]) => state.unfinished)
|
|
297
|
+
for (const [, state] of unfinished) {
|
|
298
|
+
state.unfinished = false
|
|
299
|
+
state.terminalEpoch += 1
|
|
300
|
+
}
|
|
301
|
+
for (let offset = 0; offset < unfinished.length; offset += DISPOSAL_BATCH_SIZE) {
|
|
302
|
+
const batch = unfinished.slice(offset, offset + DISPOSAL_BATCH_SIZE)
|
|
303
|
+
for (const [sessionID, state] of batch) {
|
|
304
|
+
await serialize(sessionID, state, () => callCoordinator("harness-terminal", {
|
|
305
|
+
schema_version: 1,
|
|
306
|
+
harness: "opencode",
|
|
307
|
+
session_id: sessionID,
|
|
308
|
+
outcome: "abandoned",
|
|
309
|
+
}))
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
for (const [sessionID, state] of unfinished) forgetFinishedSession(sessionID, state)
|
|
313
|
+
},
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
export const CapabilityCommandPlugin = Object.assign(capabilityCommandPlugin, {
|
|
318
|
+
contract: OPENCODE_CAPABILITY_CONTRACT,
|
|
319
|
+
})
|
|
@@ -7,22 +7,21 @@ tools:
|
|
|
7
7
|
task: true
|
|
8
8
|
---
|
|
9
9
|
|
|
10
|
-
**STOP -- DO NOT READ THIS FILE. You are already reading it. This prompt was injected into your context by
|
|
10
|
+
**STOP -- DO NOT READ THIS FILE. You are already reading it. This prompt was injected into your context by OpenCode's command system. Using the Read tool on this file wastes tokens. Begin executing Step 1 immediately.**
|
|
11
11
|
|
|
12
12
|
**graphify CLI:** the `graphify` subcommand is exposed by `gsd-tools` (on PATH). Invoke as `gsd-tools graphify …` as documented in this command and in `docs/CLI-TOOLS.md`. Other GSD queries use `gsd-tools <subcmd>` where a handler exists.
|
|
13
13
|
|
|
14
|
-
##
|
|
14
|
+
## Output heading
|
|
15
15
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
GRAPHIFY
|
|
20
|
-
```
|
|
21
|
-
|
|
22
|
-
Then proceed to Step 1.
|
|
16
|
+
Begin the final user-facing response with `GRAPHIFY`. Do not emit a standalone
|
|
17
|
+
banner before owner I/O: in headless OpenCode, content-only output can end the
|
|
18
|
+
turn before the command reaches its tools.
|
|
23
19
|
|
|
24
20
|
## Step 1 -- Config Gate
|
|
25
21
|
|
|
22
|
+
**First action:** invoke the Read tool for `.planning/config.json`; do not emit
|
|
23
|
+
text before this tool call.
|
|
24
|
+
|
|
26
25
|
Check if graphify is enabled by reading `.planning/config.json` directly using the Read tool.
|
|
27
26
|
|
|
28
27
|
**DO NOT use the gsd-tools config get-value command** -- it hard-exits on missing keys.
|
|
@@ -49,7 +48,15 @@ Then run /graphify build to create the initial graph.
|
|
|
49
48
|
|
|
50
49
|
## Step 2 -- Parse Argument
|
|
51
50
|
|
|
52
|
-
|
|
51
|
+
The exact invocation argument is delimited here:
|
|
52
|
+
|
|
53
|
+
```text
|
|
54
|
+
<argument>$ARGUMENTS</argument>
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Parse only the text between those markers to determine the operation mode. If
|
|
58
|
+
the markers contain `status`, that is the status mode; do not treat it as an
|
|
59
|
+
empty or unknown argument. Apply the same exact matching rule to every row:
|
|
53
60
|
|
|
54
61
|
| Argument | Action |
|
|
55
62
|
|----------|--------|
|
|
@@ -75,7 +82,8 @@ Modes:
|
|
|
75
82
|
|
|
76
83
|
### Step 2a -- Query
|
|
77
84
|
|
|
78
|
-
|
|
85
|
+
**Invoke the shell tool** with this exact command; do not print the command as
|
|
86
|
+
the result and do not continue until the tool returns:
|
|
79
87
|
|
|
80
88
|
```bash
|
|
81
89
|
gsd-tools graphify query <term>
|
|
@@ -91,7 +99,8 @@ Parse the JSON output and display results:
|
|
|
91
99
|
|
|
92
100
|
### Step 2b -- Status
|
|
93
101
|
|
|
94
|
-
|
|
102
|
+
**Invoke the shell tool** with this exact command; do not print the command as
|
|
103
|
+
the result and do not continue until the tool returns:
|
|
95
104
|
|
|
96
105
|
```bash
|
|
97
106
|
gsd-tools graphify status
|
|
@@ -105,7 +114,8 @@ Parse the JSON output and display:
|
|
|
105
114
|
|
|
106
115
|
### Step 2c -- Diff
|
|
107
116
|
|
|
108
|
-
|
|
117
|
+
**Invoke the shell tool** with this exact command; do not print the command as
|
|
118
|
+
the result and do not continue until the tool returns:
|
|
109
119
|
|
|
110
120
|
```bash
|
|
111
121
|
gsd-tools graphify diff
|
|
@@ -123,7 +133,8 @@ If no snapshot exists, suggest running `build` twice (first to create, second to
|
|
|
123
133
|
|
|
124
134
|
## Step 3 -- Build (Inline)
|
|
125
135
|
|
|
126
|
-
|
|
136
|
+
**Invoke the shell tool** with this exact command; do not print the command as
|
|
137
|
+
the result and do not continue until the tool returns:
|
|
127
138
|
|
|
128
139
|
```bash
|
|
129
140
|
gsd-tools graphify build [--force]
|