@michaelxwb/code-flow 0.6.5 → 1.0.0

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 (177) hide show
  1. package/README.md +75 -110
  2. package/bin/code-flow.js +12 -0
  3. package/docs/README.md +19 -0
  4. package/docs/USAGE.md +268 -0
  5. package/docs/v1-design.md +1858 -0
  6. package/docs/v1-implementation.md +186 -0
  7. package/lib/adapters.js +126 -0
  8. package/lib/cli.js +258 -0
  9. package/lib/config.js +85 -0
  10. package/lib/doctor.js +283 -0
  11. package/lib/frontmatter.js +42 -0
  12. package/lib/fs.js +141 -0
  13. package/lib/glob.js +23 -0
  14. package/lib/hook.js +99 -0
  15. package/lib/init.js +132 -0
  16. package/lib/learn.js +170 -0
  17. package/lib/locks.js +52 -0
  18. package/lib/markdown.js +13 -0
  19. package/lib/process.js +116 -0
  20. package/lib/render.js +115 -0
  21. package/lib/repo.js +48 -0
  22. package/lib/runs.js +155 -0
  23. package/lib/session.js +40 -0
  24. package/lib/spec.js +80 -0
  25. package/lib/task.js +258 -0
  26. package/lib/taskdoc.js +308 -0
  27. package/lib/toml.js +140 -0
  28. package/lib/verify.js +168 -0
  29. package/lib/worktree.js +930 -0
  30. package/package.json +21 -9
  31. package/templates/adapters/opencode.mjs +65 -0
  32. package/templates/config.json +39 -0
  33. package/templates/instructions.md +13 -0
  34. package/templates/launcher/bridge.cjs +295 -0
  35. package/templates/launcher/hook.cmd +15 -0
  36. package/templates/launcher/hook.ps1 +10 -0
  37. package/templates/launcher/hook.sh +20 -0
  38. package/templates/skills/cf-check.md +19 -0
  39. package/templates/skills/cf-init.md +16 -0
  40. package/templates/skills/cf-spec.md +20 -0
  41. package/templates/skills/cf-task-align.md +19 -0
  42. package/templates/skills/cf-task-archive.md +20 -0
  43. package/templates/skills/cf-task-plan.md +31 -0
  44. package/templates/skills/cf-task-prd.md +14 -0
  45. package/templates/skills/cf-task-start.md +41 -0
  46. package/{src/core/code-flow/specs/shared/design → templates/workflow}/design-frontend.md +22 -19
  47. package/{src/core/code-flow/specs/shared/design → templates/workflow}/design-full.md +36 -28
  48. package/{src/core/code-flow/specs/shared/design → templates/workflow}/design-lite.md +27 -17
  49. package/{src/core/code-flow/specs/shared/prd-template.md → templates/workflow/prd.md} +1 -1
  50. package/templates/workflow/project-guidelines.md +48 -0
  51. package/templates/workflow/scenarios.md +67 -0
  52. package/templates/workflow/task.md +56 -0
  53. package/docs/migrations/spec-workflow-0.6.0.md +0 -42
  54. package/src/adapters/claude/CLAUDE.md +0 -45
  55. package/src/adapters/claude/commands/cf-init.md +0 -264
  56. package/src/adapters/claude/commands/cf-learn.md +0 -291
  57. package/src/adapters/claude/commands/cf-spec.md +0 -67
  58. package/src/adapters/claude/commands/cf-stats.md +0 -86
  59. package/src/adapters/claude/commands/cf-sync.md +0 -25
  60. package/src/adapters/claude/commands/cf-task/align.md +0 -309
  61. package/src/adapters/claude/commands/cf-task/archive.md +0 -161
  62. package/src/adapters/claude/commands/cf-task/block.md +0 -36
  63. package/src/adapters/claude/commands/cf-task/graph.md +0 -80
  64. package/src/adapters/claude/commands/cf-task/note.md +0 -236
  65. package/src/adapters/claude/commands/cf-task/plan.md +0 -363
  66. package/src/adapters/claude/commands/cf-task/prd.md +0 -162
  67. package/src/adapters/claude/commands/cf-task/start.md +0 -304
  68. package/src/adapters/claude/commands/cf-task/status.md +0 -59
  69. package/src/adapters/claude/commands/cf-task/verify-e2e.md +0 -115
  70. package/src/adapters/claude/commands/cf-validate.md +0 -75
  71. package/src/adapters/claude/settings.local.json +0 -50
  72. package/src/adapters/codex/AGENTS.md +0 -45
  73. package/src/adapters/codex/config.toml +0 -3
  74. package/src/adapters/codex/hooks.json +0 -50
  75. package/src/adapters/codex/skills/cf-init/SKILL.md +0 -273
  76. package/src/adapters/codex/skills/cf-learn/SKILL.md +0 -296
  77. package/src/adapters/codex/skills/cf-spec/SKILL.md +0 -60
  78. package/src/adapters/codex/skills/cf-stats/SKILL.md +0 -87
  79. package/src/adapters/codex/skills/cf-sync/SKILL.md +0 -30
  80. package/src/adapters/codex/skills/cf-task-align/SKILL.md +0 -310
  81. package/src/adapters/codex/skills/cf-task-archive/SKILL.md +0 -162
  82. package/src/adapters/codex/skills/cf-task-block/SKILL.md +0 -37
  83. package/src/adapters/codex/skills/cf-task-graph/SKILL.md +0 -81
  84. package/src/adapters/codex/skills/cf-task-note/SKILL.md +0 -233
  85. package/src/adapters/codex/skills/cf-task-plan/SKILL.md +0 -324
  86. package/src/adapters/codex/skills/cf-task-prd/SKILL.md +0 -163
  87. package/src/adapters/codex/skills/cf-task-start/SKILL.md +0 -305
  88. package/src/adapters/codex/skills/cf-task-status/SKILL.md +0 -60
  89. package/src/adapters/codex/skills/cf-task-verify-e2e/SKILL.md +0 -111
  90. package/src/adapters/codex/skills/cf-validate/SKILL.md +0 -76
  91. package/src/adapters/costrict/CLAUDE.md +0 -45
  92. package/src/adapters/costrict/commands/cf-init.md +0 -264
  93. package/src/adapters/costrict/commands/cf-learn.md +0 -291
  94. package/src/adapters/costrict/commands/cf-spec.md +0 -67
  95. package/src/adapters/costrict/commands/cf-stats.md +0 -86
  96. package/src/adapters/costrict/commands/cf-sync.md +0 -25
  97. package/src/adapters/costrict/commands/cf-task/align.md +0 -309
  98. package/src/adapters/costrict/commands/cf-task/archive.md +0 -161
  99. package/src/adapters/costrict/commands/cf-task/block.md +0 -36
  100. package/src/adapters/costrict/commands/cf-task/graph.md +0 -80
  101. package/src/adapters/costrict/commands/cf-task/note.md +0 -236
  102. package/src/adapters/costrict/commands/cf-task/plan.md +0 -363
  103. package/src/adapters/costrict/commands/cf-task/prd.md +0 -162
  104. package/src/adapters/costrict/commands/cf-task/start.md +0 -304
  105. package/src/adapters/costrict/commands/cf-task/status.md +0 -59
  106. package/src/adapters/costrict/commands/cf-task/verify-e2e.md +0 -115
  107. package/src/adapters/costrict/commands/cf-validate.md +0 -75
  108. package/src/adapters/costrict/settings.local.json +0 -50
  109. package/src/adapters/opencode/AGENTS.md +0 -45
  110. package/src/adapters/opencode/commands/cf-init.md +0 -281
  111. package/src/adapters/opencode/commands/cf-learn.md +0 -295
  112. package/src/adapters/opencode/commands/cf-spec.md +0 -59
  113. package/src/adapters/opencode/commands/cf-stats.md +0 -90
  114. package/src/adapters/opencode/commands/cf-sync.md +0 -29
  115. package/src/adapters/opencode/commands/cf-task/align.md +0 -314
  116. package/src/adapters/opencode/commands/cf-task/archive.md +0 -166
  117. package/src/adapters/opencode/commands/cf-task/block.md +0 -40
  118. package/src/adapters/opencode/commands/cf-task/graph.md +0 -84
  119. package/src/adapters/opencode/commands/cf-task/note.md +0 -240
  120. package/src/adapters/opencode/commands/cf-task/plan.md +0 -363
  121. package/src/adapters/opencode/commands/cf-task/prd.md +0 -167
  122. package/src/adapters/opencode/commands/cf-task/start.md +0 -309
  123. package/src/adapters/opencode/commands/cf-task/status.md +0 -63
  124. package/src/adapters/opencode/commands/cf-task/verify-e2e.md +0 -115
  125. package/src/adapters/opencode/commands/cf-validate.md +0 -79
  126. package/src/adapters/opencode/opencode.json +0 -3
  127. package/src/adapters/opencode/plugins/code-flow/index.js +0 -213
  128. package/src/adapters/opencode/plugins/code-flow/package.json +0 -6
  129. package/src/cli.js +0 -1023
  130. package/src/core/code-flow/config.yml +0 -158
  131. package/src/core/code-flow/scripts/cf_acceptance_evidence.py +0 -100
  132. package/src/core/code-flow/scripts/cf_acceptance_manifest.py +0 -294
  133. package/src/core/code-flow/scripts/cf_acceptance_runner.py +0 -192
  134. package/src/core/code-flow/scripts/cf_acceptance_schema.py +0 -83
  135. package/src/core/code-flow/scripts/cf_checks.py +0 -336
  136. package/src/core/code-flow/scripts/cf_core.py +0 -787
  137. package/src/core/code-flow/scripts/cf_exec_base.py +0 -112
  138. package/src/core/code-flow/scripts/cf_feedback.py +0 -56
  139. package/src/core/code-flow/scripts/cf_log.py +0 -154
  140. package/src/core/code-flow/scripts/cf_post_hook.py +0 -196
  141. package/src/core/code-flow/scripts/cf_pre_tool_hook.py +0 -104
  142. package/src/core/code-flow/scripts/cf_scan.py +0 -295
  143. package/src/core/code-flow/scripts/cf_session_state.py +0 -37
  144. package/src/core/code-flow/scripts/cf_spec_context.py +0 -1746
  145. package/src/core/code-flow/scripts/cf_spec_gate.py +0 -264
  146. package/src/core/code-flow/scripts/cf_spec_metadata.py +0 -384
  147. package/src/core/code-flow/scripts/cf_spec_migrate.py +0 -512
  148. package/src/core/code-flow/scripts/cf_spec_resolver.py +0 -347
  149. package/src/core/code-flow/scripts/cf_spec_router.py +0 -244
  150. package/src/core/code-flow/scripts/cf_spec_session.py +0 -119
  151. package/src/core/code-flow/scripts/cf_spec_verify.py +0 -559
  152. package/src/core/code-flow/scripts/cf_stats.py +0 -506
  153. package/src/core/code-flow/scripts/cf_stop_hook.py +0 -381
  154. package/src/core/code-flow/scripts/cf_sync.py +0 -140
  155. package/src/core/code-flow/scripts/cf_task_index.py +0 -148
  156. package/src/core/code-flow/scripts/cf_task_parallel.py +0 -394
  157. package/src/core/code-flow/scripts/cf_task_runtime.py +0 -304
  158. package/src/core/code-flow/scripts/cf_task_state.py +0 -3
  159. package/src/core/code-flow/scripts/cf_task_workflow.py +0 -205
  160. package/src/core/code-flow/scripts/cf_user_prompt_hook.py +0 -178
  161. package/src/core/code-flow/scripts/cf_validation.py +0 -81
  162. package/src/core/code-flow/scripts/cf_workflow_service.py +0 -366
  163. package/src/core/code-flow/scripts/cf_workflow_transaction.py +0 -96
  164. package/src/core/code-flow/specs/backend/_map.md +0 -56
  165. package/src/core/code-flow/specs/backend/code-quality-performance.md +0 -59
  166. package/src/core/code-flow/specs/backend/database.md +0 -51
  167. package/src/core/code-flow/specs/backend/directory-structure.md +0 -54
  168. package/src/core/code-flow/specs/backend/logging.md +0 -50
  169. package/src/core/code-flow/specs/backend/platform-rules.md +0 -55
  170. package/src/core/code-flow/specs/frontend/_map.md +0 -52
  171. package/src/core/code-flow/specs/frontend/component-specs.md +0 -68
  172. package/src/core/code-flow/specs/frontend/directory-structure.md +0 -53
  173. package/src/core/code-flow/specs/frontend/quality-standards.md +0 -58
  174. package/src/core/code-flow/specs/shared/_map.md +0 -57
  175. package/src/core/code-flow/tasks/.gitkeep +0 -0
  176. package/src/core/code-flow/validation.yml +0 -53
  177. package/src/migrate/spec-workflow.js +0 -238
