@cairnkeep/cli 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (129) hide show
  1. package/AGENTS.md +45 -0
  2. package/CONTRIBUTING.md +49 -0
  3. package/LICENSE +201 -0
  4. package/README.md +149 -0
  5. package/SECURITY.md +37 -0
  6. package/bin/cairn +73 -0
  7. package/claude/agents/code-reviewer.md +174 -0
  8. package/claude/agents/security-investigator.md +112 -0
  9. package/claude/agents/security-target-selector.md +90 -0
  10. package/claude/agents/security-validator.md +97 -0
  11. package/claude/agents/wiki-ingester.md +108 -0
  12. package/claude/agents/wiki-lint-auditor.md +94 -0
  13. package/claude/agents/wiki-query-analyst.md +84 -0
  14. package/claude/commands/context-explore.md +40 -0
  15. package/claude/commands/graphify.md +150 -0
  16. package/claude/commands/memory-review.md +85 -0
  17. package/claude/commands/memory-sync.md +71 -0
  18. package/claude/commands/recall.md +32 -0
  19. package/claude/commands/remember.md +43 -0
  20. package/claude/commands/repo-review.md +191 -0
  21. package/claude/commands/security-audit.md +174 -0
  22. package/claude/commands/wiki-ingest.md +112 -0
  23. package/claude/commands/wiki-lint.md +86 -0
  24. package/claude/commands/wiki-query.md +92 -0
  25. package/claude/hooks/context-explore-pretask.sh +93 -0
  26. package/claude/hooks/memory-capture.sh +59 -0
  27. package/claude/hooks/memory-recall.sh +84 -0
  28. package/claude/hooks/memory-wakeup.sh +45 -0
  29. package/docs/building-an-overlay.md +117 -0
  30. package/docs/domain-knowledge.md +86 -0
  31. package/docs/git-providers.md +36 -0
  32. package/docs/operating.md +366 -0
  33. package/mcp-memory-server/dist/embeddings.js +122 -0
  34. package/mcp-memory-server/dist/explore-cache.js +97 -0
  35. package/mcp-memory-server/dist/index.js +1256 -0
  36. package/mcp-memory-server/package.json +36 -0
  37. package/opencode/agents/code-reviewer.md +173 -0
  38. package/opencode/agents/security-investigator.md +111 -0
  39. package/opencode/agents/security-target-selector.md +89 -0
  40. package/opencode/agents/security-validator.md +96 -0
  41. package/opencode/agents/wiki-ingester.md +107 -0
  42. package/opencode/agents/wiki-lint-auditor.md +93 -0
  43. package/opencode/agents/wiki-query-analyst.md +83 -0
  44. package/opencode/command/code-review.md +199 -0
  45. package/opencode/command/context-explore.md +42 -0
  46. package/opencode/command/graphify.md +153 -0
  47. package/opencode/command/memory-review.md +44 -0
  48. package/opencode/command/memory-sync.md +94 -0
  49. package/opencode/command/recall.md +38 -0
  50. package/opencode/command/remember.md +46 -0
  51. package/opencode/command/security-audit.md +80 -0
  52. package/opencode/command/wiki-ingest.md +55 -0
  53. package/opencode/command/wiki-lint.md +51 -0
  54. package/opencode/command/wiki-query.md +50 -0
  55. package/opencode/plugins/memory-capture.ts +188 -0
  56. package/opencode/plugins/memory-recall.ts +132 -0
  57. package/opencode/plugins/memory-wakeup.ts +102 -0
  58. package/opencode/templates/security-finding-register.yaml.template +51 -0
  59. package/opencode/templates/security-issue.md.template +43 -0
  60. package/opencode/templates/security-policy.md.template +29 -0
  61. package/opencode/templates/security-report.md.template +43 -0
  62. package/opencode/templates/wiki-contradictions.md.template +31 -0
  63. package/opencode/templates/wiki-index.md.template +40 -0
  64. package/opencode/templates/wiki-lint-report.md.template +38 -0
  65. package/opencode/templates/wiki-log.md.template +11 -0
  66. package/opencode/templates/wiki-policy.md.template +47 -0
  67. package/opencode/templates/wiki-query-answer.md.template +26 -0
  68. package/opencode/templates/wiki-source-summary.md.template +33 -0
  69. package/opencode/workflows/memory-review-workflow.md +66 -0
  70. package/opencode/workflows/security-audit-workflow.md +197 -0
  71. package/opencode/workflows/wiki-ingest-workflow.md +149 -0
  72. package/opencode/workflows/wiki-lint-workflow.md +128 -0
  73. package/opencode/workflows/wiki-query-workflow.md +122 -0
  74. package/package.json +56 -0
  75. package/scripts/bootstrap.sh +99 -0
  76. package/scripts/doctor.sh +91 -0
  77. package/scripts/install-audit-timer.sh +68 -0
  78. package/scripts/lib/assert-tool-event.mjs +50 -0
  79. package/scripts/memory-store.sh +71 -0
  80. package/scripts/memory-wiki-audit.sh +156 -0
  81. package/scripts/sync-claude-assets.sh +182 -0
  82. package/scripts/sync-opencode-explore-assets.sh +187 -0
  83. package/scripts/sync-opencode-graphify-assets.sh +152 -0
  84. package/scripts/sync-opencode-memory-assets.sh +160 -0
  85. package/scripts/sync-opencode-plugin-assets.sh +163 -0
  86. package/scripts/sync-opencode-security-assets.sh +195 -0
  87. package/scripts/sync-opencode-wiki-assets.sh +250 -0
  88. package/scripts/test-audit-timer.sh +29 -0
  89. package/scripts/test-bootstrap-untracked.sh +41 -0
  90. package/scripts/test-cli-dispatch.sh +21 -0
  91. package/scripts/test-doctor.sh +32 -0
  92. package/scripts/test-launcher-hooks.sh +66 -0
  93. package/scripts/test-memory-store.sh +44 -0
  94. package/scripts/test-remember-recall-assertions.sh +70 -0
  95. package/scripts/transcript-to-text.mjs +53 -0
  96. package/scripts/verify-docs-parity.sh +112 -0
  97. package/scripts/verify-explore-maturation.sh +288 -0
  98. package/scripts/verify-fastcontext-reliability.sh +624 -0
  99. package/scripts/verify-no-private-references.sh +145 -0
  100. package/scripts/verify-opencode-live-parity.sh +864 -0
  101. package/scripts/verify-routing-seam.sh +169 -0
  102. package/scripts/verify-token-savings-ab.sh +557 -0
  103. package/templates/PROJECT-INSTRUCTIONS.md.template +42 -0
  104. package/templates/alignment-gap-register.yaml.template +56 -0
  105. package/templates/alignment-policy.md.template +28 -0
  106. package/templates/alignment-pr-template.md.template +18 -0
  107. package/templates/alignment-report.md.template +56 -0
  108. package/templates/env.example.template +25 -0
  109. package/templates/graph-policy.md.template +37 -0
  110. package/templates/graphs-gitignore.template +3 -0
  111. package/templates/memory-wiki-audit.service.template +6 -0
  112. package/templates/memory-wiki-audit.timer.template +9 -0
  113. package/templates/memory.json.template +4 -0
  114. package/templates/planning-config.json.template +6 -0
  115. package/templates/project-brief.md.template +40 -0
  116. package/templates/security-finding-register.yaml.template +51 -0
  117. package/templates/security-issue.md.template +43 -0
  118. package/templates/security-policy.md.template +29 -0
  119. package/templates/security-report.md.template +43 -0
  120. package/templates/start-claude.sh.template +46 -0
  121. package/templates/start-opencode.sh.template +41 -0
  122. package/templates/structured-change-canvas.md.template +63 -0
  123. package/templates/wiki-contradictions.md.template +31 -0
  124. package/templates/wiki-index.md.template +40 -0
  125. package/templates/wiki-lint-report.md.template +38 -0
  126. package/templates/wiki-log.md.template +11 -0
  127. package/templates/wiki-policy.md.template +47 -0
  128. package/templates/wiki-query-answer.md.template +26 -0
  129. package/templates/wiki-source-summary.md.template +33 -0
