@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.
- package/AGENTS.md +45 -0
- package/CONTRIBUTING.md +49 -0
- package/LICENSE +201 -0
- package/README.md +149 -0
- package/SECURITY.md +37 -0
- package/bin/cairn +73 -0
- package/claude/agents/code-reviewer.md +174 -0
- package/claude/agents/security-investigator.md +112 -0
- package/claude/agents/security-target-selector.md +90 -0
- package/claude/agents/security-validator.md +97 -0
- package/claude/agents/wiki-ingester.md +108 -0
- package/claude/agents/wiki-lint-auditor.md +94 -0
- package/claude/agents/wiki-query-analyst.md +84 -0
- package/claude/commands/context-explore.md +40 -0
- package/claude/commands/graphify.md +150 -0
- package/claude/commands/memory-review.md +85 -0
- package/claude/commands/memory-sync.md +71 -0
- package/claude/commands/recall.md +32 -0
- package/claude/commands/remember.md +43 -0
- package/claude/commands/repo-review.md +191 -0
- package/claude/commands/security-audit.md +174 -0
- package/claude/commands/wiki-ingest.md +112 -0
- package/claude/commands/wiki-lint.md +86 -0
- package/claude/commands/wiki-query.md +92 -0
- package/claude/hooks/context-explore-pretask.sh +93 -0
- package/claude/hooks/memory-capture.sh +59 -0
- package/claude/hooks/memory-recall.sh +84 -0
- package/claude/hooks/memory-wakeup.sh +45 -0
- package/docs/building-an-overlay.md +117 -0
- package/docs/domain-knowledge.md +86 -0
- package/docs/git-providers.md +36 -0
- package/docs/operating.md +366 -0
- package/mcp-memory-server/dist/embeddings.js +122 -0
- package/mcp-memory-server/dist/explore-cache.js +97 -0
- package/mcp-memory-server/dist/index.js +1256 -0
- package/mcp-memory-server/package.json +36 -0
- package/opencode/agents/code-reviewer.md +173 -0
- package/opencode/agents/security-investigator.md +111 -0
- package/opencode/agents/security-target-selector.md +89 -0
- package/opencode/agents/security-validator.md +96 -0
- package/opencode/agents/wiki-ingester.md +107 -0
- package/opencode/agents/wiki-lint-auditor.md +93 -0
- package/opencode/agents/wiki-query-analyst.md +83 -0
- package/opencode/command/code-review.md +199 -0
- package/opencode/command/context-explore.md +42 -0
- package/opencode/command/graphify.md +153 -0
- package/opencode/command/memory-review.md +44 -0
- package/opencode/command/memory-sync.md +94 -0
- package/opencode/command/recall.md +38 -0
- package/opencode/command/remember.md +46 -0
- package/opencode/command/security-audit.md +80 -0
- package/opencode/command/wiki-ingest.md +55 -0
- package/opencode/command/wiki-lint.md +51 -0
- package/opencode/command/wiki-query.md +50 -0
- package/opencode/plugins/memory-capture.ts +188 -0
- package/opencode/plugins/memory-recall.ts +132 -0
- package/opencode/plugins/memory-wakeup.ts +102 -0
- package/opencode/templates/security-finding-register.yaml.template +51 -0
- package/opencode/templates/security-issue.md.template +43 -0
- package/opencode/templates/security-policy.md.template +29 -0
- package/opencode/templates/security-report.md.template +43 -0
- package/opencode/templates/wiki-contradictions.md.template +31 -0
- package/opencode/templates/wiki-index.md.template +40 -0
- package/opencode/templates/wiki-lint-report.md.template +38 -0
- package/opencode/templates/wiki-log.md.template +11 -0
- package/opencode/templates/wiki-policy.md.template +47 -0
- package/opencode/templates/wiki-query-answer.md.template +26 -0
- package/opencode/templates/wiki-source-summary.md.template +33 -0
- package/opencode/workflows/memory-review-workflow.md +66 -0
- package/opencode/workflows/security-audit-workflow.md +197 -0
- package/opencode/workflows/wiki-ingest-workflow.md +149 -0
- package/opencode/workflows/wiki-lint-workflow.md +128 -0
- package/opencode/workflows/wiki-query-workflow.md +122 -0
- package/package.json +56 -0
- package/scripts/bootstrap.sh +99 -0
- package/scripts/doctor.sh +91 -0
- package/scripts/install-audit-timer.sh +68 -0
- package/scripts/lib/assert-tool-event.mjs +50 -0
- package/scripts/memory-store.sh +71 -0
- package/scripts/memory-wiki-audit.sh +156 -0
- package/scripts/sync-claude-assets.sh +182 -0
- package/scripts/sync-opencode-explore-assets.sh +187 -0
- package/scripts/sync-opencode-graphify-assets.sh +152 -0
- package/scripts/sync-opencode-memory-assets.sh +160 -0
- package/scripts/sync-opencode-plugin-assets.sh +163 -0
- package/scripts/sync-opencode-security-assets.sh +195 -0
- package/scripts/sync-opencode-wiki-assets.sh +250 -0
- package/scripts/test-audit-timer.sh +29 -0
- package/scripts/test-bootstrap-untracked.sh +41 -0
- package/scripts/test-cli-dispatch.sh +21 -0
- package/scripts/test-doctor.sh +32 -0
- package/scripts/test-launcher-hooks.sh +66 -0
- package/scripts/test-memory-store.sh +44 -0
- package/scripts/test-remember-recall-assertions.sh +70 -0
- package/scripts/transcript-to-text.mjs +53 -0
- package/scripts/verify-docs-parity.sh +112 -0
- package/scripts/verify-explore-maturation.sh +288 -0
- package/scripts/verify-fastcontext-reliability.sh +624 -0
- package/scripts/verify-no-private-references.sh +145 -0
- package/scripts/verify-opencode-live-parity.sh +864 -0
- package/scripts/verify-routing-seam.sh +169 -0
- package/scripts/verify-token-savings-ab.sh +557 -0
- package/templates/PROJECT-INSTRUCTIONS.md.template +42 -0
- package/templates/alignment-gap-register.yaml.template +56 -0
- package/templates/alignment-policy.md.template +28 -0
- package/templates/alignment-pr-template.md.template +18 -0
- package/templates/alignment-report.md.template +56 -0
- package/templates/env.example.template +25 -0
- package/templates/graph-policy.md.template +37 -0
- package/templates/graphs-gitignore.template +3 -0
- package/templates/memory-wiki-audit.service.template +6 -0
- package/templates/memory-wiki-audit.timer.template +9 -0
- package/templates/memory.json.template +4 -0
- package/templates/planning-config.json.template +6 -0
- package/templates/project-brief.md.template +40 -0
- package/templates/security-finding-register.yaml.template +51 -0
- package/templates/security-issue.md.template +43 -0
- package/templates/security-policy.md.template +29 -0
- package/templates/security-report.md.template +43 -0
- package/templates/start-claude.sh.template +46 -0
- package/templates/start-opencode.sh.template +41 -0
- package/templates/structured-change-canvas.md.template +63 -0
- package/templates/wiki-contradictions.md.template +31 -0
- package/templates/wiki-index.md.template +40 -0
- package/templates/wiki-lint-report.md.template +38 -0
- package/templates/wiki-log.md.template +11 -0
- package/templates/wiki-policy.md.template +47 -0
- package/templates/wiki-query-answer.md.template +26 -0
- 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
|
+
}
|