@caiqueoak/flow 0.5.2 → 0.6.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.
package/README.md CHANGED
@@ -44,15 +44,16 @@ Persist only pending, in_progress and completed. Ready/Blocked are derived exclu
44
44
 
45
45
  All commands use the local installation:
46
46
 
47
- | Command | Purpose |
48
- | --------------------------------------- | --------------------------------------------------------------------------- |
49
- | `npx --no-install flow init` | Configure or add/refresh runtime integrations. |
50
- | `npx --no-install flow migrate` | Atomic structural migration, followed by semantic reconciliation via /flow. |
51
- | `npx --no-install flow status` | Progress and dependency/external blockers. |
52
- | `npx --no-install flow validate` | Artifact integrity, approvals, DAGs, traceability and deterministic gates. |
53
- | `npx --no-install flow route --json` | Next legal step and required context. |
54
- | `npx --no-install flow graph` | Regenerate dependency graph. |
55
- | `npx --no-install flow trace W015-T003` | Resolve a task's implementation commit. |
47
+ | Command | Purpose |
48
+ | ---------------------------------------- | --------------------------------------------------------------------------- |
49
+ | `npx --no-install flow init` | Configure or add/refresh runtime integrations. |
50
+ | `npx --no-install flow migrate` | Atomic structural migration, followed by semantic reconciliation via /flow. |
51
+ | `npx --no-install flow status` | Progress and dependency/external blockers. |
52
+ | `npx --no-install flow validate` | Fast artifact integrity, approvals, DAGs and traceability checks. |
53
+ | `npx --no-install flow validate --gates` | Full validation, including configured deterministic project gates. |
54
+ | `npx --no-install flow route --json` | Next legal step and required context. |
55
+ | `npx --no-install flow graph` | Regenerate dependency graph. |
56
+ | `npx --no-install flow trace W015-T003` | Resolve a task's implementation commit. |
56
57
 
57
58
  Use `--help` for options; `--path` selects a project. Existing managed projects cannot change engineering through init; use /flow and approval. Older config is refused without mutation and must be migrated first. Update with your package manager, then rerun init to refresh integrations; there is no separate update/config/gates workflow command.
58
59
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@caiqueoak/flow",
3
- "version": "0.5.2",
3
+ "version": "0.6.0",
4
4
  "description": "Readability-first, agent-agnostic software development workflow for coding agents.",
5
5
  "type": "module",
6
6
  "scripts": {
package/src/cli.mjs CHANGED
@@ -19,7 +19,19 @@ const commands = {
19
19
  };
20
20
 
21
21
  function help() {
22
- info(
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(
23
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.`
24
36
  );
25
37
  }
@@ -61,8 +61,11 @@ export function evaluateGates(root) {
61
61
  if (!fs.existsSync(file)) fail('gates.yaml does not exist.');
62
62
  const { gates } = parseGates(fs.readFileSync(file, 'utf8'));
63
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}'.`);
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) };
67
70
  });
68
71
  }
@@ -5,32 +5,46 @@ import { projectRoot } from '../shared/project-path.mjs';
5
5
  export function traceTask(root, qualifiedTaskId) {
6
6
  if (!/^W\d{3,}-T\d{3,}$/.test(qualifiedTaskId))
7
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, []]));
8
17
  let output;
9
18
  try {
10
- output = execFileSync(
11
- 'git',
12
- ['log', 'HEAD', '--fixed-strings', `--grep=Flow-Task: ${qualifiedTaskId}`, '--format=%H%x1f%s%x1f%B%x1e'],
13
- { cwd: root, encoding: 'utf8' }
14
- );
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
+ });
15
24
  } catch {
16
25
  fail('git history is unavailable; task traceability requires a Git repository.');
17
26
  }
18
- const trailerPattern = new RegExp(`^Flow-Task:\\s*${qualifiedTaskId}\\s*$`, 'm');
19
- const matches = output
27
+ for (const entry of output
20
28
  .split('\x1e')
21
29
  .filter(Boolean)
22
30
  .map((record) => {
23
31
  const [sha, subject, ...bodyParts] = record.replace(/^\n+|\n+$/g, '').split('\x1f');
24
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' }];
25
46
  })
26
- .filter(
27
- (entry) =>
28
- trailerPattern.test(entry.body) &&
29
- new RegExp(`^Flow-Work-Item:\\s*${qualifiedTaskId.split('-')[0]}\\s*$`, 'm').test(entry.body)
30
- );
31
- if (!matches.length) return { task: qualifiedTaskId, commits: [], status: 'missing' };
32
- if (matches.length > 1) return { task: qualifiedTaskId, commits: matches, status: 'ambiguous' };
33
- return { task: qualifiedTaskId, commit: matches[0], commits: matches, status: 'resolved' };
47
+ );
34
48
  }
35
49
 
