@michaelxwb/code-flow 0.6.4 → 0.8.1

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 (207) hide show
  1. package/README.md +47 -3
  2. package/docs/USAGE.md +1529 -0
  3. package/docs/acceptance-contract.md +217 -0
  4. package/docs/ci/acceptance-evidence.md +77 -0
  5. package/docs/migrations/runtime-0.7.0.md +41 -0
  6. package/docs/migrations/runtime-0.8.0.md +154 -0
  7. package/docs/migrations/runtime-0.9.0.md +40 -0
  8. package/docs/spec-injection-hardening.md +54 -0
  9. package/docs/spec-injection-performance.json +73 -0
  10. package/docs/spec-optimization.md +12 -0
  11. package/package.json +18 -3
  12. package/src/adapters/claude/CLAUDE.md +1 -1
  13. package/src/adapters/claude/commands/cf-init.md +1 -3
  14. package/src/adapters/claude/commands/cf-spec.md +23 -3
  15. package/src/adapters/claude/commands/cf-stats.md +11 -1
  16. package/src/adapters/claude/commands/cf-sync.md +20 -6
  17. package/src/adapters/claude/commands/cf-task/align.md +25 -3
  18. package/src/adapters/claude/commands/cf-task/archive.md +36 -40
  19. package/src/adapters/claude/commands/cf-task/block.md +15 -2
  20. package/src/adapters/claude/commands/cf-task/graph.md +13 -1
  21. package/src/adapters/claude/commands/cf-task/note.md +13 -1
  22. package/src/adapters/claude/commands/cf-task/plan.md +137 -23
  23. package/src/adapters/claude/commands/cf-task/prd.md +13 -2
  24. package/src/adapters/claude/commands/cf-task/start.md +67 -35
  25. package/src/adapters/claude/commands/cf-task/status.md +12 -0
  26. package/src/adapters/claude/commands/cf-task/verify-e2e.md +72 -34
  27. package/src/adapters/claude/commands/cf-validate.md +30 -5
  28. package/src/adapters/codex/AGENTS.md +1 -1
  29. package/src/adapters/codex/hooks.json +14 -10
  30. package/src/adapters/codex/skills/cf-init/SKILL.md +4 -4
  31. package/src/adapters/codex/skills/cf-spec/SKILL.md +34 -6
  32. package/src/adapters/codex/skills/cf-stats/SKILL.md +11 -1
  33. package/src/adapters/codex/skills/cf-sync/SKILL.md +21 -6
  34. package/src/adapters/codex/skills/cf-task-align/SKILL.md +30 -8
  35. package/src/adapters/codex/skills/cf-task-archive/SKILL.md +36 -40
  36. package/src/adapters/codex/skills/cf-task-block/SKILL.md +15 -2
  37. package/src/adapters/codex/skills/cf-task-graph/SKILL.md +13 -1
  38. package/src/adapters/codex/skills/cf-task-note/SKILL.md +13 -1
  39. package/src/adapters/codex/skills/cf-task-plan/SKILL.md +175 -21
  40. package/src/adapters/codex/skills/cf-task-prd/SKILL.md +13 -2
  41. package/src/adapters/codex/skills/cf-task-start/SKILL.md +67 -35
  42. package/src/adapters/codex/skills/cf-task-status/SKILL.md +12 -0
  43. package/src/adapters/codex/skills/cf-task-verify-e2e/SKILL.md +73 -34
  44. package/src/adapters/codex/skills/cf-validate/SKILL.md +31 -5
  45. package/src/adapters/commands.json +115 -0
  46. package/src/adapters/costrict/CLAUDE.md +1 -1
  47. package/src/adapters/costrict/commands/cf-init.md +1 -3
  48. package/src/adapters/costrict/commands/cf-spec.md +23 -3
  49. package/src/adapters/costrict/commands/cf-stats.md +11 -1
  50. package/src/adapters/costrict/commands/cf-sync.md +20 -6
  51. package/src/adapters/costrict/commands/cf-task/align.md +25 -3
  52. package/src/adapters/costrict/commands/cf-task/archive.md +36 -40
  53. package/src/adapters/costrict/commands/cf-task/block.md +15 -2
  54. package/src/adapters/costrict/commands/cf-task/graph.md +13 -1
  55. package/src/adapters/costrict/commands/cf-task/note.md +13 -1
  56. package/src/adapters/costrict/commands/cf-task/plan.md +137 -23
  57. package/src/adapters/costrict/commands/cf-task/prd.md +13 -2
  58. package/src/adapters/costrict/commands/cf-task/start.md +67 -35
  59. package/src/adapters/costrict/commands/cf-task/status.md +12 -0
  60. package/src/adapters/costrict/commands/cf-task/verify-e2e.md +72 -34
  61. package/src/adapters/costrict/commands/cf-validate.md +30 -5
  62. package/src/adapters/install-plan.json +100 -0
  63. package/src/adapters/opencode/AGENTS.md +1 -1
  64. package/src/adapters/opencode/commands/cf-init.md +7 -7
  65. package/src/adapters/opencode/commands/cf-learn.md +6 -6
  66. package/src/adapters/opencode/commands/cf-spec.md +34 -2
  67. package/src/adapters/opencode/commands/cf-stats.md +17 -7
  68. package/src/adapters/opencode/commands/cf-sync.md +21 -6
  69. package/src/adapters/opencode/commands/cf-task/align.md +25 -8
  70. package/src/adapters/opencode/commands/cf-task/archive.md +36 -40
  71. package/src/adapters/opencode/commands/cf-task/block.md +16 -2
  72. package/src/adapters/opencode/commands/cf-task/graph.md +14 -1
  73. package/src/adapters/opencode/commands/cf-task/note.md +14 -1
  74. package/src/adapters/opencode/commands/cf-task/plan.md +137 -23
  75. package/src/adapters/opencode/commands/cf-task/prd.md +13 -2
  76. package/src/adapters/opencode/commands/cf-task/start.md +68 -41
  77. package/src/adapters/opencode/commands/cf-task/status.md +13 -0
  78. package/src/adapters/opencode/commands/cf-task/verify-e2e.md +72 -34
  79. package/src/adapters/opencode/commands/cf-validate.md +35 -9
  80. package/src/adapters/opencode/plugins/code-flow/index.js +128 -179
  81. package/src/adapters/platform-bindings.json +370 -0
  82. package/src/cli.js +369 -316
  83. package/src/core/code-flow/config.yml +4 -1
  84. package/src/core/code-flow/runtime-commands.json +259 -0
  85. package/src/core/code-flow/scripts/cf_acceptance_manifest.py +5 -289
  86. package/src/core/code-flow/scripts/cf_acceptance_runner.py +5 -187
  87. package/src/core/code-flow/scripts/cf_audit.py +10 -0
  88. package/src/core/code-flow/scripts/cf_codex_hook.py +10 -0
  89. package/src/core/code-flow/scripts/cf_design_lint.py +10 -0
  90. package/src/core/code-flow/scripts/cf_evidence_import.py +10 -0
  91. package/src/core/code-flow/scripts/cf_feedback.py +5 -51
  92. package/src/core/code-flow/scripts/cf_opencode_event.py +10 -0
  93. package/src/core/code-flow/scripts/cf_post_hook.py +6 -191
  94. package/src/core/code-flow/scripts/cf_pre_tool_hook.py +6 -99
  95. package/src/core/code-flow/scripts/cf_scenarios.py +10 -0
  96. package/src/core/code-flow/scripts/cf_spec_context.py +5 -1714
  97. package/src/core/code-flow/scripts/cf_spec_gate.py +5 -257
  98. package/src/core/code-flow/scripts/cf_spec_session.py +5 -114
  99. package/src/core/code-flow/scripts/cf_stats.py +5 -501
  100. package/src/core/code-flow/scripts/cf_stop_hook.py +6 -376
  101. package/src/core/code-flow/scripts/cf_sync.py +5 -135
  102. package/src/core/code-flow/scripts/cf_task_index.py +5 -143
  103. package/src/core/code-flow/scripts/cf_task_parallel.py +5 -380
  104. package/src/core/code-flow/scripts/cf_task_workflow.py +6 -103
  105. package/src/core/code-flow/scripts/cf_user_prompt_hook.py +6 -173
  106. package/src/core/code-flow/scripts/cf_validation.py +6 -77
  107. package/src/core/code-flow/scripts/codeflow/__init__.py +0 -0
  108. package/src/core/code-flow/scripts/codeflow/acceptance/__init__.py +0 -0
  109. package/src/core/code-flow/scripts/codeflow/acceptance/cf_acceptance_evidence.py +347 -0
  110. package/src/core/code-flow/scripts/codeflow/acceptance/cf_acceptance_manifest_core.py +660 -0
  111. package/src/core/code-flow/scripts/codeflow/acceptance/cf_acceptance_plan.py +202 -0
  112. package/src/core/code-flow/scripts/codeflow/acceptance/cf_acceptance_registration.py +60 -0
  113. package/src/core/code-flow/scripts/codeflow/acceptance/cf_acceptance_risk.py +73 -0
  114. package/src/core/code-flow/scripts/codeflow/acceptance/cf_acceptance_schema.py +282 -0
  115. package/src/core/code-flow/scripts/codeflow/acceptance/cf_acceptance_selection.py +266 -0
  116. package/src/core/code-flow/scripts/codeflow/acceptance/cf_check_inputs.py +172 -0
  117. package/src/core/code-flow/scripts/codeflow/acceptance/cf_check_registration.py +326 -0
  118. package/src/core/code-flow/scripts/{cf_checks.py → codeflow/acceptance/cf_checks.py} +8 -11
  119. package/src/core/code-flow/scripts/codeflow/acceptance/cf_plan_quality.py +197 -0
  120. package/src/core/code-flow/scripts/codeflow/acceptance/cf_scenario_quality.py +174 -0
  121. package/src/core/code-flow/scripts/codeflow/acceptance/cf_scenario_source.py +300 -0
  122. package/src/core/code-flow/scripts/codeflow/acceptance/cf_traceability.py +502 -0
  123. package/src/core/code-flow/scripts/codeflow/cli/__init__.py +0 -0
  124. package/src/core/code-flow/scripts/codeflow/cli/cf_acceptance_manifest.py +347 -0
  125. package/src/core/code-flow/scripts/codeflow/cli/cf_acceptance_runner.py +1490 -0
  126. package/src/core/code-flow/scripts/codeflow/cli/cf_audit.py +533 -0
  127. package/src/core/code-flow/scripts/codeflow/cli/cf_design_lint.py +172 -0
  128. package/src/core/code-flow/scripts/codeflow/cli/cf_evidence_import.py +607 -0
  129. package/src/core/code-flow/scripts/codeflow/cli/cf_feedback.py +53 -0
  130. package/src/core/code-flow/scripts/codeflow/cli/cf_scenarios.py +58 -0
  131. package/src/core/code-flow/scripts/codeflow/cli/cf_spec_context.py +1175 -0
  132. package/src/core/code-flow/scripts/codeflow/cli/cf_spec_gate.py +60 -0
  133. package/src/core/code-flow/scripts/codeflow/cli/cf_spec_session.py +46 -0
  134. package/src/core/code-flow/scripts/codeflow/cli/cf_stats.py +540 -0
  135. package/src/core/code-flow/scripts/codeflow/cli/cf_sync.py +144 -0
  136. package/src/core/code-flow/scripts/codeflow/cli/cf_task_index.py +177 -0
  137. package/src/core/code-flow/scripts/codeflow/cli/cf_task_parallel.py +1142 -0
  138. package/src/core/code-flow/scripts/codeflow/cli/cf_task_workflow.py +742 -0
  139. package/src/core/code-flow/scripts/codeflow/cli/cf_validation.py +31 -0
  140. package/src/core/code-flow/scripts/codeflow/core/__init__.py +0 -0
  141. package/src/core/code-flow/scripts/codeflow/core/cf_context_cache.py +67 -0
  142. package/src/core/code-flow/scripts/{cf_core.py → codeflow/core/cf_core.py} +119 -297
  143. package/src/core/code-flow/scripts/codeflow/core/cf_exec_base.py +325 -0
  144. package/src/core/code-flow/scripts/codeflow/core/cf_fs.py +57 -0
  145. package/src/core/code-flow/scripts/codeflow/core/cf_git.py +92 -0
  146. package/src/core/code-flow/scripts/{cf_log.py → codeflow/core/cf_log.py} +13 -7
  147. package/src/core/code-flow/scripts/codeflow/core/cf_markdown.py +77 -0
  148. package/src/core/code-flow/scripts/codeflow/core/cf_native_paths.py +26 -0
  149. package/src/core/code-flow/scripts/codeflow/core/cf_patch.py +33 -0
  150. package/src/core/code-flow/scripts/codeflow/core/cf_runtime_install.py +32 -0
  151. package/src/core/code-flow/scripts/codeflow/core/cf_stop_guard.py +53 -0
  152. package/src/core/code-flow/scripts/codeflow/core/cf_task_state.py +9 -0
  153. package/src/core/code-flow/scripts/codeflow/core/cf_validators.py +408 -0
  154. package/src/core/code-flow/scripts/codeflow/core/cf_worktree_env.py +253 -0
  155. package/src/core/code-flow/scripts/codeflow/evidence/__init__.py +0 -0
  156. package/src/core/code-flow/scripts/codeflow/evidence/cf_evidence_store.py +527 -0
  157. package/src/core/code-flow/scripts/codeflow/hooks/__init__.py +0 -0
  158. package/src/core/code-flow/scripts/codeflow/hooks/cf_codex_hook.py +95 -0
  159. package/src/core/code-flow/scripts/codeflow/hooks/cf_opencode_event.py +98 -0
  160. package/src/core/code-flow/scripts/codeflow/hooks/cf_post_hook.py +30 -0
  161. package/src/core/code-flow/scripts/codeflow/hooks/cf_pre_tool_hook.py +41 -0
  162. package/src/core/code-flow/scripts/codeflow/hooks/cf_stop_hook.py +292 -0
  163. package/src/core/code-flow/scripts/codeflow/hooks/cf_user_prompt_hook.py +161 -0
  164. package/src/core/code-flow/scripts/codeflow/spec/__init__.py +0 -0
  165. package/src/core/code-flow/scripts/codeflow/spec/cf_active_task.py +412 -0
  166. package/src/core/code-flow/scripts/codeflow/spec/cf_context_refresh.py +301 -0
  167. package/src/core/code-flow/scripts/codeflow/spec/cf_edit_service.py +149 -0
  168. package/src/core/code-flow/scripts/codeflow/spec/cf_injection.py +111 -0
  169. package/src/core/code-flow/scripts/codeflow/spec/cf_rule_readiness.py +114 -0
  170. package/src/core/code-flow/scripts/{cf_scan.py → codeflow/spec/cf_scan.py} +93 -21
  171. package/src/core/code-flow/scripts/codeflow/spec/cf_spec_budget.py +55 -0
  172. package/src/core/code-flow/scripts/codeflow/spec/cf_spec_context_model.py +477 -0
  173. package/src/core/code-flow/scripts/codeflow/spec/cf_spec_impact.py +119 -0
  174. package/src/core/code-flow/scripts/{cf_spec_metadata.py → codeflow/spec/cf_spec_metadata.py} +37 -3
  175. package/src/core/code-flow/scripts/{cf_spec_migrate.py → codeflow/spec/cf_spec_migrate.py} +27 -10
  176. package/src/core/code-flow/scripts/codeflow/spec/cf_spec_optimize.py +193 -0
  177. package/src/core/code-flow/scripts/codeflow/spec/cf_spec_optimize_apply.py +187 -0
  178. package/src/core/code-flow/scripts/{cf_spec_resolver.py → codeflow/spec/cf_spec_resolver.py} +39 -17
  179. package/src/core/code-flow/scripts/{cf_spec_router.py → codeflow/spec/cf_spec_router.py} +96 -36
  180. package/src/core/code-flow/scripts/{cf_spec_verify.py → codeflow/spec/cf_spec_verify.py} +252 -26
  181. package/src/core/code-flow/scripts/codeflow/spec/cf_stage_gate.py +330 -0
  182. package/src/core/code-flow/scripts/codeflow/spec/cf_task_projection.py +83 -0
  183. package/src/core/code-flow/scripts/codeflow/spec/cf_workflow_service.py +755 -0
  184. package/src/core/code-flow/scripts/{cf_workflow_transaction.py → codeflow/spec/cf_workflow_transaction.py} +29 -15
  185. package/src/core/code-flow/scripts/codeflow/task/__init__.py +0 -0
  186. package/src/core/code-flow/scripts/codeflow/task/cf_requirement_verification.py +867 -0
  187. package/src/core/code-flow/scripts/codeflow/task/cf_task_doc.py +178 -0
  188. package/src/core/code-flow/scripts/codeflow/task/cf_task_merge.py +635 -0
  189. package/src/core/code-flow/scripts/codeflow/task/cf_task_plan.py +251 -0
  190. package/src/core/code-flow/scripts/codeflow/task/cf_task_runtime.py +570 -0
  191. package/src/core/code-flow/scripts/codeflow/task/cf_validation_service.py +73 -0
  192. package/src/core/code-flow/scripts/runtime-resources.json +29 -0
  193. package/src/core/code-flow/specs/shared/design/design-frontend.md +18 -15
  194. package/src/core/code-flow/specs/shared/design/design-full.md +33 -23
  195. package/src/core/code-flow/specs/shared/design/design-lite.md +23 -13
  196. package/src/core/code-flow/specs/shared/scenarios-template.md +55 -0
  197. package/src/core/code-flow/validation.yml +9 -3
  198. package/src/migrate/runtime.js +213 -0
  199. package/src/migrate/spec-workflow.js +16 -1
  200. package/src/runtime.js +219 -0
  201. package/src/core/code-flow/scripts/cf_acceptance_evidence.py +0 -100
  202. package/src/core/code-flow/scripts/cf_acceptance_schema.py +0 -83
  203. package/src/core/code-flow/scripts/cf_exec_base.py +0 -112
  204. package/src/core/code-flow/scripts/cf_task_runtime.py +0 -299
  205. package/src/core/code-flow/scripts/cf_task_state.py +0 -3
  206. package/src/core/code-flow/scripts/cf_workflow_service.py +0 -366
  207. /package/src/core/code-flow/scripts/{cf_session_state.py → codeflow/core/cf_session_state.py} +0 -0
