@caiqueoak/flow 0.7.0 → 0.8.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 (172) hide show
  1. package/README.md +65 -43
  2. package/dist/artifacts/backlog.d.mts +16 -0
  3. package/dist/artifacts/backlog.d.mts.map +1 -0
  4. package/dist/artifacts/backlog.mjs +193 -0
  5. package/dist/artifacts/backlog.mjs.map +1 -0
  6. package/dist/artifacts/document.d.mts +6 -0
  7. package/dist/artifacts/document.d.mts.map +1 -0
  8. package/dist/artifacts/document.mjs +39 -0
  9. package/dist/artifacts/document.mjs.map +1 -0
  10. package/dist/artifacts/engineering.d.mts +5 -0
  11. package/dist/artifacts/engineering.d.mts.map +1 -0
  12. package/dist/artifacts/engineering.mjs +27 -0
  13. package/dist/artifacts/engineering.mjs.map +1 -0
  14. package/dist/artifacts/gates.d.mts +7 -0
  15. package/dist/artifacts/gates.d.mts.map +1 -0
  16. package/dist/artifacts/gates.mjs +42 -0
  17. package/dist/artifacts/gates.mjs.map +1 -0
  18. package/dist/artifacts/implementation-plan.d.mts +11 -0
  19. package/dist/artifacts/implementation-plan.d.mts.map +1 -0
  20. package/dist/artifacts/implementation-plan.mjs +30 -0
  21. package/dist/artifacts/implementation-plan.mjs.map +1 -0
  22. package/dist/artifacts/prd.d.mts +4 -0
  23. package/dist/artifacts/prd.d.mts.map +1 -0
  24. package/dist/artifacts/prd.mjs +13 -0
  25. package/dist/artifacts/prd.mjs.map +1 -0
  26. package/dist/artifacts/review.d.mts +11 -0
  27. package/dist/artifacts/review.d.mts.map +1 -0
  28. package/dist/artifacts/review.mjs +27 -0
  29. package/dist/artifacts/review.mjs.map +1 -0
  30. package/dist/artifacts/spec.d.mts +14 -0
  31. package/dist/artifacts/spec.d.mts.map +1 -0
  32. package/dist/artifacts/spec.mjs +50 -0
  33. package/dist/artifacts/spec.mjs.map +1 -0
  34. package/dist/artifacts/state.d.mts +44 -0
  35. package/dist/artifacts/state.d.mts.map +1 -0
  36. package/dist/artifacts/state.mjs +60 -0
  37. package/dist/artifacts/state.mjs.map +1 -0
  38. package/dist/artifacts/tasks.d.mts +10 -0
  39. package/dist/artifacts/tasks.d.mts.map +1 -0
  40. package/dist/artifacts/tasks.mjs +107 -0
  41. package/dist/artifacts/tasks.mjs.map +1 -0
  42. package/dist/artifacts/work-items.d.mts +21 -0
  43. package/dist/artifacts/work-items.d.mts.map +1 -0
  44. package/dist/artifacts/work-items.mjs +81 -0
  45. package/dist/artifacts/work-items.mjs.map +1 -0
  46. package/dist/cli/command-registry.d.mts +498 -0
  47. package/dist/cli/command-registry.d.mts.map +1 -0
  48. package/dist/cli/command-registry.mjs +230 -0
  49. package/dist/cli/command-registry.mjs.map +1 -0
  50. package/dist/cli.d.mts +3 -0
  51. package/dist/cli.d.mts.map +1 -0
  52. package/dist/cli.mjs +48 -0
  53. package/dist/cli.mjs.map +1 -0
  54. package/dist/commands/doctor.d.mts +21 -0
  55. package/dist/commands/doctor.d.mts.map +1 -0
  56. package/dist/commands/doctor.mjs +79 -0
  57. package/dist/commands/doctor.mjs.map +1 -0
  58. package/dist/commands/gates.d.mts +14 -0
  59. package/dist/commands/gates.d.mts.map +1 -0
  60. package/dist/commands/gates.mjs +136 -0
  61. package/dist/commands/gates.mjs.map +1 -0
  62. package/dist/commands/graph.d.mts +11 -0
  63. package/dist/commands/graph.d.mts.map +1 -0
  64. package/dist/commands/graph.mjs +78 -0
  65. package/dist/commands/graph.mjs.map +1 -0
  66. package/dist/commands/init.d.mts +6 -0
  67. package/dist/commands/init.d.mts.map +1 -0
  68. package/dist/commands/init.mjs +126 -0
  69. package/dist/commands/init.mjs.map +1 -0
  70. package/dist/commands/migrate.d.mts +27 -0
  71. package/dist/commands/migrate.d.mts.map +1 -0
  72. package/dist/commands/migrate.mjs +379 -0
  73. package/dist/commands/migrate.mjs.map +1 -0
  74. package/dist/commands/operations.d.mts +13 -0
  75. package/dist/commands/operations.d.mts.map +1 -0
  76. package/dist/commands/operations.mjs +340 -0
  77. package/dist/commands/operations.mjs.map +1 -0
  78. package/dist/commands/route.d.mts +12 -0
  79. package/dist/commands/route.d.mts.map +1 -0
  80. package/dist/commands/route.mjs +45 -0
  81. package/dist/commands/route.mjs.map +1 -0
  82. package/dist/commands/schemas.d.mts +5 -0
  83. package/dist/commands/schemas.d.mts.map +1 -0
  84. package/dist/commands/schemas.mjs +14 -0
  85. package/dist/commands/schemas.mjs.map +1 -0
  86. package/dist/commands/status.d.mts +4 -0
  87. package/dist/commands/status.d.mts.map +1 -0
  88. package/dist/commands/status.mjs +18 -0
  89. package/dist/commands/status.mjs.map +1 -0
  90. package/dist/commands/sync.d.mts +18 -0
  91. package/dist/commands/sync.d.mts.map +1 -0
  92. package/dist/commands/sync.mjs +29 -0
  93. package/dist/commands/sync.mjs.map +1 -0
  94. package/dist/commands/trace.d.mts +30 -0
  95. package/dist/commands/trace.d.mts.map +1 -0
  96. package/dist/commands/trace.mjs +111 -0
  97. package/dist/commands/trace.mjs.map +1 -0
  98. package/dist/commands/validate.d.mts +8 -0
  99. package/dist/commands/validate.d.mts.map +1 -0
  100. package/dist/commands/validate.mjs +82 -0
  101. package/dist/commands/validate.mjs.map +1 -0
  102. package/dist/domain/contracts.d.mts +203 -0
  103. package/dist/domain/contracts.d.mts.map +1 -0
  104. package/dist/domain/contracts.mjs +126 -0
  105. package/dist/domain/contracts.mjs.map +1 -0
  106. package/dist/domain/types.d.ts +45 -0
  107. package/dist/domain/types.d.ts.map +1 -0
  108. package/dist/domain/types.js +2 -0
  109. package/dist/domain/types.js.map +1 -0
  110. package/dist/entry.d.ts +3 -0
  111. package/dist/entry.d.ts.map +1 -0
  112. package/dist/entry.js +4 -0
  113. package/dist/entry.js.map +1 -0
  114. package/dist/shared/cli-io.d.mts +15 -0
  115. package/dist/shared/cli-io.d.mts.map +1 -0
  116. package/dist/shared/cli-io.mjs +83 -0
  117. package/dist/shared/cli-io.mjs.map +1 -0
  118. package/dist/shared/profiles.d.mts +17 -0
  119. package/dist/shared/profiles.d.mts.map +1 -0
  120. package/dist/shared/profiles.mjs +25 -0
  121. package/dist/shared/profiles.mjs.map +1 -0
  122. package/dist/shared/project-config.d.mts +21 -0
  123. package/dist/shared/project-config.d.mts.map +1 -0
  124. package/dist/shared/project-config.mjs +41 -0
  125. package/dist/shared/project-config.mjs.map +1 -0
  126. package/dist/shared/project-path.d.mts +3 -0
  127. package/dist/shared/project-path.d.mts.map +1 -0
  128. package/dist/shared/project-path.mjs +9 -0
  129. package/dist/shared/project-path.mjs.map +1 -0
  130. package/dist/shared/skill-installer.d.mts +2 -0
  131. package/dist/shared/skill-installer.d.mts.map +1 -0
  132. package/dist/shared/skill-installer.mjs +33 -0
  133. package/dist/shared/skill-installer.mjs.map +1 -0
  134. package/package.json +16 -8
  135. package/schemas/backlog.schema.json +85 -0
  136. package/schemas/config.schema.json +29 -0
  137. package/schemas/gates.schema.json +17 -0
  138. package/schemas/review.schema.json +28 -0
  139. package/schemas/tasks.schema.json +66 -0
  140. package/skills/flow/SKILL.md +10 -11
  141. package/skills/flow/build/step-01-execute-task.md +2 -2
  142. package/skills/flow/discovery/bmad-comparison.md +5 -0
  143. package/skills/flow/discovery/step-01-project.md +8 -3
  144. package/skills/flow/engineering/profiles/readability-first.md +1 -0
  145. package/skills/flow/engineering/step-02-synthesize.md +1 -1
  146. package/skills/flow/invariants.md +4 -4
  147. package/skills/flow/planning/step-01-create-tasks.md +3 -0
  148. package/skills/flow/planning/step-01-plan-work-item.md +2 -2
  149. package/skills/flow/review/step-01-review-work-item.md +1 -1
  150. package/skills/flow/specification/step-01-deepen-spec.md +7 -0
  151. package/src/artifacts/backlog.mjs +0 -157
  152. package/src/artifacts/document.mjs +0 -30
  153. package/src/artifacts/engineering.mjs +0 -26
  154. package/src/artifacts/gates.mjs +0 -28
  155. package/src/artifacts/implementation-plan.mjs +0 -27
  156. package/src/artifacts/prd.mjs +0 -12
  157. package/src/artifacts/state.mjs +0 -63
  158. package/src/artifacts/tasks.mjs +0 -90
  159. package/src/cli.mjs +0 -62
  160. package/src/commands/gates.mjs +0 -71
  161. package/src/commands/graph.mjs +0 -92
  162. package/src/commands/init.mjs +0 -111
  163. package/src/commands/migrate.mjs +0 -224
  164. package/src/commands/route.mjs +0 -140
  165. package/src/commands/status.mjs +0 -35
  166. package/src/commands/trace.mjs +0 -58
  167. package/src/commands/validate.mjs +0 -197
  168. package/src/shared/cli-io.mjs +0 -86
  169. package/src/shared/profiles.mjs +0 -27
  170. package/src/shared/project-config.mjs +0 -39
  171. package/src/shared/project-path.mjs +0 -10
  172. package/src/shared/skill-installer.mjs +0 -34
