@caiqueoak/flow 0.4.0 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/README.md +44 -43
  2. package/package.json +11 -2
  3. package/skills/flow/SKILL.md +10 -137
  4. package/skills/flow/build/step-01-execute-task.md +9 -0
  5. package/skills/flow/discovery/step-01-project.md +5 -0
  6. package/skills/flow/discovery/step-02-await-approval.md +3 -0
  7. package/skills/flow/engineering/profiles/readability-first.md +20 -0
  8. package/skills/flow/engineering/step-02-synthesize.md +37 -0
  9. package/skills/flow/engineering/step-05-present.md +3 -0
  10. package/skills/flow/engineering/technology-defaults.md +44 -0
  11. package/skills/flow/invariants.md +16 -0
  12. package/skills/flow/migration/step-01-reconcile.md +7 -0
  13. package/skills/flow/planning/step-01-plan-work-item.md +9 -0
  14. package/skills/flow/planning/step-02-prepare-plan.md +33 -0
  15. package/skills/flow/planning/step-03-await-approval.md +3 -0
  16. package/skills/flow/reconcile/step-01-reconcile.md +7 -0
  17. package/skills/flow/review/step-01-review-work-item.md +5 -0
  18. package/src/artifacts/backlog.mjs +162 -0
  19. package/src/artifacts/document.mjs +30 -0
  20. package/src/artifacts/engineering.mjs +26 -0
  21. package/src/artifacts/gates.mjs +28 -0
  22. package/src/artifacts/implementation-plan.mjs +27 -0
  23. package/src/artifacts/prd.mjs +12 -0
  24. package/src/artifacts/state.mjs +63 -0
  25. package/src/artifacts/tasks.mjs +90 -0
  26. package/src/cli.mjs +34 -316
  27. package/src/commands/gates.mjs +68 -0
  28. package/src/commands/graph.mjs +83 -0
  29. package/src/commands/init.mjs +111 -0
  30. package/src/commands/migrate.mjs +224 -0
  31. package/src/commands/route.mjs +135 -0
  32. package/src/commands/status.mjs +32 -0
  33. package/src/commands/trace.mjs +44 -0
  34. package/src/commands/validate.mjs +175 -0
  35. package/src/shared/cli-io.mjs +86 -0
  36. package/src/shared/profiles.mjs +22 -0
  37. package/src/shared/project-config.mjs +39 -0
  38. package/src/shared/project-path.mjs +10 -0
  39. package/src/shared/skill-installer.mjs +34 -0
  40. package/skills/flow/references/build.md +0 -7
  41. package/skills/flow/references/discovery.md +0 -15
  42. package/skills/flow/references/graph.md +0 -74
  43. package/skills/flow/references/planning.md +0 -7
  44. package/skills/flow/references/reconcile.md +0 -5
  45. package/skills/flow/references/review.md +0 -5
  46. package/src/graph.mjs +0 -161
package/src/cli.mjs CHANGED
@@ -2,331 +2,49 @@
2
2
 
3
3
  import fs from 'node:fs';
4
4
  import path from 'node:path';
5
- import { execFileSync } from 'node:child_process';
6
5
  import { fileURLToPath } from 'node:url';
7
- import readline from 'node:readline/promises';
8
- import { stdin as inputStream, stdout as outputStream } from 'node:process';
9
-
10
- const __filename = fileURLToPath(import.meta.url);
11
- const __dirname = path.dirname(__filename);
12
- const ROOT = path.resolve(__dirname, '..');
13
- const PACKAGE = JSON.parse(fs.readFileSync(path.join(ROOT, 'package.json'), 'utf8'));
14
- const VERSION = PACKAGE.version;
15
- const PACKAGE_NAME = PACKAGE.name;
16
- const args = process.argv.slice(2);
17
-
18
- const RUNTIME_DEFINITIONS = {
19
- codex: { label: 'Codex', skillsPath: '.codex/skills' },
20
- claude: { label: 'Claude Code', skillsPath: '.claude/skills' }
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)
21
19
  };
22
20
 