@@ -0,0 +1,94 @@
1
+ ---
2
+ description: Sync project memory (AgentFS, AnythingLLM) with current MR/PR state from your git provider
3
+ argument-hint: "[--dry-run] [--force]"
4
+ tools:
5
+ read: true
6
+ write: true
7
+ edit: true
8
+ bash: true
9
+ grep: true
10
+ glob: true
11
+ agent: true
12
+ question: true
13
+ ---
14
+
15
+ Git provider: these steps use the git host set by `CAIRN_GIT_PROVIDER` (`github`/`gitlab`/`codeberg`/`forgejo`/`none`); resolve the operation-to-tool mapping from `docs/git-providers.md`. If it is `none` or unset, or no provider MCP is registered, skip the provider steps and continue locally.
16
+
17
+ <objective>
18
+ Sync all memory tools with the current state of tracked MRs and PRs.
19
+
20
+ Output artifacts:
21
+ - Updated AgentFS keys: `internal-mrs-status`, `upstream-prs-status`
22
+ - Optional AnythingLLM sync if new docs were created
23
+
24
+ Flag handling:
25
+ - `--dry-run` — show what would change without writing
26
+ - `--force` — sync even if nothing appears changed (useful after manual edits)
27
+
28
+ This is an operational command. It reads live API state and writes to memory stores.
29
+ </objective>
30
+
31
+ <context>
32
+ Arguments: $ARGUMENTS
33
+
34
+ Default behavior:
35
+ 1. Read `.opencode/memory.json` to determine project scope and workspaces
36
+ 2. Read AgentFS key `internal-mrs-status` to get tracked MR list
37
+ 3. For each tracked MR, query the git-provider MCP for current state
38
+ 4. Read AgentFS key `upstream-prs-status` to get tracked PR list
39
+ 5. For each tracked PR, query the git-provider MCP for current state
40
+ 6. Compare live state against stored state
41
+ 7. If changed (or --force):
42
+ - Update AgentFS keys with current state
43
+ - If anythingllm_workspaces is configured and new docs exist, offer sync
44
+ 8. Print summary of changes
45
+
46
+ Important rules:
47
+ - Never delete memory entries — only update them
48
+ - Preserve historical context (merged dates, close reasons) in updated entries
49
+ - If an API call fails, skip that item and continue with others
50
+ - Log the sync timestamp in updated entries
51
+ </context>
52
+
53
+ <process>
54
+ ## 1. Read memory configuration
55
+ Read `.opencode/memory.json` to get scopes and anythingllm_workspaces. If no config file exists, default to scope `project` with no workspaces.
56
+
57
+ ## 2. Extract tracked MRs and PRs from AgentFS
58
+ Read AgentFS keys `internal-mrs-status` and `upstream-prs-status` from the `project` scope.
59
+ Parse the markdown to extract tracked items:
60
+ - MRs: `project_id` and `iid` (format: `!IID` in project `ID` or `PATH`)
61
+ - PRs: `owner/repo#NUMBER`
62
+
63
+ ## 3. Query live state
64
+ For each tracked MR:
65
+ - Call the git-provider `get_merge_request` with `project_id` and `merge_request_iid`
66
+ - Capture: state, draft, assignees, merge_status, updated_at, latest discussion notes (last 3)
67
+
68
+ For each tracked PR:
69
+ - Call the git-provider `get_pull_request` (or your provider's equivalent) for the PR
70
+ - Capture: state, merged, updated_at, comments count
71
+
72
+ ## 4. Compare and diff
73
+ Compare each item's live state against the stored AgentFS state:
74
+ - State changes (open→merged, open→closed, etc.)
75
+ - Assignee changes
76
+ - New discussion notes / comments
77
+ - Draft status changes
78
+ - Merge status changes
79
+
80
+ ## 5. Write updates
81
+ If `--dry-run`, print the diff and stop.
82
+
83
+ Otherwise:
84
+ - Update AgentFS `internal-mrs-status` with current state (preserve historical notes)
85
+ - Update AgentFS `upstream-prs-status` with current state
86
+ - If `anythingllm_workspaces` is configured, check if any new `.md` files exist in the repo root that aren't yet synced. If so, offer to sync (don't auto-sync).
87
+
88
+ ## 6. Print summary
89
+ Output a compact table of:
90
+ - Each tracked item
91
+ - Current state
92
+ - Changed? (yes/no)
93
+ - Notable change (if any)
94
+ </process>
@@ -0,0 +1,38 @@
1
+ ---
2
+ description: Recall known info across memory layers (AgentFS project scope, wiki index, optional AnythingLLM)
3
+ argument-hint: "<question or topic>"
4
+ tools:
5
+ read: true
6
+ grep: true
7
+ glob: true
8
+ cairn-memory_memory_read: true
9
+ cairn-memory_memory_search: true
10
+ cairn-memory_domain_knowledge_query: true
11
+ ---
12
+
13
+ <objective>
14
+ Cross-layer read companion to the automatic session-start wakeup. Use mid-session when the task shifts and you need stored context the wakeup did not surface.
15
+ </objective>
16
+
17
+ <context>
18
+ Question or topic: $ARGUMENTS
19
+
20
+ Read order (canonical sources still win over derived layers):
21
+ 1. AgentFS project scope (short accepted facts)
22
+ 2. Wiki index and derived pages (.planning/wiki/)
23
+ 3. AnythingLLM workspaces (only if repo docs and memory are insufficient)
24
+ </context>
25
+
26
+ <process>
27
+ ## 1. AgentFS
28
+ `cairn-memory_memory_search` scope `project` with $ARGUMENTS. If nothing relevant, retry scope `all`.
29
+
30
+ ## 2. Wiki
31
+ Read `.planning/wiki/index.md` if present and follow links relevant to $ARGUMENTS.
32
+
33
+ ## 3. AnythingLLM (optional)
34
+ Only if 1 and 2 are insufficient and the answer likely lives in long-form docs: `cairn-memory_domain_knowledge_query` against the project workspace from the memory config.
35
+
36
+ ## 4. Report
37
+ Summarize what each layer returned in a few lines. If layers disagree, prefer the canonical raw source and note the staleness.
38
+ </process>
@@ -0,0 +1,46 @@
1
+ ---
2
+ description: Persist a durable finding to AgentFS, flagging wiki/AnythingLLM candidates
3
+ argument-hint: "<the fact to remember>"
4
+ tools:
5
+ read: true
6
+ grep: true
7
+ glob: true
8
+ cairn-memory_memory_read: true
9
+ cairn-memory_memory_search: true
10
+ cairn-memory_memory_write: true
11
+ cairn-memory_memory_supersede: true
12
+ ---
13
+
14
+ <objective>
15
+ Persist one durable, accepted finding so future sessions recall it without being asked.
16
+
17
+ This is the write half of the memory loop. It writes the fact to AgentFS (the shared, cross-harness durable store), and it only flags the document layers (wiki, AnythingLLM) as candidates rather than writing to them blindly, because those are compiled or embedded from canonical sources.
18
+ </objective>
19
+
20
+ <context>
21
+ Fact to remember: $ARGUMENTS
22
+
23
+ Layer shapes (do not treat as copies of the same thing):
24
+ - AgentFS project scope: short key/value facts, accepted and branch-safe. Direct write. The sole durable structured store for OpenCode.
25
+ - Wiki (`.planning/wiki/`): derived synthesis, compiled via /wiki-ingest. Not a raw-fact store.
26
+ - AnythingLLM: document embeddings, refreshed via domain_knowledge_sync. Not a raw-fact store.
27
+ </context>
28
+
29
+ <process>
30
+ ## 1. Guard
31
+ If $ARGUMENTS is empty, ask what to remember and stop. Do not invent a fact.
32
+
33
+ ## 2. Dedupe before writing
34
+ Run `cairn-memory_memory_search` (scope `project`) on the key terms of the fact. If a near-duplicate key exists, update that entry instead of creating a new one.
35
+
36
+ ## 3. Write AgentFS (project scope)
37
+ If step 2 found a near-duplicate key that should keep its prior value for audit/debugging, use `cairn-memory_memory_supersede` on that key. Otherwise use `cairn-memory_memory_write` scope `project` with a short kebab-case key and the fact as value. Convert relative dates to absolute. No em dashes or double-hyphen dashes (use commas or full stops).
38
+
39
+ ## 4. Flag document layers (do not auto-run)
40
+ - If the fact is stable, doc-worthy synthesis of a canonical source, suggest `/wiki-ingest <path>`.
41
+ - If a repo document changed and retrieval should reflect it, suggest `domain_knowledge_sync`.
42
+ State the suggestion in one line each; do not execute them here.
43
+
44
+ ## 5. Confirm
45
+ Print: the AgentFS key written, and any flagged candidates.
46
+ </process>
@@ -0,0 +1,80 @@
1
+ ---
2
+ description: Run a governed local security audit against one scoped attack surface and write repo-local artifacts
3
+ argument-hint: "[--full] [--focus <path>] [--threat-model <name>] [--verify-only] [--create-confidential-issue] [--issue-project <group/project>]"
4
+ tools:
5
+ read: true
6
+ write: true
7
+ edit: true
8
+ bash: true
9
+ grep: true
10
+ glob: true
11
+ agent: true
12
+ question: true
13
+ ---
14
+
15
+ Git provider: these steps use the git host set by `CAIRN_GIT_PROVIDER` (`github`/`gitlab`/`codeberg`/`forgejo`/`none`); resolve the operation-to-tool mapping from `docs/git-providers.md`. If it is `none` or no provider MCP is registered, skip the provider steps and continue locally.
16
+
17
+ <objective>
18
+ Run one governed local security assessment against the current repository.
19
+
20
+ Output artifacts:
21
+ - `.planning/security/REPORTS/<timestamp>.md`
22
+ - `.planning/security/CANDIDATES/<timestamp>-candidate.md`
23
+ - `.planning/security/VALIDATED/SEC-xxxx-<slug>.md` for accepted findings
24
+ - optional `.planning/security/ISSUES/<timestamp>-confidential-issue.md`
25
+ - optional update to `.planning/security/FINDINGS.yaml`
26
+
27
+ Flag handling:
28
+ - `--full` — inspect the broader repository instead of the changed surface only
29
+ - `--focus <path>` — bias target selection toward one file or subtree
30
+ - `--threat-model <name>` — label and bias the run with a concrete threat model or attack surface description; defaults to `changed-surface`
31
+ - `--verify-only` — write the audit artifacts without mutating `.planning/security/FINDINGS.yaml`
32
+ - `--create-confidential-issue` — create a repo-local confidential issue draft when an accepted finding exists
33
+ - `--issue-project <group/project>` — record the intended git-provider destination for that issue draft
34
+
35
+ This is a governed local-only workflow. It must stay inside the current repository, repo-owned local processes, local files, and localhost services launched from this repo.
36
+
37
+ Recommended `--threat-model` values:
38
+ - `changed-surface` — default broad review of the changed attack surface when no stronger prior exists
39
+ - `authenticated-http-request` — authenticated API or web request abuse path
40
+ - `privilege-escalation` — boundary crossing between roles, users, or capabilities
41
+ - `session-upgrade` — session fixation, upgrade, or post-login boundary problems
42
+ - `deserialization` — parser or decoder abuse in structured input handling
43
+ - `archive-extraction` — tar/zip/extraction path traversal or unsafe unpacking
44
+ - `path-traversal` — untrusted path resolution or file access escape
45
+ - `subprocess-injection` — unsafe shell or subprocess composition
46
+ - `config-file-injection` — generated config or template injection into downstream interpreters
47
+ - `localhost-service-abuse` — repo-owned local daemon or localhost trust-boundary misuse
48
+ - `native-crash` — native parser, memory-safety, or crash-triggering input path
49
+ - `memory-corruption` — stronger native-memory-safety focus when corruption is the specific concern
50
+
51
+ Usage rule:
52
+ - use a short, concrete phrase that helps the selector and investigator reason about attacker capability or failure mode
53
+ - prefer the values above for consistency across repos and findings
54
+ </objective>
55
+
56
+ <execution_context>
57
+ @$HOME/.config/opencode/workflows/security-audit-workflow.md
58
+ </execution_context>
59
+
60
+ <context>
61
+ Arguments: $ARGUMENTS
62
+
63
+ Default behavior:
64
+ - scaffold `.planning/security/` when missing
65
+ - choose exactly one narrow target for this run
66
+ - investigate that target deeply enough to confirm or reject one candidate finding
67
+ - validate the candidate with a stricter duplicate and reachability bar
68
+ - write durable repo-local artifacts for the outcome
69
+
70
+ Important rules:
71
+ - one scoped target per run
72
+ - one candidate finding per run at most
73
+ - rejected outcomes stay in the run history, not in the canonical ledger
74
+ - accepted findings may update `.planning/security/FINDINGS.yaml` unless `--verify-only` is set
75
+ - repo-local drafts are preferred over speculative issue filing side effects
76
+ </context>
77
+
78
+ <process>
79
+ Execute the security-audit workflow from @$HOME/.config/opencode/workflows/security-audit-workflow.md end-to-end.
80
+ </process>
@@ -0,0 +1,55 @@
1
+ ---
2
+ description: Compile one canonical source into a sparse, citation-heavy project wiki page, or re-sync an existing page to the current source with --refresh
3
+ argument-hint: "<source-path> [--refresh]"
4
+ tools:
5
+ read: true
6
+ write: true
7
+ edit: true
8
+ bash: true
9
+ grep: true
10
+ glob: true
11
+ agent: true
12
+ question: true
13
+ ---
14
+ <objective>
15
+ Compile one canonical source into the repo-local project wiki under `.planning/wiki/`, or re-sync an existing summary to the current canonical source.
16
+
17
+ Output artifacts:
18
+ - `.planning/wiki/sources/<slug>.md`
19
+ - updated `.planning/wiki/index.md`
20
+ - updated `.planning/wiki/log.md`
21
+ - optional conservative updates under `.planning/wiki/topics/` or `.planning/wiki/entities/`
22
+
23
+ This is a derived-knowledge command. Raw repository docs, ADRs, tests, interfaces, and code remain canonical. The wiki must stay sparse, citation-heavy, and conservative.
24
+
25
+ Argument handling:
26
+ - accepts exactly one non-flag argument: an absolute source path or a repo-relative path inside the current repository
27
+ - optional `--refresh` flag: re-sync an existing source summary to the current canonical source instead of writing it from scratch. In refresh mode: re-read the canonical source, update only the stable facts that changed, bump `Last reviewed`, keep the page's existing structure, and append a `wiki-refresh` log entry. Use this whenever the MR/PR, code, or doc behind a page has changed.
28
+ - the source may be a repo doc, ADR, runbook, config surface, or a code file that defines a stable interface or behavior
29
+ - do not restate an entire code file into a fake duplicate wiki page; summarize only stable, reusable facts with citations
30
+ </objective>
31
+
32
+ <execution_context>
33
+ @$HOME/.config/opencode/workflows/wiki-ingest-workflow.md
34
+ </execution_context>
35
+
36
+ <context>
37
+ Arguments: $ARGUMENTS
38
+
39
+ Default behavior:
40
+ - resolve the repo root and the requested source path
41
+ - ensure `.planning/wiki/` scaffold exists
42
+ - treat the source as immutable and canonical
43
+ - write or refresh one source summary page under `.planning/wiki/sources/`
44
+ - refresh the wiki index and append a log entry
45
+ - update topic or entity pages only when the source clearly adds reusable, cross-source knowledge
46
+
47
+ Important rules:
48
+ - the wiki complements repo docs, AgentFS memory, and AnythingLLM; it does not replace them
49
+ - source summaries must preserve provenance, freshness, and contradiction notes
50
+ - when the source is code, prefer narrow interface or invariant summaries over file paraphrase
51
+ </context>
52
+
53
+ <process>
54
+ Execute the wiki-ingest workflow from @$HOME/.config/opencode/workflows/wiki-ingest-workflow.md end-to-end.
55
+ </process>
@@ -0,0 +1,51 @@
1
+ ---
2
+ description: Audit the project wiki for citation gaps, orphan pages, stale claims, contradictions, and broken cross-references; mirror contradictions into the persistent register
3
+ argument-hint: "[--focus <path>]"
4
+ tools:
5
+ read: true
6
+ write: true
7
+ edit: true
8
+ bash: true
9
+ grep: true
10
+ glob: true
11
+ agent: true
12
+ question: true
13
+ ---
14
+ <objective>
15
+ Run an advisory lint pass over `.planning/wiki/`.
16
+
17
+ Output artifacts:
18
+ - `.planning/wiki/REPORTS/<timestamp>-lint.md` (advisory snapshot)
19
+ - `.planning/wiki/CONTRADICTIONS.md` (persistent register, updated in place)
20
+ - updated `.planning/wiki/log.md`
21
+
22
+ Flag handling:
23
+ - `--focus <path>` — restrict advisory checks to one wiki page or subdirectory under `.planning/wiki/`
24
+
25
+ This is an advisory governance command. Raw repository docs, ADRs, tests, interfaces, and code remain canonical even when the wiki disagrees or is stale.
26
+
27
+ Contradictions are classified by severity (`soft` / `scope-mismatch` / `hard`) and written to the persistent `CONTRADICTIONS.md` register. Unlike the per-run REPORTS snapshot, the register is durable: lint opens, updates, and resolves entries in place so contradictions do not leak (get reported and forgotten). An entry is `resolved` only after the conflicting wiki page has actually been reconciled to the canonical source.
28
+ </objective>
29
+
30
+ <execution_context>
31
+ @$HOME/.config/opencode/workflows/wiki-lint-workflow.md
32
+ </execution_context>
33
+
34
+ <context>
35
+ Arguments: $ARGUMENTS
36
+
37
+ Default behavior:
38
+ - ensure `.planning/wiki/` scaffold exists
39
+ - inspect the wiki index, policy, log, and relevant derived pages
40
+ - check for missing citations, orphan pages, stale claims, contradicted claims, missing obvious pages, and broken cross-references
41
+ - write a durable lint report and append a log entry
42
+
43
+ Important rules:
44
+ - lint findings are advisory and must not outrank canonical raw sources
45
+ - prefer conservative findings over speculative cleanup demands
46
+ - call out contradictions and freshness uncertainty explicitly instead of rewriting them away
47
+ </context>
48
+
49
+ <process>
50
+ Execute the wiki-lint workflow from @$HOME/.config/opencode/workflows/wiki-lint-workflow.md end-to-end.
51
+ </process>
@@ -0,0 +1,50 @@
1
+ ---
2
+ description: Answer from the project wiki first, then fall back to canonical repo sources, optionally writing back a reusable answer
3
+ argument-hint: "[--writeback] <question>"
4
+ tools:
5
+ read: true
6
+ write: true
7
+ edit: true
8
+ bash: true
9
+ grep: true
10
+ glob: true
11
+ agent: true
12
+ question: true
13
+ ---
14
+ <objective>
15
+ Answer a repository question using the compiled wiki as a first-pass map, without letting the wiki outrank raw sources.
16
+
17
+ Outputs:
18
+ - chat answer
19
+ - optional `.planning/wiki/queries/<timestamp>-<slug>.md`
20
+ - updated `.planning/wiki/index.md` and `.planning/wiki/log.md` when `--writeback` is used
21
+
22
+ Flag handling:
23
+ - `--writeback` — store a reusable answer artifact under `.planning/wiki/queries/` and refresh index/log
24
+
25
+ This is a retrieval-and-synthesis command. Raw repository docs, ADRs, tests, interfaces, and code remain canonical.
26
+ </objective>
27
+
28
+ <execution_context>
29
+ @$HOME/.config/opencode/workflows/wiki-query-workflow.md
30
+ </execution_context>
31
+
32
+ <context>
33
+ Arguments: $ARGUMENTS
34
+
35
+ Default behavior:
36
+ - read `.planning/wiki/index.md` first
37
+ - inspect only relevant wiki pages next
38
+ - if wiki pages are missing, stale, contradictory, or insufficient, read the canonical repo sources directly
39
+ - answer in chat with preserved source references
40
+ - if `--writeback` is present, write a reusable answer with citations and caveats
41
+
42
+ Important rules:
43
+ - treat `.planning/wiki/` as a derived layer, not a canonical one
44
+ - preserve contradiction and freshness notes instead of smoothing them over
45
+ - when raw sources disagree with the wiki, trust the raw sources and say so explicitly
46
+ </context>
47
+
48
+ <process>
49
+ Execute the wiki-query workflow from @$HOME/.config/opencode/workflows/wiki-query-workflow.md end-to-end.
50
+ </process>
@@ -0,0 +1,188 @@
1
+ import type { Plugin } from "@opencode-ai/plugin"
2
+ import { spawn } from "node:child_process"
3
+ import fs from "node:fs"
4
+ import path from "node:path"
5
+
6
+ // OpenCode-native reimplementation of the Claude SessionEnd `memory-capture`
7
+ // hook (OCP-01, D-08). OpenCode has no `session.end` event; per 04-RESEARCH.md
8
+ // the best-fit trigger is `session.idle` (D-09), filtered to top-level
9
+ // sessions and de-duplicated so one working session stages at most one
10
+ // capture (Pitfall 2). The session transcript is obtained via the SDK
11
+ // `client.session.messages()` call (04-SPIKE-INJECTION.md confirmed shape),
12
+ // never by reading OpenCode's undocumented internal storage layout directly
13
+ // (Anti-Pattern).
14
+ //
15
+ // SERVER_ENTRY carries the install-rendered @@INFRA_ROOT@@ token (mirrors the
16
+ // Claude hook's substitution and opencode/plugins/memory-wakeup.ts's existing
17
+ // convention — the sync script, not this plugin, resolves it to the real repo
18
+ // root at install time; never accept a runtime-overridable server path here.
19
+ // T-04-03 mitigation).
20
+ //
21
+ // Fail-open everywhere (D-03): a missing server binary, missing
22
+ // .agentfs/.planning, a missing/unset env guard, or a failed subprocess call
23
+ // must never wedge an OpenCode session — the whole handler is try/catch, and
24
+ // a missing staging file after a fire-and-forget event handler is a tolerated
25
+ // degraded outcome, not a retry loop (Pitfall 3).
26
+
27
+ const SERVER_ENTRY = "@@INFRA_ROOT@@/mcp-memory-server/dist/index.js"
28
+ const MAX_CHARS = 12000
29
+ const RETENTION_CAP = 5
30
+
31
+ type SessionMessage = {
32
+ info?: { role?: string }
33
+ parts?: Array<{ type?: string; text?: string }>
34
+ }
35
+
36
+ // Runs `node <serverEntry> extract <model>` piping `input` on stdin via
37
+ // Node's own child_process API (never the plugin runtime's `$` BunShell
38
+ // helper — live verification found `$`...`.quiet().nothrow()`'s returned
39
+ // promise has no usable `.stdin` writer at runtime in this OpenCode build,
40
+ // throwing `TypeError: undefined is not an object (evaluating
41
+ // 'shellPromise.stdin.getWriter')` on every call, which silently no-oped
42
+ // capture end to end via the outer fail-open catch (OCP-06 defect, this
43
+ // phase). Async/non-blocking so a slow extraction call never freezes a live
44
+ // interactive session; bounded by `timeoutMs` and resolves (never rejects)
45
+ // so the caller's existing fail-open handling is unchanged.
46
+ function runExtract(
47
+ serverEntry: string,
48
+ model: string,
49
+ input: string,
50
+ timeoutMs = 120000,
51
+ ): Promise<{ stdout: string; stderr: string }> {
52
+ return new Promise((resolvePromise) => {
53
+ let stdout = ""
54
+ let stderr = ""
55
+ let settled = false
56
+ const finish = () => {
57
+ if (settled) return
58
+ settled = true
59
+ clearTimeout(timer)
60
+ resolvePromise({ stdout, stderr })
61
+ }
62
+
63
+ const child = spawn("node", [serverEntry, "extract", model], {
64
+ stdio: ["pipe", "pipe", "pipe"],
65
+ })
66
+ const timer = setTimeout(() => {
67
+ try {
68
+ child.kill("SIGTERM")
69
+ } catch {
70
+ // best-effort kill only
71
+ }
72
+ }, timeoutMs)
73
+
74
+ child.stdout?.on("data", (chunk: Buffer) => {
75
+ stdout += chunk.toString("utf8")
76
+ })
77
+ child.stderr?.on("data", (chunk: Buffer) => {
78
+ stderr += chunk.toString("utf8")
79
+ })
80
+ child.on("close", finish)
81
+ child.on("error", finish)
82
+
83
+ child.stdin.write(input)
84
+ child.stdin.end()
85
+ })
86
+ }
87
+
88
+ // Reimplements scripts/transcript-to-text.mjs's behavior (skip tool/reasoning/
89
+ // file noise, join user/assistant text in order, cap total length keeping the
90
+ // most recent turns) against OpenCode's { info, parts } message shape rather
91
+ // than Claude's JSONL transcript shape (Pattern 3).
92
+ function messagesToText(messages: SessionMessage[]): string {
93
+ const turns: string[] = []
94
+ for (const message of messages) {
95
+ const role = message.info?.role
96
+ if (role !== "user" && role !== "assistant") continue
97
+ const text = (message.parts ?? [])
98
+ .filter((part) => part && part.type === "text" && typeof part.text === "string")
99
+ .map((part) => part.text as string)
100
+ .join("\n")
101
+ .trim()
102
+ if (!text) continue
103
+ turns.push(`${role === "user" ? "USER" : "ASSISTANT"}: ${text}`)
104
+ }
105
+ let out = turns.join("\n\n")
106
+ if (out.length > MAX_CHARS) out = out.slice(-MAX_CHARS)
107
+ return out
108
+ }
109
+
110
+ export const MemoryCapturePlugin: Plugin = async ({ client, directory }) => {
111
+ const processed = new Set<string>()
112
+
113
+ return {
114
+ event: async ({ event }) => {
115
+ try {
116
+ if (event.type !== "session.idle") return
117
+ const sessionID = event.properties?.sessionID
118
+ if (!sessionID) return
119
+ // De-dupe: one attempt per real top-level session, no matter how
120
+ // many times session.idle fires within it (Pitfall 2).
121
+ if (processed.has(sessionID)) return
122
+
123
+ const repo = directory
124
+ const agentfsDb = path.join(repo, ".agentfs", "project.db")
125
+ if (!fs.existsSync(agentfsDb)) return
126
+ if (!fs.existsSync(SERVER_ENTRY)) return
127
+
128
+ const apiKey = process.env.CAIRN_LLM_API_KEY
129
+ const model = process.env.CAIRN_LLM_EXTRACTION_MODEL
130
+ if (!apiKey || !model) return
131
+
132
+ // session.idle's event payload only carries sessionID (no parentID) —
133
+ // fetch the full session record to filter out subagent subsessions
134
+ // (Pitfall 2: subagent tool calls also idle and would otherwise
135
+ // over-stage past the 5-session retention cap's intent).
136
+ const sessionRes = await client.session.get({ path: { id: sessionID } })
137
+ const session = (sessionRes as { data?: { parentID?: string } })?.data
138
+ if (session?.parentID) return
139
+
140
+ // Mark processed before doing the (possibly slow) extract call so a
141
+ // second session.idle fire for this session never double-attempts,
142
+ // even if this attempt ultimately yields no staged file.
143
+ processed.add(sessionID)
144
+
145
+ const messagesRes = await client.session.messages({ path: { id: sessionID } })
146
+ const messages = ((messagesRes as { data?: SessionMessage[] })?.data ?? []) as SessionMessage[]
147
+ const text = messagesToText(messages)
148
+ if (!text) return
149
+
150
+ // Pipe the session text into the shared `extract` subcommand via
151
+ // stdin (T-04-01 mitigation) — never string-interpolate arbitrary
152
+ // session/message content into the shell command.
153
+ const res = await runExtract(SERVER_ENTRY, model, text)
154
+ const candidatesJson = String(res.stdout ?? "").trim()
155
+ if (!candidatesJson) return
156
+
157
+ let parsed: { candidates?: unknown[] }
158
+ try {
159
+ parsed = JSON.parse(candidatesJson)
160
+ } catch {
161
+ return
162
+ }
163
+ if (!Array.isArray(parsed.candidates) || parsed.candidates.length === 0) return
164
+
165
+ // Stage for the next session's accept gate — identical contract to
166
+ // claude/hooks/memory-capture.sh (D-08): one file per session, same
167
+ // candidate JSON shape, same UTC timestamp filename.
168
+ const stagingDir = path.join(repo, ".planning", "memory-staging")
169
+ fs.mkdirSync(stagingDir, { recursive: true })
170
+ const ts = new Date().toISOString().replace(/[-:]/g, "").replace(/\.\d+Z$/, "Z")
171
+ const stageFile = path.join(stagingDir, `${ts}.json`)
172
+ fs.writeFileSync(stageFile, `${candidatesJson}\n`)
173
+
174
+ // Keep the staging dir bounded — drop the oldest beyond 5 sessions.
175
+ const staged = fs
176
+ .readdirSync(stagingDir)
177
+ .filter((f) => f.endsWith(".json"))
178
+ .map((f) => ({ f, mtime: fs.statSync(path.join(stagingDir, f)).mtimeMs }))
179
+ .sort((a, b) => b.mtime - a.mtime)
180
+ for (const { f } of staged.slice(RETENTION_CAP)) {
181
+ fs.unlinkSync(path.join(stagingDir, f))
182
+ }
183
+ } catch {
184
+ // Fail open — never block a session because capture failed.
185
+ }
186
+ },
187
+ }
188
+ }