36
50
  export function runTrace({ args }) {
@@ -10,11 +10,14 @@ import { parseGates } from '../artifacts/gates.mjs';
10
10
  import { validateEngineeringDocument } from '../artifacts/engineering.mjs';
11
11
  import { validatePrdDocument } from '../artifacts/prd.mjs';
12
12
  import { validateImplementationPlan } from '../artifacts/implementation-plan.mjs';
13
- import { traceTask } from './trace.mjs';
13
+ import { traceTasks } from './trace.mjs';
14
14
  import { generateGraphMarkdown } from './graph.mjs';
15
15
  import { evaluateGates } from './gates.mjs';
16
16
 
17
- export function validateProject(root, { preCommitTask = null, skipTrace = false } = {}) {
17
+ export function validateProject(
18
+ root,
19
+ { preCommitTask = null, skipTrace = false, evaluateConfiguredGates = false, onGateResults } = {}
20
+ ) {
18
21
  const findings = [];
19
22
  const error = (code, message) => findings.push({ level: 'error', code, message });
20
23
  const flow = path.join(root, '.flow');
@@ -81,6 +84,7 @@ export function validateProject(root, { preCommitTask = null, skipTrace = false
81
84
  }
82
85
  const byId = new Map(backlog.work_items.map((item) => [item.id, item]));
83
86
  let activeTasks = 0;
87
+ const traceCandidates = [];
84
88
  for (const item of backlog.work_items) {
85
89
  const base = `work-items/${item.folder}`;
86
90
  const missing = ['spec.md', 'tasks.yaml'].filter((name) => !exists(`${base}/${name}`));
@@ -130,26 +134,32 @@ export function validateProject(root, { preCommitTask = null, skipTrace = false
130
134
  if (item.state !== 'pending' && plan.status !== 'approved') error('PLAN', `${item.id}: plan is not approved.`);
131
135
  }
132
136
  }
133
- for (const task of tasks.tasks) {
134
- if (task.state !== 'completed' || task.implementation !== 'commit' || skipTrace) continue;
135
- const qualified = qualifiedTaskId(item.id, task.id);
136
- if (qualified === preCommitTask) continue;
137
- try {
138
- if (traceTask(root, qualified).status !== 'resolved')
139
- error('TRACE', `${qualified}: expected exactly one HEAD-reachable commit with both Flow trailers.`);
140
- } catch (failure) {
141
- error('TRACE', failure.message);
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);
142
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}`);
143
150
  }
144
151
  }
145
152
  if (activeTasks > 1) error('STATE', 'Only one mutating task may be active across the project.');
146
153
  if (exists('gates.yaml')) {
147
154
  try {
148
155
  parseGates(read('gates.yaml'));
149
- if (!migrationPending)
150
- for (const gate of evaluateGates(root))
156
+ if (!migrationPending && evaluateConfiguredGates) {
157
+ const gateResults = evaluateGates(root);
158
+ onGateResults?.(gateResults);
159
+ for (const gate of gateResults)
151
160
  if (gate.blocking && ['failed', 'unsupported'].includes(gate.status))
152
161
  error('GATE', `${gate.id}: ${gate.status}`);
162
+ }
153
163
  } catch (failure) {
154
164
  error('GATES', failure.message);
155
165
  }
@@ -160,12 +170,24 @@ export function validateProject(root, { preCommitTask = null, skipTrace = false
160
170
  }
161
171
  export function runValidate({ args }) {
162
172
  const index = args.indexOf('--pre-commit');
173
+ const includeGates = args.includes('--gates');
174
+ let gateResults = [];
163
175
  const findings = validateProject(projectRoot(args), {
164
176
  preCommitTask: index < 0 ? null : args[index + 1],
165
- skipTrace: args.includes('--skip-trace')
177
+ skipTrace: args.includes('--skip-trace'),
178
+ evaluateConfiguredGates: includeGates,
179
+ onGateResults: (results) => {
180
+ gateResults = results;
181
+ }
166
182
  });
167
183
  if (args.includes('--json')) {
168
- info(JSON.stringify({ valid: findings.length === 0, findings }, null, 2));
184
+ info(
185
+ JSON.stringify(
186
+ { valid: findings.length === 0, findings, ...(includeGates ? { gates: gateResults } : {}) },
187
+ null,
188
+ 2
189
+ )
190
+ );
169
191
  if (findings.length) process.exitCode = 1;
170
192
  return;
171
193
  }
@@ -4,7 +4,12 @@ const text = fs.readFileSync(
4
4
  new URL('../../skills/flow/engineering/profiles/readability-first.md', import.meta.url),
5
5
  'utf8'
6
6
  );
7
- export const READABILITY_FIRST_PROFILE = parse(text.match(/^---\r?\n([\s\S]*?)\r?\n---/)[1]);
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);
8
13
  export const ENGINEERING_PROFILES = {
9
14
  'readability-first': READABILITY_FIRST_PROFILE,
10
15
  [READABILITY_FIRST_PROFILE.id]: READABILITY_FIRST_PROFILE