@caiqueoak/flow 0.6.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 (173) 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 +3 -3
  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 +2 -2
  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/planning/step-02-prepare-plan.md +1 -1
  150. package/skills/flow/review/step-01-review-work-item.md +2 -2
  151. package/skills/flow/specification/step-01-deepen-spec.md +7 -0
  152. package/src/artifacts/backlog.mjs +0 -157
  153. package/src/artifacts/document.mjs +0 -30
  154. package/src/artifacts/engineering.mjs +0 -26
  155. package/src/artifacts/gates.mjs +0 -28
  156. package/src/artifacts/implementation-plan.mjs +0 -27
  157. package/src/artifacts/prd.mjs +0 -12
  158. package/src/artifacts/state.mjs +0 -63
  159. package/src/artifacts/tasks.mjs +0 -90
  160. package/src/cli.mjs +0 -62
  161. package/src/commands/gates.mjs +0 -71
  162. package/src/commands/graph.mjs +0 -92
  163. package/src/commands/init.mjs +0 -111
  164. package/src/commands/migrate.mjs +0 -224
  165. package/src/commands/route.mjs +0 -135
  166. package/src/commands/status.mjs +0 -35
  167. package/src/commands/trace.mjs +0 -58
  168. package/src/commands/validate.mjs +0 -197
  169. package/src/shared/cli-io.mjs +0 -86
  170. package/src/shared/profiles.mjs +0 -27
  171. package/src/shared/project-config.mjs +0 -39
  172. package/src/shared/project-path.mjs +0 -10
  173. package/src/shared/skill-installer.mjs +0 -34
