@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 +10 -9
- package/package.json +1 -1
- package/src/cli.mjs +13 -1
- package/src/commands/gates.mjs +6 -3
- package/src/commands/trace.mjs +29 -15
- package/src/commands/validate.mjs +37 -15
- package/src/shared/profiles.mjs +6 -1
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
|
|
48
|
-
|
|
|
49
|
-
| `npx --no-install flow init`
|
|
50
|
-
| `npx --no-install flow migrate`
|
|
51
|
-
| `npx --no-install flow status`
|
|
52
|
-
| `npx --no-install flow validate`
|
|
53
|
-
| `npx --no-install flow
|
|
54
|
-
| `npx --no-install flow
|
|
55
|
-
| `npx --no-install flow
|
|
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
package/src/cli.mjs
CHANGED
|
@@ -19,7 +19,19 @@ const commands = {
|
|
|
19
19
|
};
|
|
20
20
|
|
|
21
21
|
function help() {
|
|
22
|
-
|
|
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
|
}
|
package/src/commands/gates.mjs
CHANGED
|
@@ -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
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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
|
}
|
package/src/commands/trace.mjs
CHANGED
|
@@ -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
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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
|
|
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
|
-
|
|
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 {
|
|
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(
|
|
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
|
|
135
|
-
|
|
136
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
}
|
package/src/shared/profiles.mjs
CHANGED
|
@@ -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
|
|
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
|