package/lib/render.js ADDED
@@ -0,0 +1,115 @@
1
+ 'use strict';
2
+ const path = require('node:path');
3
+ const f = require('./fs');
4
+ const config = require('./config');
5
+ const spec = require('./spec');
6
+ const glob = require('./glob');
7
+ function diagnostic(error, source) {
8
+ return { code: error.code || 'read_error', source, message: error.message };
9
+ }
10
+ function requests(settings, task, paths) {
11
+ const result = settings.always.map(ref => ({ ref, priority: 0, source: 'always' }));
12
+ for (const ref of task?.specRefs ?? []) result.push({ ref, priority: ref.includes('#') ? 1 : 2, source: 'task.spec_refs' });
13
+ for (const file of paths) {
14
+ for (const [pattern, refs] of Object.entries(settings.paths)) {
15
+ if (glob.matches(pattern, file)) {
16
+ for (const ref of refs) result.push({ ref, priority: 3, source: 'path:' + file });
17
+ }
18
+ }
19
+ }
20
+ return result.sort((a, b) => a.priority - b.priority);
21
+ }
22
+ function select(root, entries, diagnostics) {
23
+ const loaded = new Map(), selected = new Map();
24
+ for (const entry of entries) {
25
+ try {
26
+ const ref = config.reference(entry.ref);
27
+ if (!loaded.has(ref.id)) loaded.set(ref.id, spec.load(root, ref.id));
28
+ const document = loaded.get(ref.id);
29
+ const rules = ref.rule ? document.rules.filter(rule => rule.id === ref.rule) : document.rules;
30
+ f.demand(!ref.rule || rules.length, 'rule_missing', 'Missing Rule: ' + entry.ref);
31
+ for (const rule of rules) {
32
+ const key = ref.id + '#' + rule.id;
33
+ if (!selected.has(key)) selected.set(key, { ref: key, spec: ref.id, rule: rule.id,
34
+ text: rule.text, source: '.code-flow/specs/' + ref.id + '.md', selectedBy: [] });
35
+ const item = selected.get(key);
36
+ if (!item.selectedBy.includes(entry.source)) item.selectedBy.push(entry.source);
37
+ }
38
+ } catch (error) { diagnostics.push(diagnostic(error, entry.ref)); }
39
+ }
40
+ return [...selected.values()];
41
+ }
42
+ function estimate(text) {
43
+ const ascii = (text.match(/[\x00-\x7f]/g) || []).length;
44
+ return Math.ceil(ascii / 4) + [...text].length - ascii;
45
+ }
46
+ function ruleText(rule, previous) {
47
+ const header = !previous || previous.spec !== rule.spec || previous.source !== rule.source ||
48
+ previous.selectedBy.join(', ') !== rule.selectedBy.join(', ')
49
+ ? '### ' + rule.spec + '\nsource: ' + rule.source +
50
+ '\nselected-by: ' + rule.selectedBy.join(', ') + '\n'
51
+ : '';
52
+ return header + '\n- [' + rule.rule + '] ' + rule.text + '\n';
53
+ }
54
+ function fits(text, budget) {
55
+ return estimate(text) <= budget.maxTokens && Buffer.byteLength(text, 'utf8') <= budget.maxBytes;
56
+ }
57
+ function render(selected, settings, context) {
58
+ const heading = '## Spec Constraints (latest)\n\n' +
59
+ (context.session ? 'session: ' + context.session + '\n' : '') +
60
+ (context.task ? 'task: ' + context.task + '\n' : '') +
61
+ 'workflow: ' + settings.workflow + '\n';
62
+ const readMore = '\nread-more: code-flow spec inject' +
63
+ (context.task ? ' --task-file "' + context.taskFile + '" --task ' + context.taskId : '') + ' --full\n';
64
+ const reserve = '\nomitted: ' + selected.slice(0, 3).map(item => item.ref).join(', ') + readMore;
65
+ const emitted = [], omitted = [];
66
+ let text = heading, last = null;
67
+ for (const item of selected) {
68
+ const addition = '\n' + ruleText(item, last);
69
+ if (context.full || fits(text + addition + reserve, settings.budget)) {
70
+ text += addition; emitted.push(item.ref); last = item;
71
+ } else omitted.push(item);
72
+ }
73
+ if (omitted.length) text += '\nomitted: ' + omitted.slice(0, 3).map(item => item.ref).join(', ') +
74
+ (omitted.length > 3 ? ' (+' + (omitted.length - 3) + ')' : '') + readMore;
75
+ if (!context.full && !fits(text, settings.budget)) {
76
+ text = 'Spec budget too small. Run code-flow spec inject --full.\n';
77
+ }
78
+ return { text, emitted, omitted: omitted.map(item => ({ ref: item.ref, source: item.source })) };
79
+ }
80
+ function normalizePaths(root, paths) {
81
+ return [...new Set(paths.map(file => {
82
+ const relative = path.isAbsolute(file) ? path.relative(root, file) : file;
83
+ f.inside(root, relative);
84
+ return relative.replace(/\\/g, '/').replace(/^\.\//, '');
85
+ }))];
86
+ }
87
+ function inject(root, options = {}) {
88
+ const settings = config.load(root), diagnostics = [];
89
+ let task = null;
90
+ if (options.taskFile) {
91
+ try { task = require('./task').loadTask(root, options.taskFile, options.taskId); } catch (error) {
92
+ if (options.strictTask) throw error;
93
+ diagnostics.push(diagnostic(error, options.taskFile));
94
+ }
95
+ }
96
+ const paths = [];
97
+ for (const file of options.paths ?? []) {
98
+ try { paths.push(...normalizePaths(root, [file])); } catch (error) { diagnostics.push(diagnostic(error, file)); }
99
+ }
100
+ const entries = requests(settings, task, paths), selected = select(root, entries, diagnostics);
101
+ let catalog = [];
102
+ if (!task && entries.length === settings.always.length) {
103
+ const listing = spec.list(root, false); catalog = listing.items; diagnostics.push(...listing.diagnostics);
104
+ }
105
+ const identity = task ? task.relative + '#' + task.id : null;
106
+ const result = render(selected, settings, { ...options, task: identity,
107
+ taskFile: task?.relative, taskId: task?.id });
108
+ if (catalog.length) {
109
+ const navigation = '\n## Spec Catalog\n' + catalog.slice(0, 20).map(item => item.id + ': ' + item.description).join('\n') +
110
+ '\n' + catalog.length + ' Specs; code-flow spec list --json\n';
111
+ if (options.full || fits(result.text + navigation, settings.budget)) result.text += navigation;
112
+ }
113
+ return { ...result, selected, diagnostics, catalog, task: identity, taskFile: task?.relative ?? null, taskId: task?.id ?? null };
114
+ }
115
+ module.exports = { inject, select, requests, render, estimate, normalizePaths };
package/lib/repo.js ADDED
@@ -0,0 +1,48 @@
1
+ 'use strict';
2
+ const fs = require('node:fs');
3
+ const path = require('node:path');
4
+ const { spawnSync } = require('node:child_process');
5
+ const f = require('./fs');
6
+
7
+ function run(root, args) {
8
+ const result = spawnSync('git', args, { cwd: root, encoding: 'utf8', maxBuffer: 1024 * 1024 });
9
+ if (result.error) {
10
+ throw new f.FlowError('git_identity', 'Cannot read Git identity: ' + result.error.message, 2);
11
+ }
12
+ f.demand(result.status === 0, 'git_identity',
13
+ 'Cannot read Git identity: ' + (result.stderr || 'git exited ' + result.status).trim(), 2);
14
+ return result.stdout.trim();
15
+ }
16
+
17
+ function identity(root) {
18
+ const real = f.realpath(root);
19
+ if (!fs.existsSync(path.join(real, '.git'))) return { git: false, root: real, linked: false };
20
+ const gitDir = path.resolve(real, run(real, ['rev-parse', '--git-dir']));
21
+ const commonDir = path.resolve(real, run(real, ['rev-parse', '--git-common-dir']));
22
+ const top = f.realpath(run(real, ['rev-parse', '--show-toplevel']));
23
+ const linked = f.realpath(gitDir) !== f.realpath(commonDir);
24
+ const branchResult = spawnSync('git', ['symbolic-ref', '-q', 'HEAD'], { cwd: real, encoding: 'utf8' });
25
+ return { git: true, root: real, top, git_dir: f.realpath(gitDir),
26
+ git_common_dir: f.realpath(commonDir), branch: branchResult.status === 0 ? branchResult.stdout.trim() : null,
27
+ linked };
28
+ }
29
+
30
+ function state(root) {
31
+ const real = f.realpath(root);
32
+ if (!fs.existsSync(path.join(real, '.git'))) return { head: null, dirty: null };
33
+ const head = spawnSync('git', ['rev-parse', 'HEAD'], { cwd: real, encoding: 'utf8' });
34
+ const status = spawnSync('git', ['status', '--porcelain', '--untracked-files=all'],
35
+ { cwd: real, encoding: 'utf8', maxBuffer: 8 * 1024 * 1024 });
36
+ const dirty = status.status !== 0 ? null : status.stdout.split('\n').filter(Boolean)
37
+ .some(line => !line.slice(3).trim().replace(/^"|"$/g, '').startsWith('.code-flow/'));
38
+ return { head: head.status === 0 ? head.stdout.trim() : null, dirty };
39
+ }
40
+
41
+ function requireCoordinator(root, operation) {
42
+ const current = identity(root);
43
+ f.demand(!current.linked, 'coordinator_required',
44
+ operation + ' must run in the coordinator worktree: ' + current.top, 1);
45
+ return current;
46
+ }
47
+
48
+ module.exports = { identity, requireCoordinator, state };
package/lib/runs.js ADDED
@@ -0,0 +1,155 @@
1
+ 'use strict';
2
+ const fs = require('node:fs');
3
+ const path = require('node:path');
4
+ const { spawnSync } = require('node:child_process');
5
+ const f = require('./fs');
6
+ const repo = require('./repo');
7
+
8
+ const UNFINISHED = new Set(['preparing', 'running', 'merging', 'verifying', 'cleaning', 'blocked']);
9
+ const STATUSES = new Set([...UNFINISHED, 'done', 'aborted']);
10
+ const WORKTREES = '.code-flow/worktrees';
11
+
12
+ function validateLedger(ledger, runId) {
13
+ const errors = [];
14
+ if (!ledger || typeof ledger !== 'object' || Array.isArray(ledger)) return ['Run ledger must be an object'];
15
+ if (typeof ledger.run_id !== 'string' || !/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(ledger.run_id)) {
16
+ errors.push('Invalid run identity');
17
+ } else if (runId !== undefined && ledger.run_id !== runId) {
18
+ errors.push('Run ledger identity does not match: ' + runId);
19
+ }
20
+ if (!STATUSES.has(ledger.status)) errors.push('Invalid run status: ' + String(ledger.status));
21
+ if (!ledger.coordinator || typeof ledger.coordinator !== 'object' || Array.isArray(ledger.coordinator) ||
22
+ typeof ledger.coordinator.root !== 'string' || typeof ledger.coordinator.branch !== 'string' ||
23
+ typeof ledger.coordinator.git_common_dir !== 'string') {
24
+ errors.push('Run ledger has no coordinator identity');
25
+ }
26
+ if (!Array.isArray(ledger.worktrees)) errors.push('Run ledger has no worktree list');
27
+ if (!ledger.merge || typeof ledger.merge !== 'object' || !Array.isArray(ledger.merge.order)) {
28
+ errors.push('Run ledger has no merge order');
29
+ }
30
+ if (errors.length) return errors;
31
+ for (const worker of ledger.worktrees) {
32
+ if (!worker || typeof worker !== 'object' || !/^TASK-\d+$/.test(worker.task) ||
33
+ worker.path !== WORKTREES + '/' + ledger.run_id + '/' + worker.task ||
34
+ worker.branch !== 'cf-task/' + ledger.run_id + '/' + worker.task) {
35
+ errors.push('Run ledger owns an invalid worker resource: ' + String(worker?.task));
36
+ }
37
+ }
38
+ if (errors.length) return errors;
39
+ const tasks = ledger.worktrees.map(worker => worker.task).sort();
40
+ if (JSON.stringify(tasks) !== JSON.stringify([...ledger.merge.order].sort())) {
41
+ errors.push('Run ledger merge order does not match worktrees');
42
+ }
43
+ return errors;
44
+ }
45
+
46
+ function list(root) {
47
+ const base = path.join(root, WORKTREES);
48
+ if (!fs.existsSync(base)) return [];
49
+ const runs = [];
50
+ for (const entry of fs.readdirSync(base, { withFileTypes: true })) {
51
+ if (!entry.isDirectory()) continue;
52
+ const file = path.join(base, entry.name, 'run.json');
53
+ if (!fs.existsSync(file)) continue;
54
+ try {
55
+ const ledger = JSON.parse(fs.readFileSync(file, 'utf8'));
56
+ const taskFile = typeof ledger?.task_file === 'string'
57
+ ? ledger.task_file.replace(/\\/g, '/').replace(/^\.\//, '') : null;
58
+ const errors = validateLedger(ledger, entry.name);
59
+ runs.push(errors.length
60
+ ? { run_id: entry.name, status: 'invalid', task_file: null,
61
+ file: path.join(WORKTREES, entry.name, 'run.json'), error: errors[0] }
62
+ : { run_id: ledger.run_id, status: ledger.status,
63
+ task_file: taskFile, file: path.join(WORKTREES, entry.name, 'run.json'), ledger });
64
+ } catch (error) {
65
+ runs.push({ run_id: entry.name, status: 'invalid', task_file: null,
66
+ file: path.join(WORKTREES, entry.name, 'run.json'), error: error.message });
67
+ }
68
+ }
69
+ return runs;
70
+ }
71
+
72
+ function unfinished(root, filter = () => true) {
73
+ return list(root).filter(run => (UNFINISHED.has(run.status) || run.status === 'invalid') && filter(run));
74
+ }
75
+
76
+ function assertArchiveAllowed(root, requirementDir) {
77
+ const normalized = requirementDir.replace(/\\/g, '/').replace(/^\.\//, '').replace(/\/$/, '');
78
+ const blocked = unfinished(root, run => !run.task_file ||
79
+ run.task_file === normalized || run.task_file.startsWith(normalized + '/'));
80
+ f.demand(!blocked.length, 'run_incomplete',
81
+ 'Unfinished worktree run for requirement: ' + blocked.map(run => run.run_id).join(', '), 1);
82
+ }
83
+
84
+ function assertLearnAllowed(root) {
85
+ const blocked = unfinished(root);
86
+ f.demand(!blocked.length, 'run_incomplete',
87
+ 'Unfinished worktree run before Spec learning: ' + blocked.map(run => run.run_id).join(', '), 1);
88
+ }
89
+
90
+ function readWorkerLedger(file, name, owned) {
91
+ if (!fs.existsSync(file)) {
92
+ if (owned) throw new f.FlowError('run_invalid', 'Missing run ledger: ' + name, 2);
93
+ return null;
94
+ }
95
+ try {
96
+ const ledger = JSON.parse(fs.readFileSync(file, 'utf8'));
97
+ if (!ledger || !Array.isArray(ledger.worktrees)) throw new Error('missing worktrees');
98
+ return ledger;
99
+ } catch (error) {
100
+ if (owned) throw new f.FlowError('run_invalid', 'Invalid run ledger: ' + error.message, 2);
101
+ return null;
102
+ }
103
+ }
104
+
105
+ function ledgerWorkerEnv(ledger, name, current, coordinator) {
106
+ for (const worker of ledger.worktrees) {
107
+ if (!worker.env || typeof worker.path !== 'string') continue;
108
+ if (worker.path !== WORKTREES + '/' + name + '/' + worker.task) continue;
109
+ let workerRoot;
110
+ try { workerRoot = f.realpath(path.join(coordinator, worker.path)); } catch (_) { continue; }
111
+ if (workerRoot !== current) continue;
112
+ const env = worker.env;
113
+ f.demand(env && typeof env.CF_WORKTREE_ID === 'string' &&
114
+ typeof env.CF_TASK_ID === 'string' && typeof env.CF_RUN_ID === 'string' &&
115
+ env.CF_WORKTREE_ID === name + '/' + worker.task &&
116
+ env.CF_TASK_ID === worker.task && env.CF_RUN_ID === name,
117
+ 'run_invalid', 'Invalid worker environment in run ledger', 2);
118
+ return { ...env };
119
+ }
120
+ return null;
121
+ }
122
+
123
+ function workerEnv(root) {
124
+ const current = f.realpath(root);
125
+ const identity = repo.identity(current);
126
+ if (!identity.git || !identity.linked) return {};
127
+ const result = spawnSync('git', ['worktree', 'list', '--porcelain'],
128
+ { cwd: current, encoding: 'utf8', maxBuffer: 1024 * 1024 });
129
+ f.demand(result.status === 0, 'git_identity',
130
+ 'Cannot read Git worktree registration: ' + (result.stderr || '').trim(), 2);
131
+ const main = result.stdout.split('\n').find(line => line.startsWith('worktree '));
132
+ f.demand(main, 'git_identity', 'Cannot locate coordinator worktree', 2);
133
+ const coordinator = f.realpath(main.slice(9));
134
+ const base = path.join(coordinator, WORKTREES);
135
+ if (!fs.existsSync(base)) return {};
136
+ const relative = path.relative(coordinator, current).split(path.sep).join('/');
137
+ let found = null;
138
+ for (const entry of fs.readdirSync(base, { withFileTypes: true })) {
139
+ if (!entry.isDirectory()) continue;
140
+ const owned = relative.startsWith(WORKTREES + '/' + entry.name + '/');
141
+ const ledger = readWorkerLedger(path.join(base, entry.name, 'run.json'), entry.name, owned);
142
+ if (!ledger) continue;
143
+ const env = ledgerWorkerEnv(ledger, entry.name, current, coordinator);
144
+ if (!env) {
145
+ if (owned) throw new f.FlowError('run_invalid',
146
+ 'Run ledger does not own this worker: ' + entry.name, 2);
147
+ continue;
148
+ }
149
+ f.demand(!found, 'env_conflict', 'Worktree is claimed by multiple run ledgers', 1);
150
+ found = env;
151
+ }
152
+ return found ?? {};
153
+ }
154
+
155
+ module.exports = { list, unfinished, assertArchiveAllowed, assertLearnAllowed, workerEnv, validateLedger, UNFINISHED };
package/lib/session.js ADDED
@@ -0,0 +1,40 @@
1
+ 'use strict';
2
+ const path = require('node:path');
3
+ const fs = require('node:fs');
4
+ const f = require('./fs');
5
+ function token(root, platform, sessionId) {
6
+ f.string(sessionId, 'session ID');
7
+ return f.hash(JSON.stringify([f.realpath(root), platform, sessionId]));
8
+ }
9
+ function location(root, value) {
10
+ f.demand(typeof value === 'string' && /^[a-f0-9]{64}$/.test(value),
11
+ 'session_invalid', 'Session token must be 64 lowercase hexadecimal characters');
12
+ return path.join(f.transientRoot(root), 'sessions', value + '.json');
13
+ }
14
+ function read(root, value) {
15
+ const raw = f.readText(location(root, value), 65536);
16
+ if (raw === null) return null;
17
+ let result;
18
+ try { result = JSON.parse(raw); } catch (error) {
19
+ throw new f.FlowError('session_invalid', 'Invalid session binding: ' + error.message);
20
+ }
21
+ f.object(result, 'session binding');
22
+ f.string(result.task_file, 'session task_file');
23
+ f.string(result.task_id, 'session task_id');
24
+ return result;
25
+ }
26
+ async function bind(root, value, taskFile, taskId) {
27
+ const file = location(root, value);
28
+ f.string(taskFile, 'task file');
29
+ f.string(taskId, 'task id');
30
+ await f.withLock(root, 'session:' + value, async () => {
31
+ f.atomicWrite(file, JSON.stringify({ task_file: taskFile, task_id: taskId,
32
+ bound_at: new Date().toISOString() }) + '\n', f.readText(file));
33
+ });
34
+ }
35
+ async function unbind(root, value) {
36
+ const file = location(root, value);
37
+ await f.withLock(root, 'session:' + value, async () => f.removeIfPresent(file));
38
+ return { ok: true, action: 'unbind' };
39
+ }
40
+ module.exports = { token, location, read, bind, unbind };
package/lib/spec.js ADDED
@@ -0,0 +1,80 @@
1
+ 'use strict';
2
+ const fs = require('node:fs');
3
+ const path = require('node:path');
4
+ const f = require('./fs');
5
+ const fm = require('./frontmatter');
6
+ const { reference } = require('./config');
7
+ const markdown = require('./markdown');
8
+
9
+ function parseRules(body) {
10
+ const lines = body.replace(/\r\n/g, '\n').split('\n');
11
+ const rules = [], used = new Set();
12
+ let section = false, fence = null, current = null;
13
+ const finish = end => {
14
+ if (!current) return;
15
+ current.end = end;
16
+ current.text = lines.slice(current.start, end).map((line, i) =>
17
+ i === 0 ? line.replace(/^- \[RULE-[^\]]+\] /, '') : line.replace(/^ /, '')).join('\n').trim();
18
+ f.demand(!used.has(current.id), 'duplicate_rule', 'Duplicate Rule ID: ' + current.id);
19
+ used.add(current.id);
20
+ rules.push(current); current = null;
21
+ };
22
+ for (let i = 0; i < lines.length; i++) {
23
+ const line = lines[i], marker = markdown.fenceMarker(line, fence);
24
+ if (marker) {
25
+ fence = marker.next;
26
+ continue;
27
+ }
28
+ if (fence) continue;
29
+ if (/^##\s/.test(line)) { finish(i); section = /^## Rules\s*$/.test(line); continue; }
30
+ if (!section) continue;
31
+ const match = line.match(/^- \[(RULE-[A-Za-z0-9]+(?:-[A-Za-z0-9]+)*)\] (.+)$/);
32
+ if (match) { finish(i); current = { id: match[1], start: i }; }
33
+ else if (/^- /.test(line)) {
34
+ finish(i);
35
+ throw new f.FlowError('rule_invalid', 'Rule must have a stable RULE ID: ' + line);
36
+ }
37
+ }
38
+ f.demand(!fence, 'rule_fence', 'Unclosed code fence in Spec');
39
+ finish(lines.length);
40
+ return { rules, lines };
41
+ }
42
+ function header(meta) {
43
+ f.string(meta.description, 'Spec description');
44
+ f.positive(meta.next_rule, 'Spec next_rule', true);
45
+ return meta;
46
+ }
47
+ function load(root, id) {
48
+ f.demand(!id.includes('#'), 'invalid_reference', 'Expected Spec path without Rule suffix');
49
+ reference(id);
50
+ const file = f.inside(root, '.code-flow/specs/' + id + '.md');
51
+ const raw = f.readText(file);
52
+ f.demand(raw !== null, 'spec_missing', 'Missing Spec: ' + id);
53
+ const document = fm.parse(raw);
54
+ header(document.meta);
55
+ return { ...document, ...parseRules(document.body), id, file };
56
+ }
57
+ function walk(directory, prefix = '') {
58
+ if (!fs.existsSync(directory)) return [];
59
+ return fs.readdirSync(directory, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))
60
+ .flatMap(entry => {
61
+ const relative = prefix + entry.name;
62
+ if (entry.isSymbolicLink()) return [];
63
+ if (entry.isDirectory()) return walk(path.join(directory, entry.name), relative + '/');
64
+ return entry.isFile() && entry.name.endsWith('.md') ? [relative] : [];
65
+ });
66
+ }
67
+ function list(root, full = true) {
68
+ const items = [], diagnostics = [];
69
+ for (const relative of walk(f.inside(root, '.code-flow/specs'))) {
70
+ const id = relative.slice(0, -3);
71
+ try {
72
+ reference(id);
73
+ const info = full ? load(root, id) : { meta: header(fm.readHeader(f.inside(root, '.code-flow/specs/' + relative))) };
74
+ items.push({ id, path: '.code-flow/specs/' + relative, description: info.meta.description,
75
+ ...(full ? { rules: info.rules.length } : {}) });
76
+ } catch (error) { diagnostics.push({ code: error.code || 'spec_error', source: id, message: error.message }); }
77
+ }
78
+ return { items, diagnostics };
79
+ }
80
+ module.exports = { load, list, parseRules, walk, header };