package/src/cli.mjs DELETED
@@ -1,62 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- import fs from 'node:fs';
4
- import path from 'node:path';
5
- import { fileURLToPath } from 'node:url';
6
- import { CliError, info } from './shared/cli-io.mjs';
7
-
8
- const currentFile = fileURLToPath(import.meta.url);
9
- const packageRoot = path.resolve(path.dirname(currentFile), '..');
10
- const packageManifest = JSON.parse(fs.readFileSync(path.join(packageRoot, 'package.json'), 'utf8'));
11
- const commands = {
12
- init: async (context) => (await import('./commands/init.mjs')).runInit(context),
13
- graph: async (context) => (await import('./commands/graph.mjs')).runGraph(context),
14
- status: async (context) => (await import('./commands/status.mjs')).runStatus(context),
15
- validate: async (context) => (await import('./commands/validate.mjs')).runValidate(context),
16
- route: async (context) => (await import('./commands/route.mjs')).runRoute(context),
17
- trace: async (context) => (await import('./commands/trace.mjs')).runTrace(context),
18
- migrate: async (context) => (await import('./commands/migrate.mjs')).runMigrate(context)
19
- };
20
-
21
- function help() {
22
- const commandHelp = (message) =>
23
- info(
24
- message
25
- .replace(
26
- 'validate Check schemas, approvals, DAGs, traceability and deterministic gates.',
27
- 'validate Check schemas, approvals, DAGs and traceability; --gates runs project checks.'
28
- )
29
- .replace(
30
- 'validate options: --json --pre-commit W015-T003',
31
- 'validate options: --json --gates --pre-commit W015-T003'
32
- )
33
- );
34
- commandHelp(
35
- `Flow ${packageManifest.version}\n\nUse the project-local installation: npx --no-install flow <command>\n\nCommands:\n init Configure Flow and install/refresh local agent integrations.\n migrate Normalize older artifacts; /flow then reconciles legacy decisions.\n status Show progress and dependency/external blockers.\n validate Check schemas, approvals, DAGs, traceability and deterministic gates.\n graph Regenerate the derived work-item dependency graph.\n route Return the next repository-resumable agent step (--json available).\n trace Resolve W015-T003 to a HEAD-reachable commit with both Flow trailers.\n\nCommon options: --path <project>, --help, --version\ninit options: --runtime codex,claude --profile readability-first --existing-code improve|preserve\n Readability First: reusable preferences for SRP, semantic naming, cohesion and simple vertical slices.\n improve: recommend clearer structure while retaining behavior and external contracts.\n preserve: retain consistent existing conventions unless a concrete problem warrants change.\n Neither option authorizes refactoring; engineering changes require human approval.\nvalidate options: --json --pre-commit W015-T003\ntrace options: W015-T003 --json\n\nDiscovery → PRD approval → engineering approval → complete backlog → implementation-plan approval → serial implementation → review.\nUse /flow for agent workflows and engineering changes. Approved engineering.md is authoritative.\nUpdate the npm package with your package manager, then rerun init to refresh integrations.`
36
- );
37
- }
38
-
39
- export async function runCli(args = process.argv.slice(2)) {
40
- if (!args.length || args.includes('--help') || args.includes('-h')) return help();
41
- if (args.includes('--version') || args.includes('-v')) return info(packageManifest.version);
42
- const command = commands[args[0]];
43
- if (!command) throw new CliError(`unknown command '${args[0]}'. Run flow --help.`);
44
- return command({
45
- args: args.slice(1),
46
- packageRoot,
47
- packageName: packageManifest.name,
48
- version: packageManifest.version
49
- });
50
- }
51
-
52
- try {
53
- await runCli();
54
- } catch (error) {
55
- if (error?.code === 'ABORT_ERR') {
56
- info('\nFlow command canceled.');
57
- process.exitCode = 130;
58
- } else if (error instanceof CliError) {
59
- console.error(`flow: ${error.message}`);
60
- process.exitCode = error.exitCode || 1;
61
- } else throw error;
62
- }
@@ -1,71 +0,0 @@
1
- import fs from 'node:fs';
2
- import path from 'node:path';
3
- import { spawnSync } from 'node:child_process';
4
- import { fail } from '../shared/cli-io.mjs';
5
- import { parseGates } from '../artifacts/gates.mjs';
6
-
7
- function runCommandGate(root, gate) {
8
- const shell = process.platform === 'win32' ? process.env.ComSpec || 'cmd.exe' : '/bin/sh';
9
- const args = process.platform === 'win32' ? ['/d', '/s', '/c', gate.command] : ['-lc', gate.command];
10
- const result = spawnSync(shell, args, { cwd: root, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] });
11
- return {
12
- id: gate.id,
13
- kind: gate.kind,
14
- blocking: gate.blocking,
15
- status: result.status === 0 ? 'passed' : 'failed',
16
- exit_code: result.status,
17
- stdout: result.stdout,
18
- stderr: result.stderr
19
- };
20
- }
21
-
22
- function runBuiltinGate(root, gate) {
23
- if (gate.rule !== 'kebab-case-files')
24
- return {
25
- id: gate.id,
26
- kind: gate.kind,
27
- blocking: gate.blocking,
28
- status: 'unsupported',
29
- message: `Unknown builtin rule '${gate.rule}'.`
30
- };
31
- const excluded = new Set(['.git', '.flow', 'node_modules']);
32
- const violations = [];
33
- function walk(directory) {
34
- for (const entry of fs.readdirSync(directory, { withFileTypes: true })) {
35
- if (excluded.has(entry.name)) continue;
36
- const full = path.join(directory, entry.name);
37
- if (entry.isDirectory()) {
38
- walk(full);
39
- continue;
40
- }
41
- if (entry.name.startsWith('.')) continue;
42
- const ext = path.extname(entry.name);
43
- const stem = ext ? entry.name.slice(0, -ext.length) : entry.name;
44
- if (/^[A-Z0-9_.-]+$/.test(entry.name)) continue;
45
- const valid = stem.split('.').every((segment) => /^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(segment));
46
- if (!valid) violations.push(path.relative(root, full));
47
- }
48
- }
49
- walk(root);
50
- return {
51
- id: gate.id,
52
- kind: gate.kind,
53
- blocking: gate.blocking,
54
- status: violations.length ? 'failed' : 'passed',
55
- violations
56
- };
57
- }
58
-
59
- export function evaluateGates(root) {
60
- const file = path.join(root, '.flow', 'gates.yaml');
61
- if (!fs.existsSync(file)) fail('gates.yaml does not exist.');
62
- const { gates } = parseGates(fs.readFileSync(file, 'utf8'));
63
- return gates.map((gate) => {
64
- const startedAt = performance.now();
65
- let result;
66
- if (gate.kind === 'command') result = runCommandGate(root, gate);
67
- else if (gate.kind === 'builtin') result = runBuiltinGate(root, gate);
68
- else throw new Error(`Unsupported gate kind '${gate.kind}'.`);
69
- return { ...result, duration_ms: Math.round(performance.now() - startedAt) };
70
- });
71
- }
@@ -1,92 +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 {
6
- ArtifactValidationError,
7
- deriveExecutionStatus,
8
- parseBacklog,
9
- topologicalOrder
10
- } from '../artifacts/backlog.mjs';
11
-
12
- export { ArtifactValidationError as GraphValidationError };
13
-
14
- function escapeMermaid(text) {
15
- return text.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
16
- }
17
-
18
- const CLASSES = [
19
- ' classDef completed fill:#dcfce7,stroke:#16a34a,color:#14532d;',
20
- ' classDef in_progress fill:#dbeafe,stroke:#2563eb,color:#1e3a8a;',
21
- ' classDef ready fill:#fef3c7,stroke:#d97706,color:#78350f;',
22
- ' classDef blocked fill:#fee2e2,stroke:#dc2626,color:#7f1d1d;'
23
- ];
24
-
25
- const LINK_STYLES = {
26
- completed: 'stroke:#16a34a,stroke-width:2px',
27
- in_progress: 'stroke:#2563eb,stroke-width:3px',
28
- ready: 'stroke:#d97706,stroke-width:2px,stroke-dasharray:6 4',
29
- blocked: 'stroke:#dc2626,stroke-width:2px,stroke-dasharray:2 3'
30
- };
31
-
32
- export function generateGraphMarkdown(backlogText) {
33
- const { work_items: items } = parseBacklog(backlogText);
34
- const ordered = topologicalOrder(items);
35
- const byId = new Map(items.map((item) => [item.id, item]));
36
- const statuses = new Map(items.map((item) => [item.id, deriveExecutionStatus(item, byId).status]));
37
- const edges = ordered.flatMap((source) =>
38
- ordered
39
- .filter((target) => target.depends_on.includes(source.id))
40
- .map((target) => ({ source: source.id, target: target.id }))
41
- );
42
- const lines = [
43
- '# Work-item dependency graph',
44
- '',
45
- '> Derived from `../backlog.yaml`. Do not edit this file manually.',
46
- '',
47
- '## Status',
48
- '',
49
- '- 🟩 **Completed** — accepted work.',
50
- '- 🟦 **In Progress** — currently executing.',
51
- '- 🟨 **Ready** — pending work with all dependencies satisfied.',
52
- '- 🟥 **Blocked** — pending work with at least one incomplete work-item dependency.',
53
- '',
54
- '```mermaid',
55
- "%%{init: {'flowchart': {'curve': 'linear', 'nodeSpacing': 32, 'rankSpacing': 54}} }%%",
56
- 'flowchart TD'
57
- ];
58
- for (const item of ordered) lines.push(` ${item.id}["${escapeMermaid(item.id)}<br/>${escapeMermaid(item.title)}"]`);
59
- lines.push('');
60
- for (const edge of edges) lines.push(` ${edge.source} --> ${edge.target}`);
61
- lines.push('');
62
- edges.forEach((edge, index) => lines.push(` linkStyle ${index} ${LINK_STYLES[statuses.get(edge.source)]};`));
63
- lines.push('', ...CLASSES);
64
- for (const status of ['completed', 'in_progress', 'ready', 'blocked']) {
65
- const ids = ordered.filter((item) => statuses.get(item.id) === status).map((item) => item.id);
66
- if (ids.length) lines.push(` class ${ids.join(',')} ${status};`);
67
- }
68
- lines.push(
69
- '```',
70
- '',
71
- 'Dependency arrows are authoritative for execution ordering. Flow executes one mutating task at a time; independent read-only research or review may run in parallel.',
72
- ''
73
- );
74
- return lines.join('\n');
75
- }
76
-
77
- export function writeGraph(root) {
78
- const backlogPath = path.join(root, '.flow', 'backlog.yaml');
79
- if (!fs.existsSync(backlogPath)) throw new ArtifactValidationError(`Backlog not found: ${backlogPath}`);
80
- const graphPath = path.join(root, '.flow', 'docs', 'graph.md');
81
- fs.mkdirSync(path.dirname(graphPath), { recursive: true });
82
- const markdown = generateGraphMarkdown(fs.readFileSync(backlogPath, 'utf8'));
83
- const temporaryPath = `${graphPath}.${process.pid}.${Date.now()}.tmp`;
84
- fs.writeFileSync(temporaryPath, markdown, 'utf8');
85
- fs.renameSync(temporaryPath, graphPath);
86
- return { graphPath, markdown };
87
- }
88
-
89
- export function runGraph({ args }) {
90
- const result = writeGraph(projectRoot(args));
91
- info(`Generated ${result.graphPath}`);
92
- }
@@ -1,111 +0,0 @@
1
- import fs from 'node:fs';
2
- import path from 'node:path';
3
- import { fail, info, promptMultiSelect, promptSelect, promptText } from '../shared/cli-io.mjs';
4
- import { defaultConfig, readConfig, writeConfig } from '../shared/project-config.mjs';
5
- import { projectRoot, valueAfter } from '../shared/project-path.mjs';
6
- import { installRuntimeSkill } from '../shared/skill-installer.mjs';
7
- import { BROWNFIELD_POLICIES, ENGINEERING_PROFILES } from '../shared/profiles.mjs';
8
-
9
- const RUNTIME_DEFINITIONS = {
10
- codex: { label: 'Codex', skillsPath: '.codex/skills' },
11
- claude: { label: 'Claude Code', skillsPath: '.claude/skills' }
12
- };
13
- function parseRuntimeFlag(args) {
14
- const raw = valueAfter(args, '--runtime');
15
- return raw
16
- ? raw
17
- .split(',')
18
- .map((v) => v.trim().toLowerCase())
19
- .filter(Boolean)
20
- : null;
21
- }
22
- function missingBuiltinRuntimes(existing) {
23
- const types = new Set(existing.map((r) => r.type));
24
- return Object.keys(RUNTIME_DEFINITIONS).filter((type) => !types.has(type));
25
- }
26
- async function selectRuntimes(existing) {
27
- const types = new Set(existing.map((r) => r.type));
28
- const options = Object.entries(RUNTIME_DEFINITIONS)
29
- .filter(([type]) => !types.has(type))
30
- .map(([value, d]) => ({ value, label: d.label }));
31
- options.push({ value: 'custom', label: 'Custom coding agent / skills path' });
32
- return promptMultiSelect({
33
- title: existing.length ? 'Select coding agents to add:' : 'Select coding agents:',
34
- options
35
- });
36
- }
37
- async function resolveRuntime(type, existing) {
38
- if (RUNTIME_DEFINITIONS[type]) return { type, skills_path: RUNTIME_DEFINITIONS[type].skillsPath };
39
- if (type !== 'custom') fail(`unsupported runtime '${type}'. Use codex, claude, or custom.`);
40
- const fallback = `custom-${existing.filter((r) => r.type.startsWith('custom')).length + 1}`;
41
- const name = await promptText('Custom coding agent id', fallback);
42
- while (true) {
43
- const skillsPath = await promptText('Project-local skills directory', `.${name}/skills`);
44
- if (!path.isAbsolute(skillsPath) && !skillsPath.split(/[\\/]/).includes('..'))
45
- return { type: name, skills_path: skillsPath };
46
- info('Skills path must be relative and remain inside the project.');
47
- }
48
- }
49
- async function selectEngineering(args, root, config, existed) {
50
- const profileFlag = valueAfter(args, '--profile');
51
- const brownfieldFlag = valueAfter(args, '--existing-code');
52
- if (args.includes('--brownfield'))
53
- fail(
54
- 'Use --existing-code improve|preserve: improve recommends clearer structure; preserve keeps consistent conventions.'
55
- );
56
- if (existed && (profileFlag || brownfieldFlag))
57
- fail('Change engineering through /flow and human approval, not init.');
58
- if (profileFlag && !ENGINEERING_PROFILES[profileFlag]) fail(`unknown engineering profile '${profileFlag}'.`);
59
- if (brownfieldFlag && !BROWNFIELD_POLICIES[brownfieldFlag]) fail(`unknown brownfield policy '${brownfieldFlag}'.`);
60
- if (profileFlag) config.engineering.profile = ENGINEERING_PROFILES[profileFlag].id;
61
- const hasProjectFiles = fs
62
- .readdirSync(root)
63
- .some((name) => ['src', 'app', 'lib', 'packages'].includes(name) || /\.(m?[jt]sx?|py|java|go|rs|cs)$/.test(name));
64
- if (brownfieldFlag) config.engineering.existing_code_policy = brownfieldFlag;
65
- else if (!existed && hasProjectFiles)
66
- config.engineering.existing_code_policy = await promptSelect({
67
- title: 'How should Flow treat existing code conventions?',
68
- options: Object.entries(BROWNFIELD_POLICIES).map(([value, policy]) => ({
69
- value,
70
- label: policy.label,
71
- description: policy.description
72
- }))
73
- });
74
- }
75
-
76
- export async function runInit({ args, packageRoot }) {
77
- const root = projectRoot(args);
78
- const flowDirectory = path.join(root, '.flow');
79
- const existed = fs.existsSync(flowDirectory);
80
- const config = readConfig(root) || defaultConfig();
81
- if (config.schema_version !== 2)
82
- fail('Existing Flow project requires npx --no-install flow migrate before init. No files were changed.');
83
- if (existed) info('Flow project already exists. Canonical project artifacts will not be created or modified.');
84
- await selectEngineering(args, root, config, existed);
85
- const requested = parseRuntimeFlag(args);
86
- if (existed && !requested && missingBuiltinRuntimes(config.runtimes).length === 0) {
87
- writeConfig(root, config);
88
- for (const runtime of config.runtimes) installRuntimeSkill(root, runtime, packageRoot);
89
- info('All built-in coding agents are already configured. Configuration is up to date.');
90
- return;
91
- }
92
- const selected = requested || (await selectRuntimes(config.runtimes));
93
- const known = new Set(config.runtimes.map((r) => r.type));
94
- const added = [];
95
- for (const type of selected) {
96
- if (known.has(type)) continue;
97
- const runtime = await resolveRuntime(type, config.runtimes);
98
- if (known.has(runtime.type)) continue;
99
- config.runtimes.push(runtime);
100
- known.add(runtime.type);
101
- added.push(runtime);
102
- }
103
- fs.mkdirSync(flowDirectory, { recursive: true });
104
- writeConfig(root, config);
105
- for (const runtime of config.runtimes)
106
- info(`✓ ${runtime.type}: ${path.relative(root, installRuntimeSkill(root, runtime, packageRoot))}`);
107
- info(`Engineering profile: ${ENGINEERING_PROFILES[config.engineering.profile]?.label ?? config.engineering.profile}`);
108
- info(
109
- 'Flow is ready. Invoke /flow; engineering bootstrap runs before implementation when no approved contract exists.'
110
- );
111
- }
@@ -1,224 +0,0 @@
1
- import fs from 'node:fs';
2
- import path from 'node:path';
3
- import { parse, stringify } from 'yaml';
4
- import { info } from '../shared/cli-io.mjs';
5
- import { projectRoot } from '../shared/project-path.mjs';
6
- import { emptyState, stringifyState } from '../artifacts/state.mjs';
7
- import { parseBacklog } from '../artifacts/backlog.mjs';
8
- import { parseTasks } from '../artifacts/tasks.mjs';
9
- import { generateGraphMarkdown } from './graph.mjs';
10
- import { readConfig, writeConfig, defaultConfig } from '../shared/project-config.mjs';
11
-
12
- const STATES = {
13
- done: 'completed',
14
- complete: 'completed',
15
- completed: 'completed',
16
- in_progress: 'in_progress',
17
- blocked: 'pending',
18
- pending: 'pending',
19
- todo: 'pending'
20
- };
21
- function lifecycle(value) {
22
- if (!STATES[value]) throw new Error(`Unknown legacy state '${value}'.`);
23
- return STATES[value];
24
- }
25
- function number(value) {
26
- const match = String(value).match(/\d+/);
27
- if (!match) throw new Error(`Cannot normalize ID '${value}'.`);
28
- return match[0].padStart(3, '0');
29
- }
30
- function temporarySibling(file) {
31
- const directory = path.dirname(file);
32
- const base = path.basename(file);
33
- let attempt = 0;
34
- let temporary;
35
- do {
36
- temporary = path.join(directory, `.${base}.flow-migration-${process.pid}-${Date.now()}-${attempt++}`);
37
- } while (fs.existsSync(temporary));
38
- return temporary;
39
- }
40
- function sameEntry(from, to) {
41
- return fs.realpathSync.native(from) === fs.realpathSync.native(to);
42
- }
43
- function move(from, to) {
44
- if (!fs.existsSync(from)) return;
45
- if (path.resolve(from) === path.resolve(to)) return;
46
- if (fs.existsSync(to)) {
47
- if (!sameEntry(from, to)) throw new Error(`Migration destination already exists: ${to}`);
48
- const temporary = temporarySibling(from);
49
- fs.renameSync(from, temporary);
50
- try {
51
- fs.renameSync(temporary, to);
52
- } catch (error) {
53
- try {
54
- fs.renameSync(temporary, from);
55
- } catch (recoveryError) {
56
- error.preserveRecoveryData = true;
57
- error.recovery = { from, to, temporary, recoveryError };
58
- }
59
- throw error;
60
- }
61
- return;
62
- }
63
- fs.mkdirSync(path.dirname(to), { recursive: true });
64
- fs.renameSync(from, to);
65
- }
66
- function normalizeTasks(file, item) {
67
- const raw = parse(fs.readFileSync(file, 'utf8'));
68
- const mapping = new Map();
69
- const ids = new Set();
70
- for (const task of raw.tasks ?? []) {
71
- const match = String(task.id).match(/^(?:([A-Z]\d+)-)?T(\d+)$/);
72
- if (!match || (match[1] && number(match[1]) !== item.id.slice(1)))
73
- throw new Error(`Invalid/cross-work-item task ID '${task.id}'.`);
74
- const id = `T${match[2].padStart(3, '0')}`;
75
- if (ids.has(id)) throw new Error(`Task ID collision: ${id}`);
76
- ids.add(id);
77
- mapping.set(task.id, id);
78
- }
79
- const tasks = (raw.tasks ?? []).map((task) => {
80
- const state = lifecycle(task.state ?? task.status ?? 'pending');
81
- const result = {
82
- ...task,
83
- id: mapping.get(task.id),
84
- state,
85
- depends_on: (task.depends_on ?? []).map((dep) => {
86
- if (!mapping.has(dep)) throw new Error(`Unknown task dependency '${dep}'.`);
87
- return mapping.get(dep);
88
- }),
89
- implementation: state === 'completed' ? 'legacy' : task.implementation === 'none' ? 'none' : 'commit'
90
- };
91
- if (task.commit) result.legacy_commit = task.commit;
92
- delete result.commit;
93
- delete result.status;
94
- delete result.execution_id;
95
- return result;
96
- });
97
- const text = stringify({ schema_version: 1, work_item: item.id, tasks }, { lineWidth: 0 });
98
- parseTasks(text, { expectedWorkItem: item.id });
99
- fs.writeFileSync(file, text);
100
- }
101
- function migrateStaged(root) {
102
- const flow = path.join(root, '.flow');
103
- const oldConfig = readConfig(root);
104
- const oldFile = path.join(flow, fs.existsSync(path.join(flow, 'BACKLOG.yaml')) ? 'BACKLOG.yaml' : 'backlog.yaml');
105
- const raw = parse(fs.readFileSync(oldFile, 'utf8'));
106
- const mapping = new Map();
107
- const ids = new Set();
108
- for (const item of raw.work_items ?? []) {
109
- const id = `W${number(item.id)}`;
110
- if (ids.has(id)) throw new Error(`Work-item ID collision: ${id}`);
111
- ids.add(id);
112
- mapping.set(item.id, id);
113
- }
114
- const original = new Map();
115
- const work_items = (raw.work_items ?? []).map((item) => {
116
- const id = mapping.get(item.id);
117
- const folder = `${id}-${
118
- String(item.folder ?? item.title)
119
- .replace(/^(?:\d+[A-Za-z]|[A-Za-z]\d+)-?/, '')
120
- .toLowerCase()
121
- .replace(/[^a-z0-9]+/g, '-')
122
- .replace(/^-|-$/g, '') || 'work-item'
123
- }`;
124
- original.set(id, item.folder);
125
- return {
126
- id,
127
- folder,
128
- title: item.title,
129
- kind: item.kind,
130
- state: lifecycle(item.state ?? item.status ?? 'pending'),
131
- priority: item.priority ?? 1,
132
- depends_on: (item.depends_on ?? []).map((dep) => {
133
- if (!mapping.has(dep)) throw new Error(`Unknown work-item dependency '${dep}'.`);
134
- return mapping.get(dep);
135
- }),
136
- blockers: []
137
- };
138
- });
139
- const text = stringify({ schema_version: 2, work_items }, { lineWidth: 0 });
140
- parseBacklog(text);
141
- const workRoot = path.join(flow, 'work-items');
142
- for (const item of work_items) {
143
- const previous = path.join(workRoot, original.get(item.id) ?? item.folder);
144
- const folder = path.join(workRoot, item.folder);
145
- if (previous !== folder && fs.existsSync(previous)) move(previous, folder);
146
- if (!fs.existsSync(folder)) {
147
- if (item.state !== 'pending') throw new Error(`${item.id}: nonpending legacy work has no folder.`);
148
- continue;
149
- }
150
- for (const [old, next] of [
151
- ['SPEC.md', 'spec.md'],
152
- ['TASKS.yaml', 'tasks.yaml'],
153
- ['DECISIONS.md', 'legacy-decisions.md']
154
- ])
155
- move(path.join(folder, old), path.join(folder, next));
156
- if (fs.existsSync(path.join(folder, 'tasks.yaml'))) normalizeTasks(path.join(folder, 'tasks.yaml'), item);
157
- }
158
- fs.mkdirSync(path.join(flow, 'docs'), { recursive: true });
159
- fs.writeFileSync(path.join(flow, 'docs', 'legacy-backlog.yaml'), stringify(raw, { lineWidth: 0 }));
160
- for (const [old, next] of [
161
- ['PRD.md', 'prd.md'],
162
- ['ENGINEERING.md', 'legacy-engineering.md'],
163
- ['STATE.md', 'legacy-state.md'],
164
- ['DECISIONS.md', 'legacy-decisions.md'],
165
- ['SUMMARY.md', 'legacy-summary.md']
166
- ])
167
- move(path.join(flow, old), path.join(flow, 'docs', next));
168
- if (fs.existsSync(path.join(flow, 'GRAPH.md'))) fs.unlinkSync(path.join(flow, 'GRAPH.md'));
169
- if (oldFile.endsWith('BACKLOG.yaml')) move(oldFile, path.join(flow, 'backlog.yaml'));
170
- fs.writeFileSync(path.join(flow, 'backlog.yaml'), text);
171
- fs.writeFileSync(path.join(flow, 'docs', 'graph.md'), generateGraphMarkdown(text));
172
- const state = emptyState();
173
- state.execution.phase = 'migration_reconciliation';
174
- state.migration.status = 'pending_reconciliation';
175
- fs.writeFileSync(path.join(flow, 'state.yaml'), stringifyState(state));
176
- if (!fs.existsSync(path.join(flow, 'gates.yaml')))
177
- fs.writeFileSync(path.join(flow, 'gates.yaml'), 'schema_version: 1\ngates: []\n');
178
- const config = defaultConfig();
179
- config.runtimes = oldConfig?.runtimes ?? [];
180
- config.engineering.existing_code_policy = 'improve';
181
- writeConfig(root, config);
182
- }
183
- export function migrateProject(root) {
184
- const flow = path.join(root, '.flow');
185
- if (!fs.existsSync(flow)) throw new Error('.flow does not exist.');
186
- const config = readConfig(root);
187
- if (
188
- config?.schema_version === 2 &&
189
- fs.existsSync(path.join(flow, 'backlog.yaml')) &&
190
- parse(fs.readFileSync(path.join(flow, 'backlog.yaml'), 'utf8')).schema_version === 2
191
- )
192
- return { unresolved: [], unchanged: true };
193
- const staging = fs.mkdtempSync(path.join(root, '.flow-migration-'));
194
- const backup = path.join(staging, 'backup');
195
- const staged = path.join(staging, '.flow');
196
- let preserveStaging = false;
197
- try {
198
- fs.cpSync(flow, staged, { recursive: true });
199
- migrateStaged(staging);
200
- fs.renameSync(flow, backup);
201
- try {
202
- fs.renameSync(staged, flow);
203
- } catch (error) {
204
- fs.renameSync(backup, flow);
205
- throw error;
206
- }
207
- return { unresolved: ['semantic reconciliation'], unchanged: false };
208
- } catch (error) {
209
- preserveStaging = Boolean(error?.preserveRecoveryData);
210
- throw error;
211
- } finally {
212
- // Retain the original backup if even rollback failed; never delete the only copy.
213
- if (!preserveStaging && (!fs.existsSync(backup) || fs.existsSync(flow)))
214
- fs.rmSync(staging, { recursive: true, force: true });
215
- }
216
- }
217
- export function runMigrate({ args }) {
218
- const result = migrateProject(projectRoot(args));
219
- info(
220
- result.unchanged
221
- ? 'Already migrated; no files changed.'
222
- : 'Structural migration complete. Invoke /flow for semantic reconciliation before implementation.'
223
- );
224
- }