context-mode 1.0.111 → 1.0.112

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 (150) hide show
  1. package/.claude-plugin/marketplace.json +2 -2
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/.openclaw-plugin/index.ts +3 -2
  4. package/.openclaw-plugin/openclaw.plugin.json +1 -1
  5. package/.openclaw-plugin/package.json +1 -1
  6. package/README.md +152 -34
  7. package/bin/statusline.mjs +144 -127
  8. package/build/adapters/base.d.ts +8 -5
  9. package/build/adapters/base.js +8 -18
  10. package/build/adapters/claude-code/index.d.ts +24 -3
  11. package/build/adapters/claude-code/index.js +44 -11
  12. package/build/adapters/codex/hooks.d.ts +10 -5
  13. package/build/adapters/codex/hooks.js +10 -5
  14. package/build/adapters/codex/index.d.ts +17 -5
  15. package/build/adapters/codex/index.js +337 -37
  16. package/build/adapters/codex/paths.d.ts +1 -0
  17. package/build/adapters/codex/paths.js +12 -0
  18. package/build/adapters/cursor/index.d.ts +6 -0
  19. package/build/adapters/cursor/index.js +83 -2
  20. package/build/adapters/detect.d.ts +1 -1
  21. package/build/adapters/detect.js +29 -6
  22. package/build/adapters/omp/index.d.ts +65 -0
  23. package/build/adapters/omp/index.js +182 -0
  24. package/build/adapters/omp/plugin.d.ts +75 -0
  25. package/build/adapters/omp/plugin.js +220 -0
  26. package/build/adapters/openclaw/mcp-tools.d.ts +54 -0
  27. package/build/adapters/openclaw/mcp-tools.js +198 -0
  28. package/build/adapters/openclaw/plugin.d.ts +130 -0
  29. package/build/adapters/openclaw/plugin.js +629 -0
  30. package/build/adapters/openclaw/workspace-router.d.ts +29 -0
  31. package/build/adapters/openclaw/workspace-router.js +64 -0
  32. package/build/adapters/opencode/plugin.d.ts +145 -0
  33. package/build/adapters/opencode/plugin.js +457 -0
  34. package/build/adapters/pi/extension.d.ts +26 -0
  35. package/build/adapters/pi/extension.js +552 -0
  36. package/build/adapters/pi/index.d.ts +57 -0
  37. package/build/adapters/pi/index.js +173 -0
  38. package/build/adapters/pi/mcp-bridge.d.ts +113 -0
  39. package/build/adapters/pi/mcp-bridge.js +251 -0
  40. package/build/adapters/types.d.ts +11 -6
  41. package/build/cli.js +186 -170
  42. package/build/db-base.d.ts +15 -2
  43. package/build/db-base.js +50 -5
  44. package/build/executor.d.ts +2 -0
  45. package/build/executor.js +15 -2
  46. package/build/runPool.d.ts +36 -0
  47. package/build/runPool.js +51 -0
  48. package/build/runtime.js +64 -5
  49. package/build/search/auto-memory.js +6 -4
  50. package/build/security.js +30 -10
  51. package/build/server.d.ts +23 -1
  52. package/build/server.js +652 -174
  53. package/build/session/analytics.d.ts +404 -1
  54. package/build/session/analytics.js +1347 -42
  55. package/build/session/db.d.ts +114 -5
  56. package/build/session/db.js +275 -27
  57. package/build/session/event-emit.d.ts +48 -0
  58. package/build/session/event-emit.js +101 -0
  59. package/build/session/extract.d.ts +1 -0
  60. package/build/session/extract.js +79 -12
  61. package/build/session/purge.d.ts +111 -0
  62. package/build/session/purge.js +138 -0
  63. package/build/store.d.ts +7 -0
  64. package/build/store.js +69 -6
  65. package/build/util/claude-config.d.ts +26 -0
  66. package/build/util/claude-config.js +91 -0
  67. package/build/util/hook-config.d.ts +4 -0
  68. package/build/util/hook-config.js +39 -0
  69. package/cli.bundle.mjs +411 -208
  70. package/configs/antigravity/GEMINI.md +0 -3
  71. package/configs/claude-code/CLAUDE.md +1 -4
  72. package/configs/codex/AGENTS.md +1 -4
  73. package/configs/codex/config.toml +3 -0
  74. package/configs/codex/hooks.json +8 -0
  75. package/configs/cursor/context-mode.mdc +0 -3
  76. package/configs/gemini-cli/GEMINI.md +0 -3
  77. package/configs/jetbrains-copilot/copilot-instructions.md +0 -3
  78. package/configs/kilo/AGENTS.md +0 -3
  79. package/configs/kiro/KIRO.md +0 -3
  80. package/configs/omp/SYSTEM.md +85 -0
  81. package/configs/omp/mcp.json +7 -0
  82. package/configs/openclaw/AGENTS.md +0 -3
  83. package/configs/opencode/AGENTS.md +0 -3
  84. package/configs/pi/AGENTS.md +0 -3
  85. package/configs/qwen-code/QWEN.md +1 -4
  86. package/configs/vscode-copilot/copilot-instructions.md +0 -3
  87. package/configs/zed/AGENTS.md +0 -3
  88. package/hooks/codex/posttooluse.mjs +9 -2
  89. package/hooks/codex/precompact.mjs +69 -0
  90. package/hooks/codex/sessionstart.mjs +13 -9
  91. package/hooks/codex/stop.mjs +1 -2
  92. package/hooks/codex/userpromptsubmit.mjs +1 -2
  93. package/hooks/core/routing.mjs +237 -18
  94. package/hooks/cursor/afteragentresponse.mjs +1 -1
  95. package/hooks/cursor/hooks.json +31 -0
  96. package/hooks/cursor/posttooluse.mjs +1 -1
  97. package/hooks/cursor/sessionstart.mjs +5 -5
  98. package/hooks/cursor/stop.mjs +1 -1
  99. package/hooks/ensure-deps.mjs +12 -13
  100. package/hooks/gemini-cli/aftertool.mjs +1 -1
  101. package/hooks/gemini-cli/beforeagent.mjs +1 -1
  102. package/hooks/gemini-cli/precompress.mjs +3 -2
  103. package/hooks/gemini-cli/sessionstart.mjs +9 -9
  104. package/hooks/jetbrains-copilot/posttooluse.mjs +1 -1
  105. package/hooks/jetbrains-copilot/precompact.mjs +3 -2
  106. package/hooks/jetbrains-copilot/sessionstart.mjs +9 -9
  107. package/hooks/kiro/agentspawn.mjs +5 -5
  108. package/hooks/kiro/posttooluse.mjs +2 -2
  109. package/hooks/kiro/userpromptsubmit.mjs +1 -1
  110. package/hooks/posttooluse.mjs +45 -0
  111. package/hooks/precompact.mjs +17 -0
  112. package/hooks/pretooluse.mjs +23 -0
  113. package/hooks/routing-block.mjs +0 -12
  114. package/hooks/run-hook.mjs +16 -3
  115. package/hooks/session-db.bundle.mjs +27 -18
  116. package/hooks/session-extract.bundle.mjs +2 -2
  117. package/hooks/session-helpers.mjs +101 -64
  118. package/hooks/sessionstart.mjs +51 -2
  119. package/hooks/vscode-copilot/posttooluse.mjs +1 -1
  120. package/hooks/vscode-copilot/precompact.mjs +3 -2
  121. package/hooks/vscode-copilot/sessionstart.mjs +9 -9
  122. package/openclaw.plugin.json +1 -1
  123. package/package.json +14 -8
  124. package/server.bundle.mjs +349 -147
  125. package/skills/UPSTREAM-CREDITS.md +0 -51
  126. package/skills/context-mode-ops/SKILL.md +0 -299
  127. package/skills/context-mode-ops/agent-teams.md +0 -198
  128. package/skills/context-mode-ops/communication.md +0 -224
  129. package/skills/context-mode-ops/marketing.md +0 -124
  130. package/skills/context-mode-ops/release.md +0 -214
  131. package/skills/context-mode-ops/review-pr.md +0 -269
  132. package/skills/context-mode-ops/tdd.md +0 -329
  133. package/skills/context-mode-ops/triage-issue.md +0 -266
  134. package/skills/context-mode-ops/validation.md +0 -307
  135. package/skills/diagnose/SKILL.md +0 -122
  136. package/skills/diagnose/scripts/hitl-loop.template.sh +0 -41
  137. package/skills/grill-me/SKILL.md +0 -15
  138. package/skills/grill-with-docs/ADR-FORMAT.md +0 -47
  139. package/skills/grill-with-docs/CONTEXT-FORMAT.md +0 -77
  140. package/skills/grill-with-docs/SKILL.md +0 -93
  141. package/skills/improve-codebase-architecture/DEEPENING.md +0 -37
  142. package/skills/improve-codebase-architecture/INTERFACE-DESIGN.md +0 -44
  143. package/skills/improve-codebase-architecture/LANGUAGE.md +0 -53
  144. package/skills/improve-codebase-architecture/SKILL.md +0 -76
  145. package/skills/tdd/SKILL.md +0 -114
  146. package/skills/tdd/deep-modules.md +0 -33
  147. package/skills/tdd/interface-design.md +0 -31
  148. package/skills/tdd/mocking.md +0 -59
  149. package/skills/tdd/refactoring.md +0 -10
  150. package/skills/tdd/tests.md +0 -61
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Extract the agent workspace path from tool call params.
3
+ * Looks for /openclaw/workspace-<name> patterns in cwd, file_path, and command.
4
+ * Returns the workspace root (e.g. "/openclaw/workspace-trainer") or null.
5
+ */
6
+ export function extractWorkspace(params) {
7
+ // Priority: cwd > file_path > command (most specific first)
8
+ const sources = [
9
+ params.cwd,
10
+ params.file_path,
11
+ params.command,
12
+ ].filter((v) => typeof v === "string");
13
+ for (const src of sources) {
14
+ const match = src.match(/\/openclaw\/workspace-[a-zA-Z0-9_-]+/);
15
+ if (match)
16
+ return match[0];
17
+ }
18
+ return null;
19
+ }
20
+ /**
21
+ * Maps agent workspaces to sessionIds using sessionKey convention.
22
+ * sessionKey pattern: "agent:<name>:main" → workspace "/openclaw/workspace-<name>"
23
+ *
24
+ * Why this exists alongside per-session closures:
25
+ * Each register() call creates its own closure with its own sessionId, which
26
+ * naturally isolates sessions. The WorkspaceRouter acts as a safety net for
27
+ * after_tool_call events where OpenClaw may deliver the event to the wrong
28
+ * closure (e.g. tool calls interleaving across agents). It resolves the correct
29
+ * sessionId from workspace paths in tool params, falling back to the closure
30
+ * sessionId when no workspace is detected.
31
+ */
32
+ export class WorkspaceRouter {
33
+ // workspace path → sessionId
34
+ map = new Map();
35
+ /** Register a session from session_start event. */
36
+ registerSession(sessionKey, sessionId) {
37
+ const workspace = this.workspaceFromKey(sessionKey);
38
+ if (workspace) {
39
+ this.map.set(workspace, sessionId);
40
+ }
41
+ }
42
+ /** Remove a session (e.g. on command:stop). */
43
+ removeSession(sessionKey) {
44
+ const workspace = this.workspaceFromKey(sessionKey);
45
+ if (workspace) {
46
+ this.map.delete(workspace);
47
+ }
48
+ }
49
+ /** Resolve sessionId from tool call params. Returns null if no match. */
50
+ resolveSessionId(params) {
51
+ const workspace = extractWorkspace(params);
52
+ if (!workspace)
53
+ return null;
54
+ return this.map.get(workspace) ?? null;
55
+ }
56
+ /** Derive workspace path from sessionKey. */
57
+ workspaceFromKey(key) {
58
+ // Pattern: "agent:<name>:main" or "agent:<name>:<channel>"
59
+ const match = key.match(/^agent:([^:]+):/);
60
+ if (!match)
61
+ return null;
62
+ return `/openclaw/workspace-${match[1]}`;
63
+ }
64
+ }
@@ -0,0 +1,145 @@
1
+ /**
2
+ * OpenCode / KiloCode TypeScript plugin entry point for context-mode.
3
+ *
4
+ * Provides five hooks (v1.0.107 — Mickey OC-1..OC-4 follow-up):
5
+ * - tool.execute.before — Routing enforcement (deny/modify/passthrough)
6
+ * - tool.execute.after — Session event capture + first-fire AGENTS.md scan (OC-4)
7
+ * - experimental.session.compacting — Compaction snapshot + budget-capped auto-injection (OC-3)
8
+ * - experimental.chat.system.transform — ROUTING_BLOCK + resume snapshot injection (OC-1)
9
+ * - chat.message — User-prompt capture w/ CCv2 inline filter (OC-2) + AGENTS.md scan (OC-4)
10
+ *
11
+ * KiloCode loads this via: import("context-mode") → expects default export
12
+ * with shape { server: (input) => Promise<Hooks> } (PluginModule).
13
+ *
14
+ * OpenCode loads this via: import("context-mode/plugin") → also supports
15
+ * the named export ContextModePlugin for backward compat.
16
+ *
17
+ * Constraints:
18
+ * - No SessionStart hook (OpenCode doesn't support it — #14808, #5409)
19
+ * - context injection now via chat.system.transform surrogate (OC-1)
20
+ * - No routing file auto-write (avoid dirtying project trees)
21
+ * - Session cleanup happens at plugin init (no SessionStart)
22
+ */
23
+ /** KiloCode/OpenCode plugin input — both platforms pass at least `directory`. */
24
+ type PluginClientAppLogBodyExtra = {
25
+ sessionId?: string;
26
+ source?: string;
27
+ };
28
+ type PluginClientAppLogBody = {
29
+ service: string;
30
+ level: "info" | "warn" | "error" | "debug";
31
+ message: string;
32
+ extra?: PluginClientAppLogBodyExtra;
33
+ };
34
+ type PluginClientAppLogOptions = {
35
+ body: PluginClientAppLogBody;
36
+ };
37
+ type PluginClientApp = {
38
+ log: (options: PluginClientAppLogOptions) => Promise<void>;
39
+ };
40
+ type PluginClient = {
41
+ app: PluginClientApp;
42
+ };
43
+ type PluginContext = {
44
+ client: PluginClient;
45
+ directory: string;
46
+ };
47
+ /** OpenCode tool.execute.before — first parameter */
48
+ interface BeforeHookInput {
49
+ tool: string;
50
+ sessionID: string;
51
+ callID: string;
52
+ }
53
+ /** OpenCode tool.execute.before — second parameter */
54
+ interface BeforeHookOutput {
55
+ args: any;
56
+ }
57
+ /** OpenCode tool.execute.after — first parameter */
58
+ interface AfterHookInput {
59
+ tool: string;
60
+ sessionID: string;
61
+ callID: string;
62
+ args: any;
63
+ }
64
+ /** OpenCode tool.execute.after — second parameter */
65
+ interface AfterHookOutput {
66
+ title: string;
67
+ output: string;
68
+ metadata: any;
69
+ }
70
+ /** OpenCode experimental.session.compacting — first parameter */
71
+ interface CompactingHookInput {
72
+ sessionID: string;
73
+ }
74
+ /** OpenCode experimental.session.compacting — second parameter */
75
+ interface CompactingHookOutput {
76
+ context: string[];
77
+ prompt?: string;
78
+ }
79
+ /**
80
+ * OpenCode experimental.chat.system.transform — first parameter.
81
+ * Verified against sst/opencode/dev/packages/plugin/src/index.ts:
82
+ * input: { sessionID?: string; model: Model }
83
+ * `sessionID` is optional in the SDK type but is in practice always set
84
+ * (the transform runs *for* a session). We treat it as required and
85
+ * skip injection when absent rather than fall back to a fabricated ID.
86
+ *
87
+ * NOTE: We deliberately do NOT use `experimental.chat.messages.transform`.
88
+ * Its SDK input shape is `{}` (no sessionID) and its output is
89
+ * `{ messages: { info: Message; parts: Part[] }[] }` — the prior code
90
+ * (`output.messages.unshift({ role, content })`) wrote a value of the
91
+ * wrong shape and was silently dropped (Mickey / PR #376 root cause).
92
+ */
93
+ interface SystemTransformHookInput {
94
+ sessionID?: string;
95
+ model: unknown;
96
+ }
97
+ /** OpenCode experimental.chat.system.transform — second parameter */
98
+ interface SystemTransformHookOutput {
99
+ system: string[];
100
+ }
101
+ /**
102
+ * OpenCode chat.message hook — verified against
103
+ * refs/platforms/opencode/packages/plugin/src/index.ts:233.
104
+ * input: { sessionID; agent?; model?; messageID?; variant? }
105
+ * output: { message: UserMessage; parts: Part[] }
106
+ * We read text from `parts[*].text` (the orchestrator reference at
107
+ * refs/plugin-examples/opencode/opencode-orchestrator/src/plugin-handlers/
108
+ * chat-message-handler.ts:41-65 uses the same pattern).
109
+ */
110
+ interface ChatMessageHookInput {
111
+ sessionID: string;
112
+ agent?: string;
113
+ messageID?: string;
114
+ }
115
+ interface ChatMessagePart {
116
+ type: string;
117
+ text?: string;
118
+ }
119
+ interface ChatMessageHookOutput {
120
+ message: unknown;
121
+ parts: ChatMessagePart[];
122
+ }
123
+ declare const ROUTING_MARKERS: string[];
124
+ declare function systemHasRoutingInstructions(system: string[]): boolean;
125
+ /**
126
+ * Plugin factory. Called once when KiloCode/OpenCode loads the plugin.
127
+ * Returns an object mapping hook event names to async handler functions.
128
+ *
129
+ * KiloCode expects: export default { id: string, server: (input) => Promise<Hooks> }
130
+ * OpenCode expects: export const ContextModePlugin = (ctx) => Promise<Hooks>
131
+ */
132
+ declare function createContextModePlugin(ctx: PluginContext): Promise<{
133
+ "tool.execute.before": (input: BeforeHookInput, output: BeforeHookOutput) => Promise<void>;
134
+ "tool.execute.after": (input: AfterHookInput, output: AfterHookOutput) => Promise<void>;
135
+ "chat.message": (input: ChatMessageHookInput, output: ChatMessageHookOutput) => Promise<void>;
136
+ "experimental.session.compacting": (input: CompactingHookInput, output: CompactingHookOutput) => Promise<string>;
137
+ "experimental.chat.system.transform": (input: SystemTransformHookInput, output: SystemTransformHookOutput) => Promise<void>;
138
+ }>;
139
+ declare const _default: {
140
+ id: string;
141
+ server: typeof createContextModePlugin;
142
+ };
143
+ export default _default;
144
+ export { createContextModePlugin as ContextModePlugin };
145
+ export { systemHasRoutingInstructions, ROUTING_MARKERS };
@@ -0,0 +1,457 @@
1
+ /**
2
+ * OpenCode / KiloCode TypeScript plugin entry point for context-mode.
3
+ *
4
+ * Provides five hooks (v1.0.107 — Mickey OC-1..OC-4 follow-up):
5
+ * - tool.execute.before — Routing enforcement (deny/modify/passthrough)
6
+ * - tool.execute.after — Session event capture + first-fire AGENTS.md scan (OC-4)
7
+ * - experimental.session.compacting — Compaction snapshot + budget-capped auto-injection (OC-3)
8
+ * - experimental.chat.system.transform — ROUTING_BLOCK + resume snapshot injection (OC-1)
9
+ * - chat.message — User-prompt capture w/ CCv2 inline filter (OC-2) + AGENTS.md scan (OC-4)
10
+ *
11
+ * KiloCode loads this via: import("context-mode") → expects default export
12
+ * with shape { server: (input) => Promise<Hooks> } (PluginModule).
13
+ *
14
+ * OpenCode loads this via: import("context-mode/plugin") → also supports
15
+ * the named export ContextModePlugin for backward compat.
16
+ *
17
+ * Constraints:
18
+ * - No SessionStart hook (OpenCode doesn't support it — #14808, #5409)
19
+ * - context injection now via chat.system.transform surrogate (OC-1)
20
+ * - No routing file auto-write (avoid dirtying project trees)
21
+ * - Session cleanup happens at plugin init (no SessionStart)
22
+ */
23
+ import { dirname, resolve, join } from "node:path";
24
+ import { fileURLToPath, pathToFileURL } from "node:url";
25
+ import { existsSync, readFileSync } from "node:fs";
26
+ import { resolveSessionDbPath, SessionDB } from "../../session/db.js";
27
+ import { extractEvents, extractUserEvents } from "../../session/extract.js";
28
+ import { buildResumeSnapshot } from "../../session/snapshot.js";
29
+ import { OpenCodeAdapter } from "./index.js";
30
+ import { PLATFORM_ENV_VARS } from "../detect.js";
31
+ // Read package.json version once at module load (not on every hook call).
32
+ // Used in the resume-injection visible signal so users can confirm in
33
+ // OPENCODE_DEBUG logs which plugin version actually injected.
34
+ const VERSION = (() => {
35
+ try {
36
+ const pkgRoot = dirname(fileURLToPath(import.meta.url));
37
+ // Search both the legacy depths (when bundled flat under build/) and
38
+ // the post-refactor depths (when compiled to build/adapters/opencode/).
39
+ // `../../../package.json` is the canonical location after the
40
+ // `src/opencode-plugin.ts → src/adapters/opencode/plugin.ts` move.
41
+ for (const rel of ["../../../package.json", "../package.json", "./package.json"]) {
42
+ const p = resolve(pkgRoot, rel);
43
+ if (existsSync(p))
44
+ return JSON.parse(readFileSync(p, "utf8")).version ?? "unknown";
45
+ }
46
+ }
47
+ catch { /* fall through */ }
48
+ return "unknown";
49
+ })();
50
+ // Synthetic message tags emitted by harnesses (CCv2 inline filter). When the
51
+ // user "message" is actually a system-generated nudge (e.g. tool-result, system
52
+ // reminder), capturing it as user_prompt would flood the DB with noise.
53
+ const SYNTHETIC_MESSAGE_PREFIXES = [
54
+ "<task-notification>",
55
+ "<system-reminder>",
56
+ "<context_guidance>",
57
+ "<tool-result>",
58
+ ];
59
+ function isSyntheticMessage(text) {
60
+ const trimmed = text.trim();
61
+ return SYNTHETIC_MESSAGE_PREFIXES.some((p) => trimmed.startsWith(p));
62
+ }
63
+ // ── Helpers ───────────────────────────────────────────────
64
+ // Quorum markers — must NOT be substrings of each other (#487).
65
+ // Each token uniquely identifies the routing block / context-mode rules
66
+ // without overlapping any other marker. The XML tag is the primary signal;
67
+ // the two distinctive bare tool names are the secondary signals. Together
68
+ // any 2 of 3 confirm the system prompt already carries routing instructions.
69
+ const ROUTING_MARKERS = [
70
+ "<context_window_protection>",
71
+ "ctx_search",
72
+ "ctx_index",
73
+ ];
74
+ function systemHasRoutingInstructions(system) {
75
+ const text = system.join("\n");
76
+ // Word-boundary check guards against unrelated identifiers that happen to
77
+ // share a prefix/suffix (e.g. a hypothetical `ctx_search_v2`).
78
+ const wordBoundary = (m) => {
79
+ if (m.startsWith("<"))
80
+ return text.includes(m);
81
+ const re = new RegExp(`(?:^|\\W)${m.replace(/[.*+?^${}()|[\\]\\\\]/g, "\\$&")}(?:\\W|$)`);
82
+ return re.test(text);
83
+ };
84
+ return ROUTING_MARKERS.filter(wordBoundary).length >= 2;
85
+ }
86
+ /**
87
+ * Detect whether the plugin is running under KiloCode or OpenCode.
88
+ *
89
+ * Reuses the canonical PLATFORM_ENV_VARS list (src/adapters/detect.ts) instead
90
+ * of hardcoding env var names — single source of truth, future-proof if Kilo
91
+ * or OpenCode add/rename env vars upstream.
92
+ *
93
+ * Order matters: KiloCode is an OpenCode fork and sets `OPENCODE=1` in
94
+ * addition to `KILO_PID`. PLATFORM_ENV_VARS lists `kilo` BEFORE `opencode`
95
+ * so KILO_PID wins the iteration.
96
+ *
97
+ * Pre-fix version was `return process.env.KILO_PID ? "kilo" : "opencode";` —
98
+ * surfaced by github.com/mksglu/context-mode/pull/376 (mikij). Full symmetric
99
+ * fix: also actively check opencode env vars instead of blind fallback.
100
+ */
101
+ function getPlatform() {
102
+ for (const [platform, vars] of PLATFORM_ENV_VARS) {
103
+ if (platform !== "kilo" && platform !== "opencode")
104
+ continue;
105
+ if (vars.some((v) => process.env[v])) {
106
+ return platform;
107
+ }
108
+ }
109
+ // Plugin host should always set one of the env vars. Fallback to opencode
110
+ // (the wider ecosystem) when neither is set, for predictable behavior.
111
+ return "opencode";
112
+ }
113
+ // ── Plugin Factory ────────────────────────────────────────
114
+ /**
115
+ * Plugin factory. Called once when KiloCode/OpenCode loads the plugin.
116
+ * Returns an object mapping hook event names to async handler functions.
117
+ *
118
+ * KiloCode expects: export default { id: string, server: (input) => Promise<Hooks> }
119
+ * OpenCode expects: export const ContextModePlugin = (ctx) => Promise<Hooks>
120
+ */
121
+ async function createContextModePlugin(ctx) {
122
+ // Resolve build dir from compiled JS location
123
+ const platform = getPlatform();
124
+ const adapter = new OpenCodeAdapter(platform);
125
+ const buildDir = dirname(fileURLToPath(import.meta.url));
126
+ // initSecurity() looks for `<dir>/security.js`, which lives at the
127
+ // top of build/ — two levels up from this adapter directory.
128
+ const buildRoot = resolve(buildDir, "..", "..");
129
+ // Load routing module (ESM .mjs, lives outside build/ in hooks/)
130
+ const routingPath = resolve(buildDir, "..", "..", "..", "hooks", "core", "routing.mjs");
131
+ const routing = await import(pathToFileURL(routingPath).href);
132
+ await routing.initSecurity(buildRoot);
133
+ // OC-1 / OC-3: Load hook helpers once at plugin init. Dynamic import keeps
134
+ // the .mjs ESM islands isolated from the .ts compile graph.
135
+ const routingBlockPath = resolve(buildDir, "..", "..", "..", "hooks", "routing-block.mjs");
136
+ const routingBlockMod = await import(pathToFileURL(routingBlockPath).href);
137
+ const toolNamingPath = resolve(buildDir, "..", "..", "..", "hooks", "core", "tool-naming.mjs");
138
+ const toolNamingMod = await import(pathToFileURL(toolNamingPath).href);
139
+ const autoInjectionPath = resolve(buildDir, "..", "..", "..", "hooks", "auto-injection.mjs");
140
+ const autoInjectionMod = await import(pathToFileURL(autoInjectionPath).href);
141
+ // Pre-build the routing block once per process — it is platform-specific
142
+ // (tool naming differs between opencode and kilo) but does NOT depend on
143
+ // sessionID, so we cache it. createToolNamer accepts both "opencode" and
144
+ // "kilo" per hooks/core/tool-naming.mjs:25-26.
145
+ const toolNamer = toolNamingMod.createToolNamer(platform);
146
+ const routingBlock = routingBlockMod.createRoutingBlock(toolNamer);
147
+ // Initialize per-process state. We do NOT fabricate a sessionId here —
148
+ // OpenCode/Kilo provide the real `input.sessionID` on every hook, and a
149
+ // process-global UUID would (a) never match prior-session resume rows and
150
+ // (b) collide across multi-session reuse (Mickey / PR #376 root cause).
151
+ const projectDir = ctx?.directory ?? process.cwd();
152
+ // C2 narrowing: resolve DB path through the canonical helper directly.
153
+ // BaseAdapter no longer exposes getSessionDBPath; the adapter only owns
154
+ // the sessions DIR (per-platform), the helper owns the per-project FILE
155
+ // (case-fold + worktree-suffix + one-shot legacy migration).
156
+ const db = new SessionDB({
157
+ dbPath: resolveSessionDbPath({ projectDir, sessionsDir: adapter.getSessionDir() }),
158
+ });
159
+ // Clean up old sessions on startup (no SessionStart hook to do this).
160
+ db.cleanupOldSessions(7);
161
+ // OC-4 (#487 follow-up): per-session capture gate. PR #487 trusted the host
162
+ // to deliver AGENTS.md events, but OpenCode only fires `rule_content` events
163
+ // when the user explicitly reads the file. snapshot.ts:172 + analytics.ts:152
164
+ // CONSUME `rule_content` to render rules into the resume snapshot — without
165
+ // this capture path, AGENTS.md is silently absent from continuity output.
166
+ // Keyed by sessionId (NOT projectDir) so multi-session reuse within a long-
167
+ // lived plugin process still gets per-session capture exactly once.
168
+ const agentsMdCaptured = new Set();
169
+ /**
170
+ * OC-4: Read AGENTS.md (with CLAUDE.md / CONTEXT.md fallbacks) from the
171
+ * project directory and persist as `rule` + `rule_content` events. Mirrors
172
+ * the CC SessionStart pattern at hooks/sessionstart.mjs:121-132 and the
173
+ * OpenCode instruction.ts FILES order. Idempotent via `agentsMdCaptured`
174
+ * Set keyed by sessionId. Fail-soft: missing/unreadable files do not throw.
175
+ */
176
+ function captureAgentsMd(sessionId) {
177
+ if (agentsMdCaptured.has(sessionId))
178
+ return;
179
+ agentsMdCaptured.add(sessionId);
180
+ const candidates = ["AGENTS.md", "CLAUDE.md", "CONTEXT.md"];
181
+ for (const name of candidates) {
182
+ try {
183
+ const p = join(projectDir, name);
184
+ if (!existsSync(p))
185
+ continue;
186
+ const content = readFileSync(p, "utf-8");
187
+ if (!content.trim())
188
+ continue;
189
+ db.insertEvent(sessionId, {
190
+ type: "rule",
191
+ category: "rule",
192
+ data: p,
193
+ priority: 1,
194
+ }, "PluginInit");
195
+ db.insertEvent(sessionId, {
196
+ type: "rule_content",
197
+ category: "rule",
198
+ data: content,
199
+ priority: 1,
200
+ }, "PluginInit");
201
+ }
202
+ catch {
203
+ // file missing or unreadable — skip silently
204
+ }
205
+ }
206
+ }
207
+ function logger(message = "context-mode debug log", extra) {
208
+ return ctx.client.app.log({
209
+ body: {
210
+ service: "context-mode-logger",
211
+ level: "info",
212
+ message,
213
+ extra,
214
+ },
215
+ });
216
+ }
217
+ /**
218
+ * Drop-in wrapper for `logger` that NEVER rejects (#448).
219
+ *
220
+ * The OPENCODE_DEBUG branch awaits `logger(...)` from inside the chat-turn
221
+ * hot path (chat.system.transform). If `ctx.client.app.log` rejects —
222
+ * transport error, closed stream, oversized payload — the promise rejection
223
+ * propagates back to OpenCode core and can break the turn. Debug logging
224
+ * is best-effort; swallow errors silently and let the turn proceed.
225
+ */
226
+ async function safeLog(message, extra) {
227
+ try {
228
+ await logger(message, extra);
229
+ }
230
+ catch {
231
+ // Never break the turn on debug-log failure.
232
+ }
233
+ }
234
+ return {
235
+ // ── PreToolUse: Routing enforcement ─────────────────
236
+ "tool.execute.before": async (input, output) => {
237
+ const toolName = input.tool ?? "";
238
+ const toolInput = output.args ?? {};
239
+ let decision;
240
+ try {
241
+ decision = routing.routePreToolUse(toolName, toolInput, projectDir, platform);
242
+ }
243
+ catch {
244
+ return; // Routing failure → allow passthrough
245
+ }
246
+ if (!decision)
247
+ return; // No routing match → passthrough
248
+ if (decision.action === "deny" || decision.action === "ask") {
249
+ // Throw to block — OpenCode catches this and denies the tool call
250
+ throw new Error(decision.reason ?? "Blocked by context-mode");
251
+ }
252
+ if (decision.action === "modify" && decision.updatedInput) {
253
+ // Mutate output.args — OpenCode reads the mutated output object
254
+ Object.assign(output.args, decision.updatedInput);
255
+ }
256
+ if (decision.action === "context" && decision.additionalContext) {
257
+ // Mutate output.args — OpenCode reads the mutated output object
258
+ output.args.additionalContext = decision.additionalContext;
259
+ }
260
+ },
261
+ // ── PostToolUse: Session event capture ──────────────
262
+ "tool.execute.after": async (input, output) => {
263
+ const sessionId = input.sessionID;
264
+ if (!sessionId)
265
+ return;
266
+ try {
267
+ db.ensureSession(sessionId, projectDir);
268
+ // OC-4 (#487 follow-up): AGENTS.md → rule_content capture for snapshot
269
+ // and auto-memory parity. Idempotent per-session via Set guard.
270
+ captureAgentsMd(sessionId);
271
+ const hookInput = {
272
+ tool_name: input.tool ?? "",
273
+ tool_input: input.args ?? {},
274
+ tool_response: output.output,
275
+ tool_output: undefined, // OpenCode doesn't provide isError
276
+ };
277
+ const events = extractEvents(hookInput);
278
+ for (const event of events) {
279
+ // Cast: extract.ts SessionEvent lacks data_hash (computed by insertEvent)
280
+ db.insertEvent(sessionId, event, "PostToolUse");
281
+ }
282
+ }
283
+ catch {
284
+ // Silent — session capture must never break the tool call
285
+ }
286
+ },
287
+ // ── chat.message: User-prompt capture (OC-2 / Z2) ───
288
+ // SDK signature verified at refs/platforms/opencode/packages/plugin/src/
289
+ // index.ts:233. Orchestrator reference at refs/plugin-examples/opencode/
290
+ // opencode-orchestrator/src/plugin-handlers/chat-message-handler.ts:41-65.
291
+ // CCv2 inline filter: skip synthetic harness messages (system reminders,
292
+ // tool results, etc.) so we don't pollute the user-prompt event stream.
293
+ "chat.message": async (input, output) => {
294
+ const sessionId = input?.sessionID;
295
+ if (!sessionId)
296
+ return;
297
+ try {
298
+ const parts = Array.isArray(output?.parts) ? output.parts : [];
299
+ const textPart = parts.find((p) => p && p.type === "text" && typeof p.text === "string" && p.text.length > 0);
300
+ if (!textPart || !textPart.text)
301
+ return;
302
+ const message = textPart.text;
303
+ if (isSyntheticMessage(message))
304
+ return;
305
+ db.ensureSession(sessionId, projectDir);
306
+ // OC-4 (#487 follow-up): also capture on chat.message so sessions that
307
+ // never invoke a tool still seed rule_content events for continuity.
308
+ captureAgentsMd(sessionId);
309
+ // 1. Always save the raw prompt
310
+ db.insertEvent(sessionId, {
311
+ type: "user_prompt",
312
+ category: "user-prompt",
313
+ data: message,
314
+ priority: 1,
315
+ }, "UserPromptSubmit");
316
+ // 2. Extract role/decision/intent/skill events from the prompt body
317
+ const userEvents = extractUserEvents(message);
318
+ for (const ev of userEvents) {
319
+ db.insertEvent(sessionId, ev, "UserPromptSubmit");
320
+ }
321
+ }
322
+ catch {
323
+ // Silent — chat.message must never break the turn
324
+ }
325
+ },
326
+ // ── PreCompact: Snapshot generation ─────────────────
327
+ "experimental.session.compacting": async (input, output) => {
328
+ const sessionId = input.sessionID;
329
+ if (!sessionId)
330
+ return "";
331
+ try {
332
+ db.ensureSession(sessionId, projectDir);
333
+ const events = db.getEvents(sessionId);
334
+ if (events.length === 0)
335
+ return "";
336
+ const stats = db.getSessionStats(sessionId);
337
+ const snapshot = buildResumeSnapshot(events, {
338
+ compactCount: (stats?.compact_count ?? 0) + 1,
339
+ });
340
+ db.upsertResume(sessionId, snapshot, events.length);
341
+ db.incrementCompactCount(sessionId);
342
+ // Mutate output.context to inject the snapshot
343
+ output.context.push(snapshot);
344
+ if (process.env.OPENCODE_DEBUG) {
345
+ await safeLog(snapshot, {
346
+ sessionId,
347
+ source: "on compaction - snapshot",
348
+ });
349
+ }
350
+ // OC-3 / Z3: Add budget-capped auto-injection (P1 role / P2 rules /
351
+ // P3 skills / P4 intent — ≤500 tokens / ~2000 chars per
352
+ // hooks/auto-injection.mjs). Pushed as a separate context entry so
353
+ // OpenCode can fold it independently from the verbose snapshot.
354
+ try {
355
+ const autoBlock = autoInjectionMod.buildAutoInjection(events);
356
+ if (autoBlock && autoBlock.length > 0) {
357
+ output.context.push(autoBlock);
358
+ }
359
+ if (process.env.OPENCODE_DEBUG) {
360
+ await safeLog(autoBlock, {
361
+ sessionId,
362
+ source: "on compaction - autoBlock",
363
+ });
364
+ }
365
+ }
366
+ catch {
367
+ // Auto-injection failure must NOT break the snapshot path.
368
+ }
369
+ return snapshot;
370
+ }
371
+ catch {
372
+ return "";
373
+ }
374
+ },
375
+ // ── SessionStart equivalent (PR #376) ───────────────
376
+ // OpenCode lacks a real SessionStart hook (#14808, #5409). The closest
377
+ // surrogate is `experimental.chat.system.transform` — verified shape:
378
+ // input: { sessionID?: string; model: Model }
379
+ // output: { system: string[] }
380
+ // We claim the most-recent unconsumed resume snapshot atomically (race-
381
+ // safe across concurrent processes) and prepend it to the system prompt.
382
+ "experimental.chat.system.transform": async (input, output) => {
383
+ const sessionId = input?.sessionID;
384
+ if (!sessionId)
385
+ return;
386
+ // ── OC-1 / CCv1: ROUTING_BLOCK injection ──────────────
387
+ // Inject the <context_window_protection> XML block on the first
388
+ // chat.system.transform per session. This is INDEPENDENT of the
389
+ // resume snapshot path below — routing block must fire even when
390
+ // no prior session row exists. Splice at index 1 (NOT unshift) for
391
+ // the same OpenCode llm.ts:117-128 cache-fold reason as resume.
392
+ //
393
+ // Skip injection when system prompt already contains context-mode
394
+ // routing rules (e.g. via AGENTS.md / CLAUDE.md loaded by the host).
395
+ // Detect by checking for a quorum of distinctive tool names — any two
396
+ // of ctx_execute, ctx_batch_execute, ctx_fetch_and_index confirms the
397
+ // instructions are present and avoids ~2K chars of duplication.
398
+ if (Array.isArray(output?.system)) {
399
+ if (!systemHasRoutingInstructions(output.system)) {
400
+ try {
401
+ output.system.splice(1, 0, routingBlock);
402
+ }
403
+ catch {
404
+ // Never break the chat turn on routing-block injection failure.
405
+ }
406
+ if (process.env.OPENCODE_DEBUG) {
407
+ await safeLog(output.system[1], { sessionId, source: 'on routing block injection' });
408
+ }
409
+ }
410
+ else if (process.env.OPENCODE_DEBUG) {
411
+ await safeLog(`routing block skipped — system prompt already contains context-mode instructions`, { sessionId, source: 'on routing block injection' });
412
+ }
413
+ }
414
+ try {
415
+ // Pass current sessionId so SQL excludes self-injection (v1.0.106 — Mickey #376
416
+ // follow-up): if Session B compacts mid-flight and produces its own row,
417
+ // B's next system.transform must NOT claim that row back into B's prompt.
418
+ const row = db.claimLatestUnconsumedResume(sessionId);
419
+ if (!row || !row.snapshot)
420
+ return; // no row → retry on next turn
421
+ if (process.env.OPENCODE_DEBUG) {
422
+ await safeLog(row.snapshot, {
423
+ sessionId,
424
+ source: "on resume - snapshot",
425
+ });
426
+ }
427
+ if (Array.isArray(output?.system)) {
428
+ // Insert at index 1 (after the header) — NOT unshift.
429
+ // OpenCode's llm.ts:117-128 saves `header = system[0]` BEFORE this
430
+ // hook runs and then folds the rest into a 2-part structure
431
+ // `[header, body]` only if `system[0] === header` after the hook.
432
+ // Prepending via unshift replaces system[0] with the snapshot,
433
+ // making the equality check fail → cache-fold is skipped → every
434
+ // system block is sent as a separate `role: "system"` message →
435
+ // provider prompt cache is invalidated on every resume injection.
436
+ // Inserting at index 1 keeps the header invariant and lets the
437
+ // snapshot ride along inside the cached body block.
438
+ output.system.splice(1, 0, row.snapshot);
439
+ // Mark consumed only AFTER successful splice so failed paths can retry
440
+ if (process.env.OPENCODE_DEBUG) {
441
+ await safeLog(output.system[1], { sessionId, source: "on resume" });
442
+ }
443
+ }
444
+ }
445
+ catch {
446
+ // Silent — never break the chat turn
447
+ }
448
+ },
449
+ };
450
+ }
451
+ // ── Exports ──────────────────────────────────────────────
452
+ // KiloCode PluginModule: default export with { server } shape
453
+ // OpenCode compat: named export for direct import("context-mode/plugin")
454
+ export default { id: "context-mode", server: createContextModePlugin };
455
+ export { createContextModePlugin as ContextModePlugin };
456
+ // Test surface — exported for unit testing the quorum substring fix (#487).
457
+ export { systemHasRoutingInstructions, ROUTING_MARKERS };