@caiqueoak/flow 0.5.3 → 0.7.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
@@ -30,9 +30,9 @@ PRD, engineering and plans use YAML frontmatter with `schema_version: 1`, `statu
30
30
  ## Canonical artifacts
31
31
 
32
32
  - `config.yaml`: runtime/bootstrap preferences; installed package metadata owns version.
33
- - `docs/prd.md`: product truth; `docs/engineering.md`: approved engineering truth.
33
+ - `docs/prd.md`: global, cross-work-item product truth and learner-visible constraints; `docs/engineering.md`: approved global technical/code/infra truth.
34
34
  - `backlog.yaml`: schema 2, W### IDs, W###-kebab-case folders, kinds, priority, dependency DAG and lifecycle.
35
- - `work-items/W###-slug/spec.md`: bounded scope and decisions; `tasks.yaml`: schema 1, work_item, local T### task DAG.
35
+ - `work-items/W###-slug/spec.md`: bounded scope, delivery decisions, and acceptance for one work item; it references applicable PRD rules without redefining them. `tasks.yaml`: schema 1, work_item, local T### task DAG.
36
36
  - `implementation-plan.md`: human-approved implementation approach.
37
37
  - `state.yaml`: resume cursor and migration reconciliation status.
38
38
  - `gates.yaml`: schema 1, command/builtin checks only; qualitative judgment remains review instructions.
@@ -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.3",
3
+ "version": "0.7.0",
4
4
  "description": "Readability-first, agent-agnostic software development workflow for coding agents.",
5
5
  "type": "module",