@@ -1,140 +0,0 @@
1
- import fs from 'node:fs';
2
- import path from 'node:path';
3
- import { info } from '../shared/cli-io.mjs';
4
- import { projectRoot } from '../shared/project-path.mjs';
5
- import { readConfig } from '../shared/project-config.mjs';
6
- import { parseBacklog, deriveExecutionStatus } from '../artifacts/backlog.mjs';
7
- import { parseTasks, qualifiedTaskId } from '../artifacts/tasks.mjs';
8
- import { validateEngineeringDocument } from '../artifacts/engineering.mjs';
9
- import { validatePrdDocument } from '../artifacts/prd.mjs';
10
- import { validateImplementationPlan } from '../artifacts/implementation-plan.mjs';
11
- import { parseState } from '../artifacts/state.mjs';
12
-
13
- function step(phase, instruction, extra = {}) {
14
- return { action: 'continue', phase, instruction, ...extra };
15
- }
16
- function awaitApproval(phase, instruction, extra = {}) {
17
- return { action: 'stop', reason: 'consequential_decision', phase, instruction, ...extra };
18
- }
19
- function artifactApproval(file, validator, phase, draftInstruction, approvalInstruction) {
20
- if (!fs.existsSync(file)) return step(phase, draftInstruction);
21
- const result = validator(fs.readFileSync(file, 'utf8'));
22
- if (result.errors.length) return step(phase, draftInstruction, { details: result.errors });
23
- if (result.status !== 'approved') return awaitApproval(phase, approvalInstruction);
24
- return null;
25
- }
26
- export function routeProject(root) {
27
- const flow = path.join(root, '.flow');
28
- const config = readConfig(root);
29
- if (!config) return { action: 'stop', reason: 'unrecoverable_blocker', details: 'Run npx --no-install flow init.' };
30
- if (config.schema_version !== 2)
31
- return { action: 'stop', reason: 'unrecoverable_blocker', details: 'Run npx --no-install flow migrate.' };
32
- const read = (relative) => fs.readFileSync(path.join(flow, relative), 'utf8');
33
- const exists = (relative) => fs.existsSync(path.join(flow, relative));
34
- const state = exists('state.yaml') ? parseState(read('state.yaml')) : null;
35
- if (state?.migration.status === 'pending_reconciliation')
36
- return step('migration_reconciliation', 'migration/step-01-reconcile.md', {
37
- required_context: [
38
- 'backlog.yaml',
39
- 'docs/prd.md',
40
- 'docs/legacy-state.md',
41
- 'docs/legacy-decisions.md',
42
- 'docs/legacy-engineering.md'
43
- ]
44
- .filter(exists)
45
- .map((file) => `.flow/${file}`)
46
- });
47
- if (state?.stop_reason && state.stop_reason !== 'finished')
48
- return { action: 'stop', reason: state.stop_reason, phase: state.execution.phase, step: state.execution.step };
49
- const productRoute = artifactApproval(
50
- path.join(flow, 'docs/prd.md'),
51
- validatePrdDocument,
52
- 'discovery',
53
- 'discovery/step-01-project.md',
54
- 'discovery/step-02-await-approval.md'
55
- );
56
- if (productRoute) return productRoute;
57
- const engineeringRoute = artifactApproval(
58
- path.join(flow, 'docs/engineering.md'),
59
- validateEngineeringDocument,
60
- 'engineering',
61
- 'engineering/step-02-synthesize.md',
62
- 'engineering/step-05-present.md'
63
- );
64
- if (engineeringRoute) return engineeringRoute;
65
- const engineeringText = read('docs/engineering.md');
66
- const planning = () =>
67
- step('backlog_planning', 'planning/step-01-plan-work-item.md', {
68
- required_context: ['.flow/docs/prd.md', '.flow/docs/engineering.md']
69
- });
70
- if (!exists('backlog.yaml')) return planning();
71
- const backlog = parseBacklog(read('backlog.yaml'));
72
- for (const item of backlog.work_items) {
73
- if (!exists(`work-items/${item.folder}/spec.md`) || !exists(`work-items/${item.folder}/tasks.yaml`))
74
- return planning();
75
- parseTasks(read(`work-items/${item.folder}/tasks.yaml`), { expectedWorkItem: item.id });
76
- }
77
- const byId = new Map(backlog.work_items.map((item) => [item.id, item]));
78
- const active = backlog.work_items.find((item) => item.state === 'in_progress');
79
- if (active && deriveExecutionStatus(active, byId).status === 'blocked')
80
- return {
81
- action: 'stop',
82
- reason: 'dependency_blocked',
83
- work_item: active.id,
84
- details: deriveExecutionStatus(active, byId).reasons
85
- };
86
- const item =
87
- active ??
88
- backlog.work_items
89
- .filter((item) => deriveExecutionStatus(item, byId).status === 'ready')
90
- .sort((a, b) => a.priority - b.priority || Number(a.id.slice(1)) - Number(b.id.slice(1)))[0];
91
- if (!item)
92
- return {
93
- action: 'stop',
94
- reason: backlog.work_items.every((item) => item.state === 'completed') ? 'finished' : 'external_action'
95
- };
96
- const base = `work-items/${item.folder}`;
97
- const context = [
98
- '.flow/docs/prd.md',
99
- '.flow/docs/engineering.md',
100
- `.flow/${base}/spec.md`,
101
- `.flow/${base}/tasks.yaml`
102
- ];
103
- const tasks = parseTasks(read(`${base}/tasks.yaml`), { expectedWorkItem: item.id });
104
- const extra = { work_item: item.id, required_context: context };
105
- const prepare = () => step('work_item_plan_approval', 'planning/step-02-prepare-plan.md', extra);
106
- if (!exists(`${base}/implementation-plan.md`)) return prepare();
107
- const plan = validateImplementationPlan(read(`${base}/implementation-plan.md`), {
108
- workItem: item.id,
109
- engineeringText,
110
- specText: read(`${base}/spec.md`)
111
- });
112
- if (plan.errors.length) return prepare();
113
- if (plan.status !== 'approved')
114
- return awaitApproval('work_item_plan_approval', 'planning/step-03-await-approval.md', extra);
115
- context.push(`.flow/${base}/implementation-plan.md`);
116
- const taskById = new Map(tasks.tasks.map((task) => [task.id, task]));
117
- const task =
118
- tasks.tasks.find((task) => task.state === 'in_progress') ??
119
- tasks.tasks
120
- .filter(
121
- (task) => task.state === 'pending' && task.depends_on.every((id) => taskById.get(id).state === 'completed')
122
- )
123
- .sort((a, b) => Number(a.id.slice(1)) - Number(b.id.slice(1)))[0];
124
- if (task)
125
- return step('implementation', 'build/step-01-execute-task.md', {
126
- ...extra,
127
- task: qualifiedTaskId(item.id, task.id)
128
- });
129
- if (tasks.tasks.every((task) => task.state === 'completed'))
130
- return step('work_item_review', 'review/step-01-review-work-item.md', extra);
131
- return step('reconcile', 'reconcile/step-01-reconcile.md', extra);
132
- }
133
- export function runRoute({ args }) {
134
- const result = routeProject(projectRoot(args));
135
- info(
136
- args.includes('--json')
137
- ? JSON.stringify(result, null, 2)
138
- : `${result.action}: ${result.phase ?? result.reason}${result.work_item ? ` ${result.work_item}` : ''}`
139
- );
140
- }
@@ -1,35 +0,0 @@
1
- import fs from 'node:fs';
2
- import path from 'node:path';
3
- import { info } from '../shared/cli-io.mjs';
4
- import { projectRoot } from '../shared/project-path.mjs';
5
- import { deriveExecutionStatus, parseBacklog } from '../artifacts/backlog.mjs';
6
- import { routeProject } from './route.mjs';
7
-
8
- export function runStatus({ args }) {
9
- const root = projectRoot(args);
10
- if (!fs.existsSync(path.join(root, '.flow', 'backlog.yaml')))
11
- return info(`Next: ${routeProject(root).phase ?? 'setup'}`);
12
- const backlog = parseBacklog(fs.readFileSync(path.join(root, '.flow', 'backlog.yaml'), 'utf8'));
13
- const byId = new Map(backlog.work_items.map((item) => [item.id, item]));
14
- const groups = new Map(['in_progress', 'ready', 'blocked', 'completed'].map((status) => [status, []]));
15
- for (const item of backlog.work_items) {
16
- const derived = deriveExecutionStatus(item, byId);
17
- groups.get(derived.status).push({
18
- ...item,
19
- reasons: derived.reasons,
20
- blockers: item.blockers.filter((blocker) => blocker.status === 'unresolved')
21
- });
22
- }
23
- const lines = [];
24
- for (const [status, items] of groups) {
25
- if (!items.length) continue;
26
- lines.push(status.replace('_', ' ').replace(/^./, (c) => c.toUpperCase()));
27
- for (const item of items) {
28
- const deps = item.reasons.filter((r) => r.type === 'dependency').map((r) => r.ref);
29
- lines.push(` ${item.id} — ${item.title}${deps.length ? ` ← ${deps.join(', ')}` : ''}`);
30
- for (const blocker of item.blockers) lines.push(` blocker: ${blocker.description} [${blocker.id}]`);
31
- }
32
- lines.push('');
33
- }
34
- info(lines.join('\n').trim());
35
- }
@@ -1,58 +0,0 @@
1
- import { execFileSync } from 'node:child_process';
2
- import { info, fail } from '../shared/cli-io.mjs';
3
- import { projectRoot } from '../shared/project-path.mjs';
4
-
5
- export function traceTask(root, qualifiedTaskId) {
6
- if (!/^W\d{3,}-T\d{3,}$/.test(qualifiedTaskId))
7
- fail(`invalid qualified task ID '${qualifiedTaskId}'. Expected W015-T003.`);
8
- return traceTasks(root, [qualifiedTaskId]).get(qualifiedTaskId);
9
- }
10
-
11
- export function traceTasks(root, qualifiedTaskIds) {
12
- for (const qualifiedTaskId of qualifiedTaskIds)
13
- if (!/^W\d{3,}-T\d{3,}$/.test(qualifiedTaskId))
14
- fail(`invalid qualified task ID '${qualifiedTaskId}'. Expected W015-T003.`);
15
- const requested = new Set(qualifiedTaskIds);
16
- const matches = new Map([...requested].map((task) => [task, []]));
17
- let output;
18
- try {
19
- output = execFileSync('git', ['log', 'HEAD', '--format=%H%x1f%s%x1f%B%x1e'], {
20
- cwd: root,
21
- encoding: 'utf8',
22
- maxBuffer: 50 * 1024 * 1024
23
- });
24
- } catch {
25
- fail('git history is unavailable; task traceability requires a Git repository.');
26
- }
27
- for (const entry of output
28
- .split('\x1e')
29
- .filter(Boolean)
30
- .map((record) => {
31
- const [sha, subject, ...bodyParts] = record.replace(/^\n+|\n+$/g, '').split('\x1f');
32
- return { sha, subject, body: bodyParts.join('\x1f') };
33
- })) {
34
- const workItems = new Set([...entry.body.matchAll(/^Flow-Work-Item:\s*(W\d{3,})\s*$/gm)].map((match) => match[1]));
35
- for (const match of entry.body.matchAll(/^Flow-Task:\s*(W\d{3,}-T\d{3,})\s*$/gm)) {
36
- const task = match[1];
37
- if (requested.has(task) && workItems.has(task.split('-')[0])) matches.get(task).push(entry);
38
- }
39
- }
40
- return new Map(
41
- [...requested].map((task) => {
42
- const commits = matches.get(task);
43
- if (!commits.length) return [task, { task, commits, status: 'missing' }];
44
- if (commits.length > 1) return [task, { task, commits, status: 'ambiguous' }];
45
- return [task, { task, commit: commits[0], commits, status: 'resolved' }];
46
- })
47
- );
48
- }
49
-
50
- export function runTrace({ args }) {
51
- const task = args.find((arg) => !arg.startsWith('--'));
52
- if (!task) fail('flow trace requires a qualified task ID, e.g. W015-T003.');
53
- const result = traceTask(projectRoot(args), task);
54
- if (args.includes('--json')) return info(JSON.stringify(result, null, 2));
55
- if (result.status === 'missing') fail(`No reachable commit declares Flow-Task: ${task}.`);
56
- if (result.status === 'ambiguous') fail(`Multiple reachable commits declare Flow-Task: ${task}.`);
57
- info(`${task} -> ${result.commit.sha}\n${result.commit.subject}`);
58
- }
@@ -1,197 +0,0 @@
1
- import fs from 'node:fs';
2
- import path from 'node:path';
3
- import { info, fail } from '../shared/cli-io.mjs';
4
- import { projectRoot } from '../shared/project-path.mjs';
5
- import { readConfig } from '../shared/project-config.mjs';
6
- import { parseBacklog, deriveExecutionStatus } from '../artifacts/backlog.mjs';
7
- import { parseTasks, qualifiedTaskId } from '../artifacts/tasks.mjs';
8
- import { parseState } from '../artifacts/state.mjs';
9
- import { parseGates } from '../artifacts/gates.mjs';
10
- import { validateEngineeringDocument } from '../artifacts/engineering.mjs';
11
- import { validatePrdDocument } from '../artifacts/prd.mjs';
12
- import { validateImplementationPlan } from '../artifacts/implementation-plan.mjs';
13
- import { traceTasks } from './trace.mjs';
14
- import { generateGraphMarkdown } from './graph.mjs';
15
- import { evaluateGates } from './gates.mjs';
16
-
17
- export function validateProject(
18
- root,
19
- { preCommitTask = null, skipTrace = false, evaluateConfiguredGates = false, onGateResults } = {}
20
- ) {
21
- const findings = [];
22
- const error = (code, message) => findings.push({ level: 'error', code, message });
23
- const flow = path.join(root, '.flow');
24
- const exists = (relative) => fs.existsSync(path.join(flow, relative));
25
- const hasExactRootEntry = (name) => fs.readdirSync(flow).includes(name);
26
- const read = (relative) => fs.readFileSync(path.join(flow, relative), 'utf8');
27
- let config;
28
- try {
29
- config = readConfig(root);
30
- } catch (failure) {
31
- error('CONFIG', failure.message);
32
- }
33
- if (!config || config.schema_version !== 2) {
34
- error('CONFIG', 'Initialize Flow or migrate the existing project.');
35
- return findings;
36
- }
37
- if (
38
- config.engineering.profile !== 'flow/readability-first@1' ||
39
- !['improve', 'preserve', 'not_applicable'].includes(config.engineering.existing_code_policy)
40
- )
41
- error('CONFIG', 'Invalid engineering bootstrap preferences.');
42
- for (const name of ['STATE.md', 'DECISIONS.md', 'SUMMARY.md', 'BACKLOG.yaml', 'PRD.md', 'ENGINEERING.md', 'GRAPH.md'])
43
- if (hasExactRootEntry(name)) error('LEGACY', `${name} must be migrated.`);
44
- let state;
45
- try {
46
- state = exists('state.yaml') ? parseState(read('state.yaml')) : null;
47
- } catch (failure) {
48
- error('STATE', failure.message);
49
- }
50
- const migrationPending = state?.migration.status === 'pending_reconciliation';
51
- const backlogRequired = [
52
- 'backlog_planning',
53
- 'work_item_plan_approval',
54
- 'implementation',
55
- 'work_item_review',
56
- 'complete'
57
- ].includes(state?.execution.phase);
58
- if (!exists('backlog.yaml')) {
59
- if (backlogRequired) error('MISSING', 'Missing backlog.yaml.');
60
- return findings;
61
- }
62
- let backlog;
63
- try {
64
- backlog = parseBacklog(read('backlog.yaml'));
65
- } catch (failure) {
66
- error('BACKLOG', failure.message);
67
- return findings;
68
- }
69
- const anyStarted = backlog.work_items.some((item) => item.state !== 'pending');
70
- if (!migrationPending) {
71
- for (const [name, validator] of [
72
- ['prd.md', validatePrdDocument],
73
- ['engineering.md', validateEngineeringDocument]
74
- ]) {
75
- if (!exists(`docs/${name}`)) {
76
- error('MISSING', `Missing docs/${name}.`);
77
- continue;
78
- }
79
- const document = validator(read(`docs/${name}`));
80
- for (const message of document.errors) error('DOCUMENT', `${name}: ${message}`);
81
- if (anyStarted && document.status !== 'approved')
82
- error('APPROVAL', `${name} must be approved before implementation.`);
83
- }
84
- }
85
- const byId = new Map(backlog.work_items.map((item) => [item.id, item]));
86
- let activeTasks = 0;
87
- const traceCandidates = [];
88
- for (const item of backlog.work_items) {
89
- const base = `work-items/${item.folder}`;
90
- const missing = ['spec.md', 'tasks.yaml'].filter((name) => !exists(`${base}/${name}`));
91
- if (missing.length) {
92
- if (!(migrationPending && item.state === 'pending'))
93
- error('WORK_ITEM', `${item.id} is missing ${missing.join(', ')}.`);
94
- continue;
95
- }
96
- let tasks;
97
- try {
98
- tasks = parseTasks(read(`${base}/tasks.yaml`), { expectedWorkItem: item.id });
99
- } catch (failure) {
100
- error('TASKS', failure.message);
101
- continue;
102
- }
103
- const historical = tasks.tasks.length > 0 && tasks.tasks.every((task) => task.implementation === 'legacy');
104
- activeTasks += tasks.tasks.filter((task) => task.state === 'in_progress').length;
105
- if (tasks.tasks.some((task) => task.state === 'in_progress') && item.state !== 'in_progress')
106
- error('STATE', `${item.id}: active task requires active work item.`);
107
- if (item.state === 'completed' && tasks.tasks.some((task) => task.state !== 'completed'))
108
- error('STATE', `${item.id}: completed work has incomplete tasks.`);
109
- if (item.state === 'in_progress' && deriveExecutionStatus(item, byId).status === 'blocked')
110
- error('STATE', `${item.id}: active work is blocked.`);
111
- if (!historical && !migrationPending) {
112
- const spec = read(`${base}/spec.md`);
113
- for (const heading of [
114
- '## Status',
115
- '## Goal',
116
- '## Scope',
117
- '## Non-goals',
118
- '## Requirements',
119
- '## Acceptance criteria',
120
- '## Decisions'
121
- ])
122
- if (!spec.split(/\r?\n/).includes(heading)) error('SPEC', `${item.id} missing ${heading}.`);
123
- const planPath = `${base}/implementation-plan.md`;
124
- if (!exists(planPath)) {
125
- if (item.state !== 'pending') error('PLAN', `${item.id} requires an approved implementation plan.`);
126
- } else if (exists('docs/engineering.md')) {
127
- const plan = validateImplementationPlan(read(planPath), {
128
- workItem: item.id,
129
- engineeringText: read('docs/engineering.md'),
130
- specText: spec,
131
- checkRevisions: item.state !== 'completed'
132
- });
133
- for (const message of plan.errors) error('PLAN', `${item.id}: ${message}`);
134
- if (item.state !== 'pending' && plan.status !== 'approved') error('PLAN', `${item.id}: plan is not approved.`);
135
- }
136
- }
137
- for (const task of tasks.tasks)
138
- if (task.state === 'completed' && task.implementation === 'commit' && !skipTrace) {
139
- const qualified = qualifiedTaskId(item.id, task.id);
140
- if (qualified !== preCommitTask) traceCandidates.push(qualified);
141
- }
142
- }
143
- if (traceCandidates.length) {
144
- try {
145
- for (const [qualified, result] of traceTasks(root, traceCandidates))
146
- if (result.status !== 'resolved')
147
- error('TRACE', `${qualified}: expected exactly one HEAD-reachable commit with both Flow trailers.`);
148
- } catch (failure) {
149
- for (const qualified of traceCandidates) error('TRACE', `${qualified}: ${failure.message}`);
150
- }
151
- }
152
- if (activeTasks > 1) error('STATE', 'Only one mutating task may be active across the project.');
153
- if (exists('gates.yaml')) {
154
- try {
155
- parseGates(read('gates.yaml'));
156
- if (!migrationPending && evaluateConfiguredGates) {
157
- const gateResults = evaluateGates(root);
158
- onGateResults?.(gateResults);
159
- for (const gate of gateResults)
160
- if (gate.blocking && ['failed', 'unsupported'].includes(gate.status))
161
- error('GATE', `${gate.id}: ${gate.status}`);
162
- }
163
- } catch (failure) {
164
- error('GATES', failure.message);
165
- }
166
- } else if (!migrationPending) error('MISSING', 'Missing gates.yaml.');
167
- if (!exists('docs/graph.md') || read('docs/graph.md') !== generateGraphMarkdown(read('backlog.yaml')))
168
- error('GRAPH', 'Graph is stale; run npx --no-install flow graph.');
169
- return findings;
170
- }
171
- export function runValidate({ args }) {
172
- const index = args.indexOf('--pre-commit');
173
- const includeGates = args.includes('--gates');
174
- let gateResults = [];
175
- const findings = validateProject(projectRoot(args), {
176
- preCommitTask: index < 0 ? null : args[index + 1],
177
- skipTrace: args.includes('--skip-trace'),
178
- evaluateConfiguredGates: includeGates,
179
- onGateResults: (results) => {
180
- gateResults = results;
181
- }
182
- });
183
- if (args.includes('--json')) {
184
- info(
185
- JSON.stringify(
186
- { valid: findings.length === 0, findings, ...(includeGates ? { gates: gateResults } : {}) },
187
- null,
188
- 2
189
- )
190
- );
191
- if (findings.length) process.exitCode = 1;
192
- return;
193
- }
194
- if (!findings.length) return info('Flow project is valid.');
195
- for (const finding of findings) info(`${finding.code}: ${finding.message}`);
196
- fail(`${findings.length} validation finding(s).`);
197
- }
@@ -1,86 +0,0 @@
1
- import readline from 'node:readline/promises';
2
- import { stdin as inputStream, stdout as outputStream } from 'node:process';
3
-
4
- export class CliError extends Error {}
5
- export function fail(message, code = 1) {
6
- const error = new CliError(message);
7
- error.exitCode = code;
8
- throw error;
9
- }
10
- export function info(message = '') {
11
- console.log(message);
12
- }
13
- function abortedPromptError() {
14
- const error = new Error('Prompt aborted.');
15
- error.code = 'ABORT_ERR';
16
- return error;
17
- }
18
- function question(readlineInterface, message) {
19
- return new Promise((resolve, reject) => {
20
- let settled = false;
21
- const finish = (callback, value) => {
22
- if (settled) return;
23
- settled = true;
24
- readlineInterface.removeListener('close', onClose);
25
- callback(value);
26
- };
27
- const onClose = () => finish(reject, abortedPromptError());
28
- readlineInterface.once('close', onClose);
29
- readlineInterface.question(message).then(
30
- (answer) => finish(resolve, answer),
31
- (error) => finish(reject, error)
32
- );
33
- });
34
- }
35
- export async function promptText(message, defaultValue = '') {
36
- const rl = readline.createInterface({ input: inputStream, output: outputStream });
37
- try {
38
- const answer = (await question(rl, `${message}${defaultValue ? ` [${defaultValue}]` : ''}: `)).trim();
39
- return answer || defaultValue;
40
- } finally {
41
- rl.close();
42
- }
43
- }
44
- export async function promptSelect({ title, options, defaultIndex = 0 }) {
45
- info(title);
46
- options.forEach((option, index) =>
47
- info(
48
- ` ${index === defaultIndex ? '›' : ' '} ${index + 1}. ${option.label}${option.description ? `\n ${option.description}` : ''}`
49
- )
50
- );
51
- const rl = readline.createInterface({ input: inputStream, output: outputStream });
52
- try {
53
- while (true) {
54
- const answer = (await question(rl, `Selection [${defaultIndex + 1}]: `)).trim();
55
- const index = answer ? Number.parseInt(answer, 10) - 1 : defaultIndex;
56
- if (Number.isInteger(index) && index >= 0 && index < options.length) return options[index].value;
57
- info('Choose a valid number.');
58
- }
59
- } finally {
60
- rl.close();
61
- }
62
- }
63
- export async function promptMultiSelect({ title, options }) {
64
- info(title);
65
- options.forEach((option, index) => info(` [ ] ${index + 1}. ${option.label}`));
66
- info(' (Select multiple with comma-separated numbers, e.g. 1,2)');
67
- const rl = readline.createInterface({ input: inputStream, output: outputStream });
68
- try {
69
- while (true) {
70
- const answer = (await question(rl, 'Selection: ')).trim();
71
- const indices = [
72
- ...new Set(
73
- answer
74
- .split(',')
75
- .map((value) => Number.parseInt(value.trim(), 10))
76
- .filter(Number.isInteger)
77
- )
78
- ];
79
- if (indices.length && indices.every((index) => index >= 1 && index <= options.length))
80
- return indices.map((index) => options[index - 1].value);
81
- info('Choose one or more valid numbers.');
82
- }
83
- } finally {
84
- rl.close();
85
- }
86
- }
@@ -1,27 +0,0 @@
1
- import fs from 'node:fs';
2
- import { parse } from 'yaml';
3
- const text = fs.readFileSync(
4
- new URL('../../skills/flow/engineering/profiles/readability-first.md', import.meta.url),
5
- 'utf8'
6
- );
7
- export function parseProfileFrontmatter(text) {
8
- const match = text.match(/^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/);
9
- if (!match) throw new Error('Missing YAML frontmatter in Flow profile.');
10
- return parse(match[1]);
11
- }
12
- export const READABILITY_FIRST_PROFILE = parseProfileFrontmatter(text);
13
- export const ENGINEERING_PROFILES = {
14
- 'readability-first': READABILITY_FIRST_PROFILE,
15
- [READABILITY_FIRST_PROFILE.id]: READABILITY_FIRST_PROFILE
16
- };
17
- export const BROWNFIELD_POLICIES = {
18
- improve: {
19
- label: 'Improve existing structure — Recommended',
20
- description:
21
- 'Preserve behavior and external contracts; recommend clearer structure where justified. Does not authorize refactoring.'
22
- },
23
- preserve: {
24
- label: 'Keep existing structure',
25
- description: 'Retain consistent conventions unless a concrete problem warrants an approved change.'
26
- }
27
- };
@@ -1,39 +0,0 @@
1
- import fs from 'node:fs';
2
- import path from 'node:path';
3
- import { parse, stringify } from 'yaml';
4
-
5
- export function configPath(root) {
6
- return path.join(root, '.flow', 'config.yaml');
7
- }
8
-
9
- export function defaultConfig() {
10
- return {
11
- schema_version: 2,
12
- runtimes: [],
13
- engineering: { profile: 'flow/readability-first@1', existing_code_policy: 'not_applicable' }
14
- };
15
- }
16
-
17
- export function readConfig(root) {
18
- const file = configPath(root);
19
- if (!fs.existsSync(file)) return null;
20
- const raw = parse(fs.readFileSync(file, 'utf8')) ?? {};
21
- const config = defaultConfig();
22
- config.schema_version = raw.schema_version ?? 1;
23
- config.runtimes = Array.isArray(raw.runtimes) ? raw.runtimes : [];
24
- config.engineering = { ...config.engineering, ...(raw.engineering ?? {}) };
25
- return config;
26
- }
27
-
28
- export function writeConfig(root, config) {
29
- const normalized = {
30
- schema_version: 2,
31
- runtimes: config.runtimes ?? [],
32
- engineering: {
33
- profile: config.engineering?.profile ?? defaultConfig().engineering.profile,
34
- existing_code_policy: config.engineering?.existing_code_policy ?? 'not_applicable'
35
- }
36
- };
37
- fs.mkdirSync(path.join(root, '.flow'), { recursive: true });
38
- fs.writeFileSync(configPath(root), stringify(normalized, { lineWidth: 0 }), 'utf8');
39
- }
@@ -1,10 +0,0 @@
1
- import path from 'node:path';
2
-
3
- export function valueAfter(args, name) {
4
- const index = args.indexOf(name);
5
- return index >= 0 ? args[index + 1] : undefined;
6
- }
7
-
8
- export function projectRoot(args) {
9
- return path.resolve(valueAfter(args, '--path') || process.cwd());
10
- }
@@ -1,34 +0,0 @@
1
- import fs from 'node:fs';
2
- import path from 'node:path';
3
-
4
- function copyDirectory(source, target) {
5
- fs.mkdirSync(target, { recursive: true });
6
- for (const entry of fs.readdirSync(source, { withFileTypes: true })) {
7
- const sourcePath = path.join(source, entry.name);
8
- const targetPath = path.join(target, entry.name);
9
- if (entry.isDirectory()) copyDirectory(sourcePath, targetPath);
10
- else fs.copyFileSync(sourcePath, targetPath);
11
- }
12
- }
13
-
14
- export function installRuntimeSkill(root, runtime, packageRoot) {
15
- if (
16
- typeof runtime.skills_path !== 'string' ||
17
- !runtime.skills_path ||
18
- path.isAbsolute(runtime.skills_path) ||
19
- runtime.skills_path.split(/[\\/]/).includes('..')
20
- )
21
- throw new Error('Runtime skills_path must remain project-local.');
22
- const target = path.join(root, runtime.skills_path, 'flow');
23
- let ancestor = root;
24
- for (const segment of path.relative(root, target).split(path.sep)) {
25
- ancestor = path.join(ancestor, segment);
26
- if (fs.existsSync(ancestor) && fs.lstatSync(ancestor).isSymbolicLink())
27
- throw new Error('Refusing a symlinked runtime path.');
28
- }
29
- if (fs.existsSync(target) && fs.lstatSync(target).isSymbolicLink())
30
- throw new Error('Refusing to overwrite a symlinked skill.');
31
- fs.rmSync(target, { recursive: true, force: true });
32
- copyDirectory(path.join(packageRoot, 'skills', 'flow'), target);
33
- return target;
34
- }