23
- function fail(message, code = 1) { console.error(`flow: ${message}`); process.exit(code); }
24
- function info(message = '') { console.log(message); }
25
- function hasFlag(name) { return args.includes(name); }
26
- function valueAfter(name) { const i = args.indexOf(name); return i >= 0 ? args[i + 1] : undefined; }
27
- function npmCommand() { return process.platform === 'win32' ? 'npm.cmd' : 'npm'; }
28
- function projectRoot() { return path.resolve(valueAfter('--path') || process.cwd()); }
29
- function configPath(root) { return path.join(root, '.flow', 'config.yaml'); }
30
- function packagePath(root) { return path.join(root, 'node_modules', '@caiqueoak', 'flow'); }
31
-
32
- function abortedPromptError() {
33
- const error = new Error('Prompt aborted.');
34
- error.code = 'ABORT_ERR';
35
- return error;
36
- }
37
-
38
- function question(rl, message) {
39
- return new Promise((resolve, reject) => {
40
- let settled = false;
41
- const finish = (callback, value) => {
42
- if (settled) return;
43
- settled = true;
44
- rl.removeListener('close', onClose);
45
- callback(value);
46
- };
47
- const onClose = () => finish(reject, abortedPromptError());
48
- rl.once('close', onClose);
49
- rl.question(message).then(
50
- (answer) => finish(resolve, answer),
51
- (error) => finish(reject, error)
52
- );
53
- });
54
- }
55
-
56
- function runNpm(npmArgs, options = {}) {
57
- if (process.platform === 'win32') {
58
- return execFileSync(process.env.ComSpec || 'cmd.exe', ['/d', '/s', '/c', npmCommand(), ...npmArgs], options);
59
- }
60
- return execFileSync(npmCommand(), npmArgs, {
61
- ...options
62
- });
63
- }
64
-
65
- function copyDir(source, target) {
66
- fs.mkdirSync(target, { recursive: true });
67
- for (const entry of fs.readdirSync(source, { withFileTypes: true })) {
68
- const src = path.join(source, entry.name);
69
- const dst = path.join(target, entry.name);
70
- if (entry.isDirectory()) copyDir(src, dst);
71
- else fs.copyFileSync(src, dst);
72
- }
73
- }
74
-
75
- function quoteYaml(value) { return /^[A-Za-z0-9_.\/-]+$/.test(value) ? value : JSON.stringify(value); }
76
- function defaultConfig() {
77
- return { frameworkVersion: VERSION, runtimes: [], continueAcrossWorkItems: true, workItemsConcurrency: 'auto', taskConcurrency: 'auto' };
78
- }
79
-
80
- function readConfig(root) {
81
- const file = configPath(root);
82
- if (!fs.existsSync(file)) return null;
83
- const text = fs.readFileSync(file, 'utf8');
84
- const cfg = defaultConfig();
85
- const v = text.match(/^[ \t]*version:[ \t]*([^\s#]+)[ \t]*$/m);
86
- if (v) cfg.frameworkVersion = v[1].replace(/^['"]|['"]$/g, '');
87
- const runtimeBlock = text.match(/^runtimes:\s*\n([\s\S]*?)(?=^[A-Za-z_][A-Za-z0-9_]*:|\Z)/m)?.[1] || '';
88
- const entries = runtimeBlock.split(/(?=^\s*-\s+type:)/m).filter((x) => /-\s+type:/.test(x));
89
- for (const entry of entries) {
90
- const type = entry.match(/-\s+type:\s*([^\s#]+)/)?.[1]?.replace(/^['"]|['"]$/g, '');
91
- const skillsPath = entry.match(/skills_path:\s*([^\n#]+)/)?.[1]?.trim().replace(/^['"]|['"]$/g, '');
92
- if (type && skillsPath) cfg.runtimes.push({ type, skills_path: skillsPath });
93
- }
94
- return cfg;
95
- }
96
-
97
- function writeConfig(root, config) {
98
- const lines = ['schema_version: 1', 'framework:', ' name: flow', ` version: ${VERSION}`, 'runtimes:'];
99
- if (!config.runtimes.length) lines.push(' []');
100
- else for (const runtime of config.runtimes) {
101
- lines.push(` - type: ${quoteYaml(runtime.type)}`);
102
- lines.push(` skills_path: ${quoteYaml(runtime.skills_path)}`);
103
- }
104
- lines.push('autonomy:', ' continue_across_work_items: true', ' stop_on:', ' - consequential_decision', ' - external_approval', ' - unrecoverable_blocker', ' - no_ready_work', 'parallelism:', ' strategy: maximum_safe', ' max_concurrent_work_items: auto', ' max_concurrent_tasks_per_work_item: auto', 'efficiency:', ' token_usage: optimize', ' prefer_primary_orchestrator: true', ' delegate_only_when_beneficial: true', '');
105
- fs.mkdirSync(path.join(root, '.flow'), { recursive: true });
106
- fs.writeFileSync(configPath(root), lines.join('\n'), 'utf8');
107
- }
108
-
109
- function installRuntimeSkill(root, runtime, packageRoot = ROOT) {
110
- const target = path.join(root, runtime.skills_path, 'flow');
111
- copyDir(path.join(packageRoot, 'skills', 'flow'), target);
112
- return target;
113
- }
114
- function parseRuntimeFlag() {
115
- const raw = valueAfter('--runtime');
116
- return raw ? raw.split(',').map((x) => x.trim().toLowerCase()).filter(Boolean) : null;
117
- }
118
-
119
- function missingBuiltinRuntimes(existing) {
120
- const existingTypes = new Set(existing.map((r) => r.type));
121
- return Object.keys(RUNTIME_DEFINITIONS).filter((type) => !existingTypes.has(type));
122
- }
123
-
124
- async function promptMultiSelect(existing) {
125
- const existingTypes = new Set(existing.map((r) => r.type));
126
- const options = Object.entries(RUNTIME_DEFINITIONS).filter(([type]) => !existingTypes.has(type)).map(([value, def]) => ({ value, label: def.label }));
127
- options.push({ value: 'custom', label: 'Custom coding agent / skills path' });
128
- info(existing.length ? 'Select coding agents to add:' : 'Select coding agents:');
129
- options.forEach((opt, i) => info(` [ ] ${i + 1}. ${opt.label}`));
130
- info(' (Select multiple with comma-separated numbers, e.g. 1,2)');
131
- const rl = readline.createInterface({ input: inputStream, output: outputStream });
132
- try {
133
- while (true) {
134
- const answer = (await question(rl, 'Selection: ')).trim();
135
- const indices = [...new Set(answer.split(',').map((v) => Number.parseInt(v.trim(), 10)).filter(Number.isInteger))];
136
- if (indices.length && indices.every((n) => n >= 1 && n <= options.length)) return indices.map((n) => options[n - 1].value);
137
- info('Choose one or more valid numbers.');
138
- }
139
- } finally { rl.close(); }
140
- }
141
-
142
- async function promptText(message, defaultValue = '') {
143
- const rl = readline.createInterface({ input: inputStream, output: outputStream });
144
- try {
145
- const answer = (await question(rl, `${message}${defaultValue ? ` [${defaultValue}]` : ''}: `)).trim();
146
- return answer || defaultValue;
147
- } finally { rl.close(); }
148
- }
149
-
150
- async function resolveRuntime(type, existing) {
151
- if (RUNTIME_DEFINITIONS[type]) return { type, skills_path: RUNTIME_DEFINITIONS[type].skillsPath };
152
- if (type !== 'custom') fail(`unsupported runtime '${type}'. Use codex, claude, or custom.`);
153
- const fallback = `custom-${existing.filter((r) => r.type.startsWith('custom')).length + 1}`;
154
- const name = await promptText('Custom coding agent id', fallback);
155
- while (true) {
156
- const skillsPath = await promptText('Project-local skills directory', `.${name}/skills`);
157
- if (!path.isAbsolute(skillsPath) && !skillsPath.split(/[\\/]/).includes('..')) return { type: name, skills_path: skillsPath };
158
- info('Skills path must be relative and remain inside the project.');
159
- }
160
- }
161
-
162
- async function initProject() {
163
- const root = projectRoot();
164
- const flowDir = path.join(root, '.flow');
165
- const existed = fs.existsSync(flowDir);
166
- const config = readConfig(root) || defaultConfig();
167
- if (existed) {
168
- info('Flow project already exists. Canonical project artifacts will not be created or modified.');
169
- if (config.runtimes.length) info(`Configured coding agents: ${config.runtimes.map((r) => r.type).join(', ')}`);
170
- }
171
-
172
- const requested = parseRuntimeFlag();
173
- if (existed && !requested && missingBuiltinRuntimes(config.runtimes).length === 0) {
174
- info('All built-in coding agents are already configured. Nothing to add.');
175
- info('Use --runtime custom only when you intentionally want to add a custom coding agent.');
176
- return;
177
- }
178
-
179
- const selected = requested || await promptMultiSelect(config.runtimes);
180
- const knownTypes = new Set(config.runtimes.map((r) => r.type));
181
- const added = [];
182
- for (const type of selected) {
183
- if (knownTypes.has(type)) continue;
184
- const runtime = await resolveRuntime(type, config.runtimes);
185
- if (knownTypes.has(runtime.type)) continue;
186
- config.runtimes.push(runtime); knownTypes.add(runtime.type); added.push(runtime);
187
- }
188
- fs.mkdirSync(flowDir, { recursive: true });
189
- writeConfig(root, config);
190
- for (const runtime of added) info(`✓ ${runtime.type}: ${path.relative(root, installRuntimeSkill(root, runtime))}`);
191
- if (!added.length) info('No new coding-agent integration was added.');
192
- else { info(); info('Flow is ready. Open a configured coding agent and invoke /flow.'); }
193
- }
194
-
195
- function containingNodeModules(packageRoot) {
196
- let current = path.resolve(packageRoot);
197
- while (true) {
198
- if (path.basename(current).toLowerCase() === 'node_modules') return current;
199
- const parent = path.dirname(current);
200
- if (parent === current) return null;
201
- current = parent;
202
- }
203
- }
204
-
205
- function installedPackageVersion(packageRoot) {
206
- const manifest = path.join(packageRoot, 'package.json');
207
- if (!fs.existsSync(manifest)) return null;
208
- return JSON.parse(fs.readFileSync(manifest, 'utf8')).version || null;
209
- }
210
-
211
- function lockedPackageVersion(root) {
212
- const lockfile = path.join(root, 'package-lock.json');
213
- if (!fs.existsSync(lockfile)) return null;
214
- try {
215
- return JSON.parse(fs.readFileSync(lockfile, 'utf8')).packages?.[`node_modules/${PACKAGE_NAME}`]?.version || null;
216
- } catch { return null; }
217
- }
218
-
219
- function globalUpdatePlan(root) {
220
- try {
221
- const globalNodeModules = path.resolve(runNpm(['root', '--global'], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim());
222
- const globalPackageRoot = path.join(globalNodeModules, '@caiqueoak', 'flow');
223
- if (path.resolve(ROOT).startsWith(`${globalNodeModules}${path.sep}`) && fs.existsSync(path.join(globalPackageRoot, 'package.json'))) {
224
- return {
225
- npmArgs: ['update', '--global', PACKAGE_NAME],
226
- cwd: root,
227
- packageRoot: globalPackageRoot,
228
- mode: 'global'
229
- };
230
- }
231
- } catch { /* fall through to local installation detection */ }
232
- return null;
233
- }
234
-
235
- function updatePlan(root) {
236
- const global = globalUpdatePlan(root);
237
- if (global) return global;
238
-
239
- const nodeModules = containingNodeModules(ROOT);
240
- if (nodeModules) {
241
- const installRoot = path.dirname(nodeModules);
242
- return {
243
- npmArgs: ['update', PACKAGE_NAME],
244
- cwd: installRoot,
245
- packageRoot: ROOT,
246
- mode: 'local'
247
- };
248
- }
249
-
250
- const projectPackageRoot = packagePath(root);
251
- if (fs.existsSync(path.join(projectPackageRoot, 'package.json'))) {
252
- return {
253
- npmArgs: ['update', PACKAGE_NAME],
254
- cwd: root,
255
- packageRoot: projectPackageRoot,
256
- mode: 'project'
257
- };
258
- }
259
-
260
- fail('cannot determine how this Flow CLI was installed. Reinstall @caiqueoak/flow with npm, then run flow update again.');
261
- }
262
-
263
- function repairDivergentProjectInstall(plan) {
264
- if (plan.mode === 'global') return;
265
- const expected = lockedPackageVersion(plan.cwd);
266
- const actual = installedPackageVersion(plan.packageRoot);
267
- if (!expected || !actual || expected === actual) return;
268
-
269
- const backup = path.join(plan.cwd, `.flow-update-backup-${process.pid}-${Date.now()}`);
270
- info(`Repairing divergent ${PACKAGE_NAME} installation (${actual} on disk, ${expected} in package-lock.json)...`);
271
- fs.renameSync(plan.packageRoot, backup);
272
- try {
273
- runNpm(['install'], { cwd: plan.cwd, stdio: 'inherit' });
274
- if (installedPackageVersion(plan.packageRoot) !== expected) {
275
- throw new Error(`npm install did not restore ${PACKAGE_NAME}@${expected}.`);
276
- }
277
- fs.rmSync(backup, { recursive: true, force: true });
278
- } catch (error) {
279
- try {
280
- fs.rmSync(plan.packageRoot, { recursive: true, force: true });
281
- fs.renameSync(backup, plan.packageRoot);
282
- } catch { /* preserve the original npm error below */ }
283
- throw error;
284
- }
285
- }
286
-
287
- function update() {
288
- const root = projectRoot();
289
- const config = readConfig(root);
290
- if (!config) fail('this project is not initialized. Run flow init first.');
291
- if (!config.runtimes.length) fail('no coding agents are configured. Run flow init to add one.');
292
-
293
- const plan = updatePlan(root);
294
- info(`Updating ${PACKAGE_NAME} (${plan.mode} installation)...`);
295
- try {
296
- repairDivergentProjectInstall(plan);
297
- runNpm(plan.npmArgs, { cwd: plan.cwd, stdio: 'inherit' });
298
- } catch { fail('npm update failed. Existing project state and installed skills were not intentionally removed.'); }
299
-
300
- if (!fs.existsSync(path.join(plan.packageRoot, 'package.json'))) fail(`updated package not found at ${plan.packageRoot}.`);
301
- const latest = JSON.parse(fs.readFileSync(path.join(plan.packageRoot, 'package.json'), 'utf8'));
302
- for (const runtime of config.runtimes) info(`✓ ${runtime.type}: ${path.relative(root, installRuntimeSkill(root, runtime, plan.packageRoot))}`);
303
- const text = fs.readFileSync(configPath(root), 'utf8');
304
- fs.writeFileSync(configPath(root), text.replace(/(^framework:\s*\n(?:.*\n)*?\s+version:\s*)[^\n]+/m, `$1${latest.version}`), 'utf8');
305
- info(`Flow updated to ${latest.version}.`);
306
- }
307
-
308
- async function graph() {
309
- const { writeGraph } = await import('./graph.mjs');
310
- const result = writeGraph(projectRoot());
311
- info(`Generated ${result.graphPath}`);
312
- }
313
-
314
21
  function help() {
315
- info(`Flow ${VERSION}\n\nUsage:\n flow init [--path <project>] [--runtime codex,claude]\n flow update [--path <project>]\n flow graph [--path <project>]\n flow --version\n\nflow init creates only .flow/config.yaml and installs the project-local /flow skill for selected coding agents.\nIf .flow already exists, init only adds coding-agent integrations and exits without prompting when all built-in integrations are already configured.\nflow update updates the installation that provides the Flow CLI (project-local or global) and refreshes every configured project-local skill.\nflow graph deterministically projects .flow/BACKLOG.yaml into .flow/GRAPH.md. There is no flow install command and no automatic/background update mechanism.`);
22
+ info(
23
+ `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.`
24
+ );
25
+ }
26
+
27
+ export async function runCli(args = process.argv.slice(2)) {
28
+ if (!args.length || args.includes('--help') || args.includes('-h')) return help();
29
+ if (args.includes('--version') || args.includes('-v')) return info(packageManifest.version);
30
+ const command = commands[args[0]];
31
+ if (!command) throw new CliError(`unknown command '${args[0]}'. Run flow --help.`);
32
+ return command({
33
+ args: args.slice(1),
34
+ packageRoot,
35
+ packageName: packageManifest.name,
36
+ version: packageManifest.version
37
+ });
316
38
  }
317
39
 
318
40
  try {
319
- if (!args.length || hasFlag('--help') || hasFlag('-h')) help();
320
- else if (hasFlag('--version') || hasFlag('-v')) info(VERSION);
321
- else if (args[0] === 'init') await initProject();
322
- else if (args[0] === 'update') update();
323
- else if (args[0] === 'graph') await graph();
324
- else fail(`unknown command '${args[0]}'. Run flow --help.`);
41
+ await runCli();
325
42
  } catch (error) {
326
43
  if (error?.code === 'ABORT_ERR') {
327
44
  info('\nFlow command canceled.');
328
45
  process.exitCode = 130;
329
- } else {
330
- throw error;
331
- }
46
+ } else if (error instanceof CliError) {
47
+ console.error(`flow: ${error.message}`);
48
+ process.exitCode = error.exitCode || 1;
49
+ } else throw error;
332
50
  }
@@ -0,0 +1,68 @@
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
+ if (gate.kind === 'command') return runCommandGate(root, gate);
65
+ if (gate.kind === 'builtin') return runBuiltinGate(root, gate);
66
+ throw new Error(`Unsupported gate kind '${gate.kind}'.`);
67
+ });
68
+ }
@@ -0,0 +1,83 @@
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
+ export function generateGraphMarkdown(backlogText) {
26
+ const { work_items: items } = parseBacklog(backlogText);
27
+ const ordered = topologicalOrder(items);
28
+ const byId = new Map(items.map((item) => [item.id, item]));
29
+ const statuses = new Map(items.map((item) => [item.id, deriveExecutionStatus(item, byId).status]));
30
+ const edges = ordered.flatMap((source) =>
31
+ ordered
32
+ .filter((target) => target.depends_on.includes(source.id))
33
+ .map((target) => ({ source: source.id, target: target.id }))
34
+ );
35
+ const lines = [
36
+ '# Work-item dependency graph',
37
+ '',
38
+ '> Derived from `../backlog.yaml`. Do not edit this file manually.',
39
+ '',
40
+ '## Status',
41
+ '',
42
+ '- **Completed** — accepted work.',
43
+ '- **In Progress** — currently executing.',
44
+ '- **Ready** — pending work with all dependencies satisfied.',
45
+ '- **Blocked** — pending work with at least one incomplete dependency or explicit external blocker.',
46
+ '',
47
+ '```mermaid',
48
+ "%%{init: {'flowchart': {'curve': 'linear', 'nodeSpacing': 32, 'rankSpacing': 54}} }%%",
49
+ 'flowchart TD'
50
+ ];
51
+ for (const item of ordered) lines.push(` ${item.id}["${escapeMermaid(item.id)}<br/>${escapeMermaid(item.title)}"]`);
52
+ lines.push('');
53
+ for (const edge of edges) lines.push(` ${edge.source} --> ${edge.target}`);
54
+ lines.push('', ...CLASSES);
55
+ for (const status of ['completed', 'in_progress', 'ready', 'blocked']) {
56
+ const ids = ordered.filter((item) => statuses.get(item.id) === status).map((item) => item.id);
57
+ if (ids.length) lines.push(` class ${ids.join(',')} ${status};`);
58
+ }
59
+ lines.push(
60
+ '```',
61
+ '',
62
+ '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.',
63
+ ''
64
+ );
65
+ return lines.join('\n');
66
+ }
67
+
68
+ export function writeGraph(root) {
69
+ const backlogPath = path.join(root, '.flow', 'backlog.yaml');
70
+ if (!fs.existsSync(backlogPath)) throw new ArtifactValidationError(`Backlog not found: ${backlogPath}`);
71
+ const graphPath = path.join(root, '.flow', 'docs', 'graph.md');
72
+ fs.mkdirSync(path.dirname(graphPath), { recursive: true });
73
+ const markdown = generateGraphMarkdown(fs.readFileSync(backlogPath, 'utf8'));
74
+ const temporaryPath = `${graphPath}.${process.pid}.${Date.now()}.tmp`;
75
+ fs.writeFileSync(temporaryPath, markdown, 'utf8');
76
+ fs.renameSync(temporaryPath, graphPath);
77
+ return { graphPath, markdown };
78
+ }
79
+
80
+ export function runGraph({ args }) {
81
+ const result = writeGraph(projectRoot(args));
82
+ info(`Generated ${result.graphPath}`);
83
+ }
@@ -0,0 +1,111 @@
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
+ }