6
6
  "scripts": {
@@ -10,12 +10,12 @@ description: Repository-resumable software delivery through discovery, approved
10
10
  Read invariants.md once per invocation. All Flow CLI commands MUST use the project-local installation: `npx --no-install flow`. If unavailable, stop and ask for local installation; never use a global executable or fetch a different package automatically.
11
11
 
12
12
  1. Run `npx --no-install flow route --json`.
13
- 2. Read the returned instruction from this installed skill and every returned required_context file. Planning, implementation and review require rereading the FULL .flow/docs/engineering.md immediately before that step, including after resumption.
13
+ 2. Read the returned instruction from this installed skill and every returned required_context file. Planning, implementation and review require rereading the FULL `.flow/docs/engineering.md` immediately before that step, including after resumption. For a selected work item, use the PRD only for applicable global product rules and use its spec for the bounded delivery scope.
14
14
  3. Execute only the routed step, persist canonical artifacts and run its required checks.
15
15
  4. Route again. Continue until an actual human decision, external action, unrecoverable blocker or completion.
16
16
 
17
17
  If stopped for approval, present the exact proposal and ask explicitly. Never approve your own work or treat silence as consent. Human approval must be persisted with status: approved and approved_at: ISO timestamp in the approved document. Clear state.stop_reason only when the current message resolves its decision. Revisions require a new approval, not copying an old approved_at.
18
18
 
19
- Artifact owners: docs/prd.md product; docs/engineering.md engineering; backlog.yaml work-item DAG; work-items/W###-slug/spec.md scope; tasks.yaml task DAG; implementation-plan.md approved approach; state.yaml cursor; gates.yaml mechanical checks; docs/graph.md derived. Preserve completed history; changed completed scope becomes new maintenance work.
19
+ Artifact owners: `docs/prd.md` owns global, cross-work-item product rules and learner-visible constraints; `docs/engineering.md` owns global technical, code, infrastructure, and pattern constraints; `backlog.yaml` owns the work-item DAG; `work-items/W###-slug/spec.md` owns only bounded scope, acceptance, and decisions for that work item, referencing applicable PRD rules without redefining them; `tasks.yaml` owns the task DAG; `implementation-plan.md` owns the approved approach; `state.yaml` owns the cursor; `gates.yaml` owns mechanical checks; `docs/graph.md` is derived. Preserve completed history; changed completed scope becomes new maintenance work.
20
20
 
21
21
  Engineering changes go through reconcile/step-01-reconcile.md: propose the change, obtain approval and invalidate affected plans. A profile is bootstrap input, never a second policy engine competing with approved engineering.
@@ -1,6 +1,6 @@
1
1
  # Build — execute one approved task
2
2
 
3
- Read the FULL engineering.md, spec, tasks and approved current implementation plan. Verify current route and approval hashes before application edits. Persist both selected work item and task in_progress; no other mutating task may be active.
3
+ Read the applicable global PRD rules, the FULL engineering.md, spec, tasks and approved current implementation plan. Verify current route and approval hashes before application edits. Persist both selected work item and task in_progress; no other mutating task may be active.
4
4
 
5
5
  Implement the bounded approved change for readability: semantic naming, mandatory SRP, low coupling, high cohesion, locality and justified complexity. If the approach must change materially, revise the plan and ask approval before continuing.
6
6
 
@@ -1,5 +1,5 @@
1
1
  # Discovery — define product
2
2
 
3
- Read current intent and existing product evidence. Create .flow/docs/prd.md as a coherent proposal, not architecture. Frontmatter: schema_version: 1, status: draft. Headings: # Product Requirements; ## Purpose; ## Users; ## Scope; ## Requirements; ## Constraints; ## Non-goals.
3
+ Read current intent and existing product evidence. Create `.flow/docs/prd.md` as a coherent proposal for global, cross-work-item product rules and learner-visible constraints, not architecture or work-item implementation detail. Frontmatter: schema_version: 1, status: draft. Headings: # Product Requirements; ## Purpose; ## Users; ## Scope; ## Requirements; ## Constraints; ## Non-goals.
4
4
 
5
5
  Create state.yaml with schema_version: 1, execution.phase: discovery, active.work_item/task: null, stop_reason: null, migration.status: not_required. Do not create incomplete work-item folders or implement code. Present scope and ask PRD approval through the next routed step.
@@ -2,7 +2,7 @@
2
2
 
3
3
  Read the approved PRD, config, full existing engineering evidence, repository tooling/contracts, profiles/readability-first.md and technology-defaults.md. Use existing_code_policy improve to critique accidental structure while preserving behavior; preserve gives consistent patterns stronger weight. Neither authorizes refactoring.
4
4
 
5
- Recommend a complete but proportional contract. Frontmatter: schema_version: 1, status: draft, baseline.profile: flow/readability-first@1, baseline.existing_code_policy: improve|preserve|not_applicable. Include exact headings:
5
+ Recommend a complete but proportional technical contract. Keep global product behavior in the PRD and bounded delivery behavior in work-item specs; engineering records only the technical/code/infra constraints that realize them. Frontmatter: schema_version: 1, status: draft, baseline.profile: flow/readability-first@1, baseline.existing_code_policy: improve|preserve|not_applicable. Include exact headings:
6
6
 
7
7
  # Engineering
8
8
 
@@ -2,7 +2,7 @@
2
2
 
3
3
  Reread the FULL approved engineering.md and PRD. Decompose all known approved scope into bounded work items and dependencies; do not invent future scope. Create backlog.yaml schema_version: 2, work_items with id W###, folder W###-kebab-case, kind feature|technical|maintenance, title, state pending, priority positive integer, depends_on and blockers.
4
4
 
5
- Create EVERY known work-item folder with spec.md and tasks.yaml before implementation. Existing completed artifacts stay intact. Specs include ## Status, ## Goal, ## Scope, ## Non-goals, ## Requirements, ## Acceptance criteria, ## Decisions, ## Implementation, ## Final outcome, ## Validation and ## Follow-up. Tasks: schema_version: 1, work_item: W###, tasks with local T### IDs, title, state pending, depends_on, implementation commit|none. Preserve already completed task history.
5
+ Create EVERY known work-item folder with spec.md and tasks.yaml before implementation. Existing completed artifacts stay intact. Each spec owns only its bounded scope, delivery decisions, and acceptance; reference applicable global PRD rules rather than copying or redefining them. Specs include ## Status, ## Goal, ## Scope, ## Non-goals, ## Requirements, ## Acceptance criteria, ## Decisions, ## Implementation, ## Final outcome, ## Validation and ## Follow-up. Tasks: schema_version: 1, work_item: W###, tasks with local T### IDs, title, state pending, depends_on, implementation commit|none. Preserve already completed task history.
6
6
 
7
7
  Structured blockers: id stable-kebab-case, type external_action|consequential_decision, description, status unresolved|resolved. Do not encode dependencies as external blockers.
8
8
 
@@ -1,6 +1,6 @@
1
1
  # Planning — draft an implementation plan
2
2
 
3
- Reread the FULL approved engineering.md, selected spec/tasks, affected code and tests. Write implementation-plan.md BEFORE application edits. Frontmatter: schema_version: 1, work_item: W###, status: draft, engineering_revision and spec_revision SHA256 of the exact UTF-8 files. Calculate with Node crypto; never guess.
3
+ Reread the applicable global PRD rules, the FULL approved engineering.md, selected spec/tasks, affected code and tests. The PRD supplies product constraints; the spec supplies only this work item's bounded delivery scope. Write implementation-plan.md BEFORE application edits. Frontmatter: schema_version: 1, work_item: W###, status: draft, engineering_revision and spec_revision SHA256 of the exact UTF-8 files. Calculate with Node crypto; never guess.
4
4
 
5
5
  Use exact headings:
6
6
 
@@ -1,5 +1,5 @@
1
1
  # Review — verify delivery
2
2
 
3
- Reread FULL engineering.md, frozen scope, tasks and approved plan. Review acceptance, regressions, SRP, semantic naming, low coupling/high cohesion, vertical-slice locality and complexity ROI. Explain concrete defects, not stylistic alternatives. Run actual tests and npx --no-install flow validate; never write fake gate evidence.
3
+ Reread applicable global PRD rules, FULL engineering.md, frozen scope, tasks and approved plan. Review acceptance, regressions, SRP, semantic naming, low coupling/high cohesion, vertical-slice locality and complexity ROI. Explain concrete defects, not stylistic alternatives. Run actual tests and npx --no-install flow validate; never write fake gate evidence.
4
4
 
5
5
  Defects become new fix tasks. Material approach changes require revised plan approval; never rewrite completed task history. Complete only after all tasks, acceptance, gates, qualitative review and traceability pass. Populate spec outcome/implementation/validation; synchronize backlog/state/graph, validate and route again.
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
  }
@@ -94,7 +94,12 @@ export function routeProject(root) {
94
94
  reason: backlog.work_items.every((item) => item.state === 'completed') ? 'finished' : 'external_action'
95
95
  };
96
96
  const base = `work-items/${item.folder}`;
97
- const context = ['.flow/docs/engineering.md', `.flow/${base}/spec.md`, `.flow/${base}/tasks.yaml`];
97
+ const context = [
98
+ '.flow/docs/prd.md',
99
+ '.flow/docs/engineering.md',
100
+ `.flow/${base}/spec.md`,
101
+ `.flow/${base}/tasks.yaml`
102
+ ];
98
103
  const tasks = parseTasks(read(`${base}/tasks.yaml`), { expectedWorkItem: item.id });
99
104
  const extra = { work_item: item.id, required_context: context };
100
105
  const prepare = () => step('work_item_plan_approval', 'planning/step-02-prepare-plan.md', extra);
@@ -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
  }