@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
package/src/runtime.js ADDED
@@ -0,0 +1,219 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const crypto = require('crypto');
6
+ const { spawnSync } = require('child_process');
7
+ const pkg = require('../package.json');
8
+
9
+ function projectRoot(cwd) {
10
+ let current = path.resolve(cwd);
11
+ while (true) {
12
+ if (fs.existsSync(path.join(current, '.code-flow/config.yml'))) return current;
13
+ const parent = path.dirname(current);
14
+ if (parent === current) throw new Error('Run code-flow inside an initialized project.');
15
+ current = parent;
16
+ }
17
+ }
18
+
19
+ function commands(root) {
20
+ const registry = JSON.parse(fs.readFileSync(path.join(root, '.code-flow/runtime-commands.json'), 'utf8'));
21
+ if (registry.schema_version !== 1) throw new Error('Unsupported runtime command contract.');
22
+ return registry.commands;
23
+ }
24
+
25
+ function projectCommandFamily(name) {
26
+ if (!name || name.startsWith('-')) return false;
27
+ let current = path.resolve(process.cwd());
28
+ while (true) {
29
+ if (fs.existsSync(path.join(current, '.code-flow/runtime-commands.json'))) {
30
+ try {
31
+ return Object.keys(commands(current)).some(command => command.split(' ')[0] === name);
32
+ } catch (error) {
33
+ process.stderr.write(`code-flow contract: ${error.message}\n`);
34
+ return false;
35
+ }
36
+ }
37
+ const parent = path.dirname(current);
38
+ if (parent === current) return false;
39
+ current = parent;
40
+ }
41
+ }
42
+
43
+ function versionRepair(root) {
44
+ const file = path.join(root, '.code-flow/.version');
45
+ if (fs.existsSync(file)) {
46
+ const version = fs.readFileSync(file, 'utf8').trim();
47
+ if (/^\d+\.\d+\.\d+$/.test(version)) {
48
+ const installed = version.split('.').map(Number);
49
+ const current = pkg.version.split('.').map(Number);
50
+ const index = installed.findIndex((value, position) => value !== current[position]);
51
+ if (index >= 0 && installed[index] > current[index]) {
52
+ return `CLI ${pkg.version} is older than project runtime ${version}. Upgrade the CLI: npm install --global ${pkg.name}@${version}`;
53
+ }
54
+ }
55
+ }
56
+ return 'code-flow migrate --runtime --dry-run';
57
+ }
58
+
59
+ function digest(file) {
60
+ return crypto.createHash('sha256').update(fs.readFileSync(file)).digest('hex');
61
+ }
62
+
63
+ function artifactDigest(relative, source) {
64
+ if (relative !== '.opencode/plugins/code-flow/package.json') return digest(source);
65
+ const data = JSON.parse(fs.readFileSync(source, 'utf8'));
66
+ data.version = pkg.version;
67
+ return crypto.createHash('sha256').update(JSON.stringify(data, null, 2) + '\n').digest('hex');
68
+ }
69
+
70
+ function filesUnder(directory, prefix) {
71
+ if (!fs.existsSync(directory)) return [];
72
+ return fs.readdirSync(directory, { withFileTypes: true }).flatMap(entry => {
73
+ if (entry.name === '__pycache__' || entry.name === '.DS_Store') return [];
74
+ const relative = path.posix.join(prefix, entry.name);
75
+ return entry.isDirectory() ? filesUnder(path.join(directory, entry.name), relative) : [relative];
76
+ });
77
+ }
78
+
79
+ function artifactSources(platforms) {
80
+ const base = path.join(__dirname, 'core/code-flow');
81
+ const sources = { '.code-flow/runtime-commands.json': path.join(base, 'runtime-commands.json') };
82
+ for (const relative of filesUnder(path.join(base, 'scripts'), 'scripts')) {
83
+ sources[`.code-flow/${relative}`] = path.join(base, relative);
84
+ }
85
+ for (const platform of platforms) {
86
+ const adapter = path.join(__dirname, 'adapters', platform);
87
+ const area = platform === 'codex' ? 'skills' : 'commands';
88
+ const destination = platform === 'codex' ? '.agents' : `.${platform}`;
89
+ for (const relative of filesUnder(path.join(adapter, area), area)) {
90
+ sources[`${destination}/${relative}`] = path.join(adapter, relative);
91
+ }
92
+ if (platform === 'opencode') {
93
+ for (const relative of filesUnder(path.join(adapter, 'plugins'), 'plugins')) {
94
+ sources[`.opencode/${relative}`] = path.join(adapter, relative);
95
+ }
96
+ }
97
+ }
98
+ return sources;
99
+ }
100
+
101
+ function hookContracts(platforms) {
102
+ const contracts = {};
103
+ for (const platform of platforms.filter(value => value !== 'opencode')) {
104
+ const name = platform === 'codex' ? 'hooks.json' : 'settings.local.json';
105
+ const source = path.join(__dirname, 'adapters', platform, name);
106
+ contracts[`.${platform}/${name}`] = JSON.parse(fs.readFileSync(source, 'utf8')).hooks;
107
+ }
108
+ return contracts;
109
+ }
110
+
111
+ function writeInstall(root, platforms) {
112
+ const files = Object.fromEntries(Object.entries(artifactSources(platforms))
113
+ .map(([relative, source]) => [relative, artifactDigest(relative, source)]));
114
+ const manifest = { schema_version: 1, version: pkg.version, platforms,
115
+ files, hook_contracts: hookContracts(platforms) };
116
+ const target = path.join(root, '.code-flow/.runtime-install.json');
117
+ fs.writeFileSync(`${target}.tmp`, JSON.stringify(manifest, null, 2) + '\n');
118
+ fs.renameSync(`${target}.tmp`, target);
119
+ }
120
+
121
+ function verifyHooks(root, contracts) {
122
+ for (const [relative, expected] of Object.entries(contracts)) {
123
+ const actual = JSON.parse(fs.readFileSync(path.join(root, relative), 'utf8')).hooks;
124
+ for (const [event, groups] of Object.entries(expected)) {
125
+ for (const group of groups) {
126
+ const candidates = (actual[event] || []).filter(item => (item.matcher || '') === (group.matcher || ''));
127
+ for (const hook of group.hooks) {
128
+ if (!candidates.some(item => item.hooks.some(value => JSON.stringify(value) === JSON.stringify(hook)))) {
129
+ throw new Error(`Managed hook has drifted: ${relative}:${event}`);
130
+ }
131
+ }
132
+ }
133
+ }
134
+ }
135
+ }
136
+
137
+ function verify(root, duringMigration = false) {
138
+ if (!duringMigration && fs.existsSync(path.join(root, '.code-flow/.runtime-migration.lock'))) {
139
+ throw new Error('Runtime migration is in progress or needs recovery.');
140
+ }
141
+ const manifest = JSON.parse(fs.readFileSync(path.join(root, '.code-flow/.runtime-install.json'), 'utf8'));
142
+ const version = fs.readFileSync(path.join(root, '.code-flow/.version'), 'utf8').trim();
143
+ if (manifest.schema_version !== 1 || manifest.version !== version || version !== pkg.version) {
144
+ throw new Error(`Runtime ${version} and CLI ${pkg.version} versions differ.`);
145
+ }
146
+ if (!manifest.files || Object.keys(manifest.files).length === 0 || !manifest.hook_contracts) {
147
+ throw new Error('Runtime installation manifest is incomplete.');
148
+ }
149
+ const expected = artifactSources(manifest.platforms);
150
+ if (Object.keys(expected).length !== Object.keys(manifest.files).length) {
151
+ throw new Error('Installed artifact set differs from the CLI contract.');
152
+ }
153
+ for (const [relative, source] of Object.entries(expected)) {
154
+ if (manifest.files[relative] !== artifactDigest(relative, source)) {
155
+ throw new Error(`Installed contract differs from the CLI package: ${relative}`);
156
+ }
157
+ }
158
+ for (const [relative, hash] of Object.entries(manifest.files)) {
159
+ const target = path.resolve(root, relative);
160
+ if (!target.startsWith(root + path.sep) || digest(target) !== hash) {
161
+ throw new Error(`Managed runtime artifact has drifted: ${relative}`);
162
+ }
163
+ }
164
+ verifyHooks(root, manifest.hook_contracts);
165
+ }
166
+
167
+ function execute(root, contract, values, scripts = path.join(root, '.code-flow/scripts')) {
168
+ try {
169
+ if (values.length === 1 || (values.length === 2 && ['--help', '-h'].includes(values[1]))) {
170
+ const family = Object.keys(contract).filter(command => command.startsWith(values[0] + ' '));
171
+ if (family.length) {
172
+ process.stdout.write(`Usage:\n${family.map(command => ` code-flow ${command} [options]`).join('\n')}\nUse <command> --help for its actual parameters.\n`);
173
+ return 0;
174
+ }
175
+ }
176
+ const key = Object.keys(contract).sort((a, b) => b.length - a.length)
177
+ .find(command => command.split(' ').every((part, index) => values[index] === part));
178
+ if (!key) throw new Error(`Unknown runtime command. Available: ${Object.keys(contract).join(', ')}`);
179
+ const entry = contract[key];
180
+ if (!/^cf_[a-z_]+$/.test(entry.module)) throw new Error('Invalid runtime module in command contract.');
181
+ const script = path.join(scripts, `${entry.module}.py`);
182
+ const args = [...entry.prefix, ...values.slice(key.split(' ').length)];
183
+ const publicName = entry.prefix.length ? key.split(' ').slice(0, -entry.prefix.length).join(' ') : key;
184
+ const result = spawnSync('python3', [script, ...args], {
185
+ cwd: root, stdio: 'inherit', env: { ...process.env,
186
+ CF_RUNTIME_COMMAND: `code-flow ${publicName}`, CF_RUNTIME_ACTION: entry.prefix.join(' '),
187
+ CF_CLI_VERSION: pkg.version },
188
+ });
189
+ if (result.error) throw result.error;
190
+ return result.status === null ? 2 : result.status;
191
+ } catch (error) {
192
+ process.stderr.write(`code-flow runtime: ${error.message}\n`);
193
+ return 2;
194
+ }
195
+ }
196
+
197
+ function run(values) {
198
+ let root;
199
+ try {
200
+ root = projectRoot(process.cwd());
201
+ } catch (error) {
202
+ process.stderr.write(`code-flow runtime: ${error.message}\n`);
203
+ return 3;
204
+ }
205
+ try {
206
+ if (values[0] === 'spec' && values[1] === 'doctor') {
207
+ const base = path.join(__dirname, 'core/code-flow');
208
+ const contract = JSON.parse(fs.readFileSync(path.join(base, 'runtime-commands.json'), 'utf8')).commands;
209
+ return execute(root, { 'spec doctor': contract['spec doctor'] }, values, path.join(base, 'scripts'));
210
+ }
211
+ verify(root);
212
+ return execute(root, commands(root), values);
213
+ } catch (error) {
214
+ process.stderr.write(`code-flow installation: ${error.message}\nRepair: ${versionRepair(root)}\n`);
215
+ return 3;
216
+ }
217
+ }
218
+
219
+ module.exports = { artifactSources, commands, digest, hookContracts, projectCommandFamily, projectRoot, run, verify, writeInstall };
@@ -1,100 +0,0 @@
1
- """Append execution history without erasing the user's acceptance contract."""
2
- from __future__ import annotations
3
-
4
- import json
5
- import re
6
- from datetime import datetime, timezone
7
- from pathlib import Path
8
- from typing import Mapping, Sequence
9
- from uuid import uuid4
10
-
11
- STATES = frozenset(("planned", "pending", "tbd", "verified", "unverified", "failed", "incomplete", "not_configured", "e2e_deferred", "manual_pending"))
12
-
13
-
14
- def scenario_line(line: str, scenario_id: str) -> bool:
15
- return bool(re.match(rf"^\s*(?:\|\s*|[-*]\s+){re.escape(scenario_id)}(?=\s*[:|])", line))
16
-
17
-
18
- def line_status(line: str, scenario_id: str) -> str:
19
- if not scenario_line(line, scenario_id):
20
- return ""
21
- if line.lstrip().startswith("|"):
22
- cells = [cell.strip().lower() for cell in line.strip().strip("|").split("|")]
23
- return next((cell for cell in reversed(cells) if cell in STATES), "")
24
- match = re.match(r"^\s*[-*]\s+[^:]+:\s*([a-z0-9_]+)\b", line, re.I)
25
- return match.group(1).lower() if match and match.group(1).lower() in STATES else ""
26
-
27
-
28
- def _status_line(line: str, scenario_id: str, status: str) -> str:
29
- if not scenario_line(line, scenario_id):
30
- return line
31
- if line.lstrip().startswith("|"):
32
- cells = line.split("|")
33
- for index in range(len(cells) - 1, 0, -1):
34
- if cells[index].strip().lower() in STATES:
35
- cells[index] = f" {status} "
36
- return "|".join(cells)
37
- return line.rstrip().rstrip("|") + f"| {status} |"
38
- prefix = r"(^\s*[-*]\s+[^:]+:\s*)"
39
- if line_status(line, scenario_id):
40
- return re.sub(prefix + r"(?:" + "|".join(sorted(STATES)) + r")\b", rf"\g<1>{status}", line, count=1, flags=re.I)
41
- return re.sub(prefix, rf"\g<1>{status} — ", line, count=1)
42
-
43
-
44
- def _patch_section(section: str, scenario_id: str, status: str, record: str) -> str:
45
- contract = re.search(r"(?ms)^### Acceptance Contract[^\n]*\n(.*?)(?=^### |^## |\Z)", section)
46
- if contract:
47
- body = "\n".join(_status_line(line, scenario_id, status) for line in contract.group(1).splitlines()) + "\n"
48
- section = section[:contract.start(1)] + body + section[contract.end(1):]
49
- evidence = re.search(r"(?ms)^### Acceptance Evidence[^\n]*\n(.*?)(?=^### |^## |\Z)", section)
50
- if evidence:
51
- body = evidence.group(1)
52
- if record not in body.splitlines():
53
- body = body.rstrip() + "\n" + record + "\n\n"
54
- section = section[:evidence.start(1)] + body + section[evidence.end(1):]
55
- else:
56
- section = section.rstrip() + "\n\n### Acceptance Evidence\n" + record + "\n"
57
- return section
58
-
59
-
60
- def sync_task_evidence(task: Path, scenario_id: str, confirmed_by: str, evidence: str, owner: str = "", status: str = "verified") -> None:
61
- text = task.read_text(encoding="utf-8")
62
- task_pattern = re.escape(owner) if owner else r"TASK-\d+"
63
- match = re.search(rf"(?ms)^##\s+{task_pattern}:.*?(?=^## |\Z)", text)
64
- if match is None:
65
- raise ValueError(f"acceptance owner missing: {owner}")
66
- record = f"- {scenario_id}: {status} — {evidence} (confirmed_by: {confirmed_by})"
67
- section = _patch_section(match.group(0), scenario_id, status, record)
68
- text = text[:match.start()] + section + text[match.end():]
69
- coverage = re.search(r"(?ms)^## Acceptance Coverage[^\n]*\n(.*?)(?=^## |\Z)", text)
70
- if coverage:
71
- body = "\n".join(_status_line(line, scenario_id, status) for line in coverage.group(1).splitlines()) + "\n"
72
- text = text[:coverage.start(1)] + body + text[coverage.end(1):]
73
- from cf_spec_context import _atomic_text
74
- _atomic_text(task, text)
75
-
76
-
77
- def persist_results(path: Path, data: dict[str, object], results: Sequence[Mapping[str, object]]) -> None:
78
- rows = {row["id"]: row for row in data["scenarios"]}
79
- run_id, executed_at = uuid4().hex, datetime.now(timezone.utc).isoformat()
80
- updates = []
81
- for result in results:
82
- state = str(result["status"])
83
- if state in ("not_included", "manual_pending"):
84
- continue
85
- row = rows[result["id"]]
86
- row["revision"] = int(row.get("revision", 0)) + 1
87
- status = "verified" if state == "passed" else state
88
- row["status"] = status
89
- record = {**result, "revision": row["revision"], "run_id": run_id, "executed_at": executed_at}
90
- row["evidence"] = record
91
- row.setdefault("runs", []).append(record)
92
- updates.append((row, record, status))
93
- from cf_spec_context import _atomic_text
94
- _atomic_text(path, json.dumps(data, ensure_ascii=False, indent=2) + "\n")
95
- task_name = data.get("task_file")
96
- if isinstance(task_name, str) and task_name:
97
- task = Path(task_name) if Path(task_name).is_absolute() else path.parent / task_name
98
- for row, record, status in updates:
99
- detail = f"automated command {record['status']}; run_id={run_id}"
100
- sync_task_evidence(task, str(row["id"]), "runner", detail, str(row.get("owner", "")), status)
@@ -1,83 +0,0 @@
1
- """Validated acceptance records shared by execution and lifecycle gates."""
2
- from __future__ import annotations
3
-
4
- import json
5
- import math
6
- from pathlib import Path
7
- from typing import Mapping
8
-
9
-
10
- def verified_evidence(row: Mapping[str, object]) -> bool:
11
- evidence = row.get("evidence")
12
- if row.get("status") != "verified" or not isinstance(evidence, dict):
13
- return False
14
- if row.get("kind") != "manual":
15
- return evidence.get("status") == "passed" and evidence.get("exit_code") == 0
16
- author, detail = evidence.get("confirmed_by"), evidence.get("evidence")
17
- agents = {"agent", "assistant", "codex", "claude", "opencode", "costrict"}
18
- return (isinstance(author, str) and bool(author.strip())
19
- and author.strip().lower().split(":", 1)[0] not in agents
20
- and isinstance(detail, str) and bool(detail.strip()))
21
-
22
-
23
- def validate_scenarios(value: object) -> list[dict[str, object]]:
24
- if not isinstance(value, list):
25
- raise ValueError("invalid manifest scenarios: expected list")
26
- seen: set[str] = set()
27
- for item in value:
28
- if not isinstance(item, dict):
29
- raise ValueError("invalid scenario: expected object")
30
- scenario_id = item.get("id")
31
- if not isinstance(scenario_id, str) or not scenario_id.strip():
32
- raise ValueError("scenario id missing")
33
- if scenario_id in seen:
34
- raise ValueError(f"duplicate scenario id: {scenario_id}")
35
- seen.add(scenario_id)
36
- for field in ("kind", "owner", "cwd"):
37
- if field in item and not isinstance(item[field], str):
38
- raise ValueError(f"invalid scenario {scenario_id}:{field}")
39
- command = item.get("command")
40
- if command is not None and (not isinstance(command, list) or not all(isinstance(x, str) for x in command)):
41
- raise ValueError(f"invalid scenario {scenario_id}:command")
42
- timeout = item.get("timeout", 60)
43
- if isinstance(timeout, bool) or not isinstance(timeout, (int, float)) or not math.isfinite(timeout) or timeout <= 0:
44
- raise ValueError(f"invalid scenario {scenario_id}:timeout")
45
- deps = item.get("depends_on", [])
46
- if not isinstance(deps, list) or not all(isinstance(x, str) for x in deps):
47
- raise ValueError(f"invalid scenario {scenario_id}:depends_on")
48
- if "runs" in item and not isinstance(item["runs"], list):
49
- raise ValueError(f"invalid scenario {scenario_id}:runs")
50
- revision = item.get("revision", 0)
51
- if isinstance(revision, bool) or not isinstance(revision, int) or revision < 0:
52
- raise ValueError(f"invalid scenario {scenario_id}:revision")
53
- for item in value:
54
- unknown = set(item.get("depends_on", [])) - seen
55
- if unknown:
56
- raise ValueError(f"acceptance scenario dependency missing: {', '.join(sorted(unknown))}")
57
- return value
58
-
59
-
60
- def load_manifest(path: Path) -> dict[str, object]:
61
- data = json.loads(path.read_text(encoding="utf-8"))
62
- if not isinstance(data, dict):
63
- raise ValueError("invalid acceptance manifest: expected object")
64
- validate_scenarios(data.get("scenarios"))
65
- if "task_file" in data and not isinstance(data["task_file"], str):
66
- raise ValueError("invalid acceptance manifest: task_file")
67
- return data
68
-
69
-
70
- def validate_execution_baseline(path: Path, data: Mapping[str, object]) -> None:
71
- """Legacy standalone manifests remain usable; locked plans must match."""
72
- task_name = data.get("task_file")
73
- if not isinstance(task_name, str) or not task_name:
74
- return
75
- task = Path(task_name)
76
- task = task if task.is_absolute() else path.parent / task
77
- if not task.is_file():
78
- raise ValueError(f"acceptance task file missing: {task}")
79
- if "## Acceptance Coverage" in task.read_text(encoding="utf-8"):
80
- from cf_acceptance_manifest import validate_manifest
81
- valid, reason = validate_manifest(str(task), str(path))
82
- if not valid:
83
- raise ValueError(reason)
@@ -1,112 +0,0 @@
1
- #!/usr/bin/env python3
2
- """Shared execution substrate: deadline-aware argv subprocess runner.
3
-
4
- All check executors (acceptance scenarios, Spec verifiers, session
5
- validators) funnel through here so one entry deadline constrains the whole
6
- chain, commands never pass through a shell, and unrun work is reported as
7
- `incomplete` instead of silently skipped or falsely passed.
8
- """
9
-
10
- from __future__ import annotations
11
-
12
- import shlex
13
- import json
14
- import subprocess
15
- import time
16
- from contextlib import contextmanager
17
- from contextvars import ContextVar
18
- from pathlib import Path
19
- from typing import Iterator, Mapping, Optional, Sequence
20
-
21
- _EXECUTIONS: ContextVar[Optional[dict[str, dict[str, object]]]] = ContextVar("cf_executions", default=None)
22
-
23
-
24
- @contextmanager
25
- def execution_session() -> Iterator[None]:
26
- """Share results only inside one validation operation, never across turns."""
27
- if _EXECUTIONS.get() is not None:
28
- yield
29
- return
30
- token = _EXECUTIONS.set({})
31
- try:
32
- yield
33
- finally:
34
- _EXECUTIONS.reset(token)
35
-
36
-
37
- def invalidate_executions() -> None:
38
- cache = _EXECUTIONS.get()
39
- if cache is not None:
40
- cache.clear()
41
-
42
-
43
- def execution_key(argv: Sequence[str], cwd: str, timeout: float) -> str:
44
- """Identity of a command under the current process environment."""
45
- return json.dumps([list(argv), str(Path(cwd).resolve()), float(timeout)], ensure_ascii=False)
46
-
47
-
48
- def remaining_seconds(deadline: Optional[float]) -> Optional[float]:
49
- """Seconds left until the absolute monotonic deadline (None = unbounded)."""
50
- if deadline is None:
51
- return None
52
- return deadline - time.monotonic()
53
-
54
-
55
- def build_argv(template: str, files: Sequence[str]) -> list[str]:
56
- """Expand a validator command template into argv without a shell.
57
-
58
- `{files}` must be a standalone token and expands to one argv entry per
59
- file (spaces/metachars safe). Embedded usage is a config error — refusing
60
- is safer than reintroducing shell splitting.
61
- """
62
- tokens = shlex.split(template or "")
63
- if not tokens:
64
- return []
65
- argv: list[str] = []
66
- for token in tokens:
67
- if token == "{files}":
68
- argv.extend(files)
69
- elif "{files}" in token:
70
- raise ValueError(f"validator command must keep {{files}} as a standalone token: {template!r}")
71
- else:
72
- argv.append(token)
73
- return argv
74
-
75
-
76
- def run_command(
77
- argv: Sequence[str],
78
- cwd: str,
79
- timeout: float,
80
- deadline: Optional[float] = None,
81
- ) -> dict[str, object]:
82
- """Run argv synchronously under timeout+deadline. Never uses a shell."""
83
- key = execution_key(argv, cwd, timeout)
84
- cache = _EXECUTIONS.get()
85
- if cache is not None and key in cache:
86
- return dict(cache[key])
87
- result = _execute(argv, cwd, timeout, deadline)
88
- if cache is not None and result["status"] == "ok":
89
- cache[key] = result
90
- return result
91
-
92
-
93
- def _execute(argv: Sequence[str], cwd: str, timeout: float, deadline: Optional[float]) -> dict[str, object]:
94
- remaining = remaining_seconds(deadline)
95
- if remaining is not None and remaining <= 0:
96
- return {"status": "deadline_exceeded", "returncode": None, "stdout": "", "stderr": ""}
97
- effective = min(timeout, remaining) if remaining is not None else timeout
98
- try:
99
- proc = subprocess.run(
100
- list(argv), cwd=cwd, capture_output=True, text=True, timeout=max(effective, 0.01)
101
- )
102
- except subprocess.TimeoutExpired as exc:
103
- out = exc.stdout
104
- return {
105
- "status": "timeout",
106
- "returncode": None,
107
- "stdout": (out.decode() if isinstance(out, bytes) else (out or ""))[-4000:],
108
- "stderr": "",
109
- }
110
- except OSError as exc:
111
- return {"status": "spawn_error", "returncode": None, "stdout": "", "stderr": str(exc)}
112
- return {"status": "ok", "returncode": proc.returncode, "stdout": proc.stdout or "", "stderr": proc.stderr or ""}