@@ -1,213 +1,162 @@
1
- import { execFile } from "child_process";
2
- import { join } from "path";
3
- import { appendFileSync, mkdirSync } from "fs";
1
+ import { execFile } from "node:child_process";
2
+ import { join, isAbsolute, resolve, dirname } from "node:path";
3
+ import { existsSync } from "node:fs";
4
4
 
5
- // Note: intentionally no `import { Plugin } from "@opencode/plugin"`.
6
- // The server does not auto-provide that package for local plugins, and
7
- // `Plugin.define` is only an identity wrapper — a plain default export
8
- // with `id` + `setup` satisfies the v2 plugin schema dependency-free,
9
- // so `code-flow init` works offline without npm install.
10
-
11
- const SCRIPT_DIR = ".code-flow/scripts";
12
- const STOP_CHECK_TIMEOUT = 35000;
13
5
  const HOOK_TIMEOUT = 5000;
14
-
15
- // Per-session queued feedback. Single map, append-only: idle/stop-check and
16
- // post-check feedback accumulate here and are consumed by the next context
17
- // hook. Never overwrite — a prompt hook arriving between idle and context
18
- // must not drop queued stop-check feedback.
19
- const sessionContext = new Map();
20
-
21
- export function mergePending(existing, incoming) {
22
- if (!existing) return incoming;
23
- if (!incoming) return existing;
24
- return existing + "\n\n" + incoming;
25
- }
26
-
27
- function debugLog(projectRoot, msg) {
28
- if (process.env.CF_DEBUG !== "1") return;
29
- try {
30
- const dir = join(projectRoot, ".code-flow");
31
- mkdirSync(dir, { recursive: true });
32
- const ts = new Date().toISOString().replace("T", " ").slice(0, 19);
33
- appendFileSync(join(dir, ".debug.log"), `${ts} [opencode] ${msg}\n`);
34
- } catch {}
6
+ const IDLE_TIMEOUT = 35000;
7
+
8
+ function runtimeScript(directory) {
9
+ let root = resolve(directory);
10
+ while (!existsSync(join(root, ".code-flow/config.yml"))) {
11
+ const parent = dirname(root);
12
+ if (parent === root) throw new Error(`No code-flow runtime for OpenCode session at ${directory}`);
13
+ root = parent;
14
+ }
15
+ return join(root, ".code-flow/scripts/cf_opencode_event.py");
35
16
  }
36
17
 
37
- function pythonPath(projectRoot, script) {
38
- return join(projectRoot, SCRIPT_DIR, script);
18
+ /** Combine model-visible feedback without dropping earlier gate failures. */
19
+ export function mergePending(existing, incoming) {
20
+ return [existing, incoming].filter(Boolean).join("\n\n");
39
21
  }
40
22
 
41
- function callHook(projectRoot, script, input, timeout = HOOK_TIMEOUT) {
42
- // Async child process: never block the plugin event thread. opencode's
43
- // idle event cannot interrupt a turn, so stop-check failures queue into
44
- // sessionContext for the next context hook (documented platform gap
45
- // vs. blocking Stop hooks).
46
- return new Promise((resolve) => {
47
- const child = execFile(
48
- "python3",
49
- [pythonPath(projectRoot, script)],
50
- { cwd: projectRoot, timeout, maxBuffer: 1024 * 1024 },
51
- (error, stdout, stderr) => {
52
- if (error) {
53
- debugLog(projectRoot, `callHook ${script} failed: ${error.message || stderr}`);
54
- resolve(null);
55
- return;
56
- }
57
- const text = (stdout || "").trim();
58
- if (!text) {
59
- resolve(null);
60
- return;
61
- }
23
+ function callRuntime(directory, kind, event, signal) {
24
+ return new Promise((accept, reject) => {
25
+ const child = execFile(process.platform === "win32" ? "py" : "python3",
26
+ [...(process.platform === "win32" ? ["-3"] : []), runtimeScript(directory)],
27
+ { cwd: directory, timeout: kind === "session.idle" ? IDLE_TIMEOUT : HOOK_TIMEOUT,
28
+ maxBuffer: 1024 * 1024, signal }, (error, stdout, stderr) => {
29
+ if (error) return reject(new Error(`code-flow native runtime: ${error.message}; ${stderr.trim()}`));
30
+ if (stderr.trim()) console.error(stderr.trim());
62
31
  try {
63
- resolve(JSON.parse(text));
64
- } catch (e) {
65
- debugLog(projectRoot, `callHook ${script} bad JSON: ${e.message}`);
66
- resolve(null);
67
- }
68
- }
69
- );
70
- if (input !== undefined && child.stdin) {
71
- child.stdin.write(JSON.stringify(input));
72
- child.stdin.end();
73
- }
32
+ const result = JSON.parse(stdout);
33
+ if (result.error) throw new Error(result.error);
34
+ if (typeof result.context !== "string" || typeof result.blocked !== "boolean") {
35
+ throw new Error("Invalid code-flow native RPC response");
36
+ }
37
+ accept(result);
38
+ } catch (failure) { reject(failure); }
39
+ });
40
+ child.stdin.on("error", reject);
41
+ child.stdin.end(JSON.stringify({ directory, kind, event }));
74
42
  });
75
43
  }
76
44
 
77
- function queueFeedback(sessionID, text) {
78
- if (!sessionID || !text) return;
79
- sessionContext.set(sessionID, mergePending(sessionContext.get(sessionID), text));
80
- }
81
-
82
- function consumeFeedback(sessionID) {
83
- const pending = sessionContext.get(sessionID);
84
- if (pending) sessionContext.delete(sessionID);
85
- return pending;
86
- }
87
-
88
- function toolInputFilePath(input) {
89
- if (!input || typeof input !== "object") return "";
90
- const args = input;
91
- return args.filePath || args.file_path || args.path || "";
45
+ function createState() {
46
+ const pending = new Map();
47
+ const jobs = new Map();
48
+ let disposed = false;
49
+ return {
50
+ fault: "",
51
+ queue(sid, text) { if (!disposed && text) pending.set(sid, mergePending(pending.get(sid), text)); },
52
+ consume(sid) { const text = pending.get(sid); pending.delete(sid); return text; },
53
+ async wait(sid) { await jobs.get(sid); },
54
+ run(sid, operation) {
55
+ const job = (jobs.get(sid) || Promise.resolve()).then(operation, operation);
56
+ jobs.set(sid, job);
57
+ return job.finally(() => { if (jobs.get(sid) === job) jobs.delete(sid); });
58
+ },
59
+ clear(sid) { pending.delete(sid); },
60
+ dispose() { disposed = true; pending.clear(); jobs.clear(); },
61
+ };
92
62
  }
93
63
 
94
- async function runStopCheck(projectRoot, sessionID) {
95
- const result = await callHook(projectRoot, "cf_stop_hook.py", { session_id: sessionID }, STOP_CHECK_TIMEOUT);
96
- if (result?.reason) {
97
- // idle 无法阻断,校验失败排队到下一轮 context hook
98
- queueFeedback(sessionID, result.reason);
99
- debugLog(projectRoot, `stop-check feedback queued`);
64
+ async function sessionInfo(ctx, sid) {
65
+ if (typeof sid !== "string" || !sid) throw new Error("OpenCode native event requires sessionID");
66
+ const info = await ctx.session.get({ sessionID: sid });
67
+ if (typeof info?.location?.directory !== "string" || !isAbsolute(info.location.directory)) {
68
+ throw new Error(`OpenCode session ${sid} has no absolute location.directory`);
100
69
  }
70
+ return info;
101
71
  }
102
72
 
103
- async function registerPromptHook(ctx, projectRoot) {
104
- await ctx.session.hook("prompt", async (event) => {
105
- const promptText = event.prompt?.text || "";
106
- if (!promptText) return;
107
- debugLog(projectRoot, `prompt sid=${event.sessionID} prompt_len=${promptText.length}`);
108
- const result = await callHook(projectRoot, "cf_user_prompt_hook.py", {
109
- prompt: promptText,
110
- session_id: event.sessionID,
111
- });
112
- const additional = result?.hookSpecificOutput?.additionalContext;
113
- if (additional) {
114
- // Append, never overwrite: queued stop-check feedback survives.
115
- queueFeedback(event.sessionID, additional);
116
- debugLog(projectRoot, `hook matched — context ${additional.length} chars cached`);
117
- } else {
118
- debugLog(projectRoot, `hook returned no context`);
73
+ async function invoke(ctx, state, kind, event, signal) {
74
+ let result;
75
+ try {
76
+ const info = await sessionInfo(ctx, event.sessionID);
77
+ result = await callRuntime(info.location.directory, kind, event, signal);
78
+ } catch (error) {
79
+ if (!signal.aborted) {
80
+ const message = `code-flow OpenCode: ${error.message}`;
81
+ // The event subscriber owns idle error feedback, including lookup errors.
82
+ if (kind !== "session.idle") state.queue(event.sessionID, message);
83
+ console.error(message);
119
84
  }
120
- });
85
+ throw error;
86
+ }
87
+ state.queue(event.sessionID, result.context);
88
+ if (result.blocked && ["tool.before", "session.prompt"].includes(kind)) throw new Error(result.context);
121
89
  }
122
90
 
123
- async function registerToolHook(ctx, projectRoot) {
124
- await ctx.tool.hook("execute.after", async (event) => {
125
- const tool = String(event.tool || "").toLowerCase();
126
- if (!["edit", "write", "multiedit", "patch"].includes(tool)) return;
127
- const filePath = toolInputFilePath(event.input);
128
- if (!filePath) return;
129
- const result = await callHook(projectRoot, "cf_post_hook.py", {
130
- tool_name: tool === "write" ? "Write" : "Edit",
131
- tool_input: { file_path: filePath },
132
- session_id: event.sessionID || "",
133
- });
134
- const additional = result?.hookSpecificOutput?.additionalContext;
135
- if (additional) {
136
- // 反馈排队,下一轮 context hook 注入(opencode 无法当轮插话)
137
- queueFeedback(event.sessionID, additional);
138
- debugLog(projectRoot, `post-check feedback queued ${additional.length} chars`);
91
+ async function registerHooks(ctx, state, signal) {
92
+ await ctx.session.hook("prompt", event => {
93
+ if (!event.prompt?.text?.trim()) return;
94
+ return state.run(event.sessionID, () => invoke(ctx, state, "session.prompt", event, signal));
95
+ });
96
+ await ctx.session.hook("context", async event => {
97
+ try { await state.wait(event.sessionID); } catch (error) {
98
+ console.error(`code-flow pending event failed: ${error.message}`);
139
99
  }
100
+ const text = mergePending(state.consume(event.sessionID), state.fault);
101
+ state.fault = "";
102
+ if (text) event.system.push({ type: "text", text });
140
103
  });
141
- }
142
-
143
- async function registerContextHook(ctx) {
144
- await ctx.session.hook("context", (event) => {
145
- const pending = consumeFeedback(event.sessionID);
146
- if (pending) {
147
- event.system.push({ type: "text", text: pending });
148
- debugLog(ctx.location.directory, `context — injected ${pending.length} chars`);
104
+ await ctx.tool.hook("execute.before", event => {
105
+ if (!["edit", "write", "patch"].includes(event.tool)) return;
106
+ return state.run(event.sessionID, () => invoke(ctx, state, "tool.before", event, signal));
107
+ });
108
+ await ctx.tool.hook("execute.after", async event => {
109
+ if (!["edit", "write", "patch"].includes(event.tool)) return;
110
+ try {
111
+ await state.run(event.sessionID, () => invoke(ctx, state, "tool.after", event, signal));
112
+ } catch (error) {
113
+ // The mutation has already happened. Preserve its result and expose the
114
+ // runtime error through the next native context hook.
115
+ if (!signal.aborted) console.error(`code-flow post-edit check failed: ${error.message}`);
149
116
  }
150
117
  });
151
118
  }
152
119
 
153
- // 子会话(子 agent / worktree 并行任务)的 idle 不得触发主工作区 stop-check:
154
- // 子会话的编辑发生在自己的工作区,主区既无 active marker 也没有对应文件,
155
- // 触发只会重复执行主区 validators(全量测试)。用 session.created.parentID 标记子会话。
156
- export function createSessionRegistry() {
157
- const children = new Set();
158
- return {
159
- observe(event) {
160
- if (!event || typeof event.type !== "string") return null;
161
- const sid = event.data?.sessionID || "";
162
- if (!sid) return null;
163
- if (event.type === "session.created") {
164
- if (event.data?.parentID) children.add(sid);
165
- return { type: "created", sid };
166
- }
167
- if (event.type === "session.idle") {
168
- return { type: "idle", sid, child: children.has(sid) };
169
- }
170
- return null;
171
- },
172
- };
120
+ async function observeIdle(ctx, state, event, signal) {
121
+ const sid = event.data?.sessionID;
122
+ if (!sid) throw new Error("OpenCode session.idle requires data.sessionID");
123
+ const info = await sessionInfo(ctx, sid);
124
+ if (resolve(info.location.directory) !== resolve(ctx.location.directory)) return;
125
+ if (info.parentID) {
126
+ const parent = await sessionInfo(ctx, info.parentID);
127
+ if (resolve(parent.location.directory) === resolve(info.location.directory)) return;
128
+ }
129
+ await state.run(sid, () => invoke(ctx, state, "session.idle", { sessionID: sid }, signal));
173
130
  }
174
131
 
175
- function startEventSubscription(ctx, projectRoot) {
176
- const controller = new AbortController();
177
- const registry = createSessionRegistry();
178
- void (async () => {
179
- try {
180
- for await (const event of ctx.event.subscribe({ signal: controller.signal })) {
181
- const observed = registry.observe(event);
182
- if (!observed) continue;
183
- if (observed.type === "created") {
184
- debugLog(projectRoot, `session.created sid=${observed.sid}`);
185
- sessionContext.delete(observed.sid);
186
- } else if (observed.child) {
187
- debugLog(projectRoot, `session.idle sid=${observed.sid} (child) — stop-check skipped`);
188
- } else {
189
- await runStopCheck(projectRoot, observed.sid);
190
- }
191
- }
192
- } catch (err) {
193
- if (err?.name !== "AbortError") {
194
- debugLog(projectRoot, `event subscription ended: ${err?.message || err}`);
132
+ async function subscribe(ctx, state, signal) {
133
+ try {
134
+ for await (const event of ctx.event.subscribe({ signal })) {
135
+ try {
136
+ if (event.type === "session.deleted") state.clear(event.data.sessionID);
137
+ if (event.type === "session.idle") await observeIdle(ctx, state, event, signal);
138
+ } catch (error) {
139
+ if (signal.aborted) return;
140
+ state.queue(event.data?.sessionID, `code-flow idle check failed: ${error.message}`);
141
+ console.error(`code-flow idle check failed: ${error.message}`);
195
142
  }
196
143
  }
197
- })();
198
- return () => controller.abort();
144
+ } catch (error) {
145
+ if (!signal.aborted) {
146
+ state.fault = `code-flow OpenCode event subscription failed: ${error.message}`;
147
+ console.error(state.fault);
148
+ }
149
+ }
199
150
  }
200
151
 
201
- const CodeFlowPlugin = {
152
+ // V2's native definition shape. No v1 server() entry or protocol adapter.
153
+ export default {
202
154
  id: "code-flow",
203
155
  async setup(ctx) {
204
- const projectRoot = ctx.location.directory;
205
- await registerPromptHook(ctx, projectRoot);
206
- await registerToolHook(ctx, projectRoot);
207
- await registerContextHook(ctx);
208
- const stopSubscription = startEventSubscription(ctx, projectRoot);
209
- return () => stopSubscription();
156
+ const state = createState();
157
+ const controller = new AbortController();
158
+ await registerHooks(ctx, state, controller.signal);
159
+ void subscribe(ctx, state, controller.signal);
160
+ return () => { controller.abort(); state.dispose(); };
210
161
  },
211
162
  };
212
-
213
- export default CodeFlowPlugin;