@davidorex/pi-workflows 0.5.0 → 0.9.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 (51) hide show
  1. package/README.md +1 -1
  2. package/agents/audit-finding-verifier.agent.yaml +40 -0
  3. package/agents/audit-results-router.agent.yaml +30 -0
  4. package/agents/gap-resolution-assessor.agent.yaml +40 -0
  5. package/dist/expression.d.ts.map +1 -1
  6. package/dist/expression.js +6 -0
  7. package/dist/expression.js.map +1 -1
  8. package/dist/index.d.ts.map +1 -1
  9. package/dist/index.js +5 -4
  10. package/dist/index.js.map +1 -1
  11. package/dist/step-agent.d.ts.map +1 -1
  12. package/dist/step-agent.js +13 -1
  13. package/dist/step-agent.js.map +1 -1
  14. package/dist/step-block.d.ts +18 -0
  15. package/dist/step-block.d.ts.map +1 -0
  16. package/dist/step-block.js +204 -0
  17. package/dist/step-block.js.map +1 -0
  18. package/dist/template-validation.d.ts +25 -0
  19. package/dist/template-validation.d.ts.map +1 -0
  20. package/dist/template-validation.js +341 -0
  21. package/dist/template-validation.js.map +1 -0
  22. package/dist/types.d.ts +31 -0
  23. package/dist/types.d.ts.map +1 -1
  24. package/dist/workflow-executor.d.ts.map +1 -1
  25. package/dist/workflow-executor.js +31 -5
  26. package/dist/workflow-executor.js.map +1 -1
  27. package/dist/workflow-sdk.d.ts +1 -0
  28. package/dist/workflow-sdk.d.ts.map +1 -1
  29. package/dist/workflow-sdk.js +7 -0
  30. package/dist/workflow-sdk.js.map +1 -1
  31. package/dist/workflow-spec.d.ts.map +1 -1
  32. package/dist/workflow-spec.js +106 -0
  33. package/dist/workflow-spec.js.map +1 -1
  34. package/package.json +7 -8
  35. package/schemas/audit-routing-manifest.schema.json +38 -0
  36. package/schemas/finding-verification.schema.json +34 -0
  37. package/schemas/resolution-assessment.schema.json +36 -0
  38. package/skills/pi-workflows/SKILL.md +1 -1
  39. package/skills/pi-workflows/references/bundled-resources.md +12 -3
  40. package/templates/audit-finding-verifier/task.md +59 -0
  41. package/templates/audit-results-router/task.md +37 -0
  42. package/templates/gap-resolution-assessor/task.md +48 -0
  43. package/templates/handoff-writer/task.md +16 -6
  44. package/templates/phase-author/task.md +8 -0
  45. package/workflows/create-handoff.workflow.yaml +56 -35
  46. package/workflows/create-phase.workflow.yaml +27 -32
  47. package/workflows/do-gap.workflow.yaml +48 -28
  48. package/workflows/fix-audit.workflow.yaml +89 -127
  49. package/workflows/gap-to-phase.workflow.yaml +47 -42
  50. package/workflows/parallel-analysis.workflow.yaml +20 -8
  51. package/workflows/plan-from-requirements.workflow.yaml +16 -27
@@ -13,21 +13,26 @@ input:
13
13
  file: .project/gaps.json
14
14
  array: gaps
15
15
  filter: { status: open }
16
- label: id
16
+ label: [id, description]
17
17
  value: id
18
18
 
19
19
  steps:
20
+ load-blocks:
21
+ block:
22
+ read: gaps
23
+ output:
24
+ format: json
25
+
20
26
  load:
21
27
  command: |
22
- node --experimental-strip-types -e "
23
- import fs from 'fs';
24
- const gapId = process.argv[1];
25
- const data = JSON.parse(fs.readFileSync('.project/gaps.json', 'utf8'));
26
- const gap = data.gaps.find(g => g.id === gapId);
28
+ node -e "
29
+ const gaps = JSON.parse(process.argv[1]);
30
+ const gapId = process.argv[2];
31
+ const gap = gaps.gaps.find(g => g.id === gapId);
27
32
  if (!gap) { console.error('Gap not found: ' + gapId); process.exit(1); }
28
- if (gap.status === 'resolved') { console.error('Gap already resolved: ' + gapId); process.exit(1); }
33
+ if (gap.status === 'resolved') { console.error('Gap already resolved'); process.exit(1); }
29
34
  console.log(JSON.stringify({ gap }));
30
- " '${{ input.gap_id }}'
35
+ " '${{ steps.load-blocks.output | json }}' '${{ input.gap_id }}'
31
36
  output:
32
37
  format: json
33
38
 
@@ -74,7 +79,7 @@ steps:
74
79
  format: json
75
80
  schema: ../schemas/execution-results.schema.json
76
81
 
77
- verify:
82
+ run-checks:
78
83
  command: |
79
84
  node --experimental-strip-types -e "
80
85
  import fs from 'fs';
@@ -100,7 +105,9 @@ steps:
100
105
  if (stderr.includes('error')) errors.push('Lint errors in ' + f);
101
106
  }
102
107
  }
103
- } catch {}
108
+ } catch (e) {
109
+ errors.push('Could not determine changed files: ' + (e.message || 'unknown'));
110
+ }
104
111
 
105
112
  // Validate blocks (JSON parsability)
106
113
  try {
@@ -115,7 +122,9 @@ steps:
115
122
  }
116
123
  }
117
124
  }
118
- } catch {}
125
+ } catch (e) {
126
+ errors.push('Block validation error: ' + (e.message || 'unknown'));
127
+ }
119
128
 
120
129
  const passed = errors.length === 0;
121
130
  console.log(JSON.stringify({ status: passed ? 'passed' : 'failed', errors, passed }));
@@ -123,33 +132,44 @@ steps:
123
132
  output:
124
133
  format: json
125
134
 
135
+ assess-resolution:
136
+ when: "${{ steps.run-checks.output.passed }}"
137
+ agent: gap-resolution-assessor
138
+ input:
139
+ gap: "${{ steps.load.output.gap }}"
140
+ investigation: "${{ steps.investigate.output }}"
141
+ implementation_results: "${{ steps.implement.output }}"
142
+ check_results: "${{ steps.run-checks.output }}"
143
+ context: [implement]
144
+ output:
145
+ format: json
146
+ schema: ../schemas/resolution-assessment.schema.json
147
+
126
148
  route:
127
- when: "${{ steps.verify.output.passed }}"
128
- command: |
129
- node --experimental-strip-types -e "
130
- import fs from 'fs';
131
- const gapId = process.argv[1];
132
- const gapsPath = '.project/gaps.json';
133
- const data = JSON.parse(fs.readFileSync(gapsPath, 'utf8'));
134
- const gap = data.gaps.find(g => g.id === gapId);
135
- if (!gap) { console.error('Gap not found: ' + gapId); process.exit(1); }
136
- Object.assign(gap, { status: 'resolved', resolved_by: 'do-gap-workflow' });
137
- fs.writeFileSync(gapsPath, JSON.stringify(data, null, 2) + '\n');
138
- console.log(JSON.stringify({ routed: true, gap_id: gapId, status: 'resolved' }));
139
- " '${{ input.gap_id }}'
149
+ when: "${{ steps.assess-resolution.output.resolved }}"
150
+ block:
151
+ update:
152
+ name: gaps
153
+ key: gaps
154
+ match:
155
+ id: "${{ input.gap_id }}"
156
+ set:
157
+ status: resolved
158
+ resolved_by: "${{ steps.assess-resolution.output.resolution_summary }}"
140
159
  output:
141
160
  format: json
142
161
 
143
162
  check:
144
- when: "${{ steps.route.output.routed }}"
163
+ when: "${{ steps.route.output.updated }}"
145
164
  gate:
146
- check: "test '${{ steps.verify.output.status }}' = 'passed'"
165
+ check: "test '${{ steps.assess-resolution.output.resolved }}' = 'true'"
147
166
  onFail: fail
148
167
 
149
168
  completion:
150
169
  message: |
151
- Gap ${{ input.gap_id }} — ${{ steps.verify.output.status }}
170
+ Gap ${{ input.gap_id }} — ${{ steps.assess-resolution.output.verdict }}
152
171
  Investigation complexity: ${{ steps.investigate.output.complexity }}
153
172
  Specs implemented: ${{ steps.decompose.output.specs | length }}
173
+ Resolution: ${{ steps.assess-resolution.output.resolution_summary }}
154
174
  include:
155
- - steps.verify.output
175
+ - steps.assess-resolution.output
@@ -3,41 +3,35 @@ description: Fix audit findings grouped by related clusters
3
3
  version: "1"
4
4
 
5
5
  steps:
6
- load:
7
- command: |
8
- node --experimental-strip-types -e "
9
- import fs from 'fs';
10
- const auditsDir = '.project/audits';
11
- const files = fs.readdirSync(auditsDir).filter(f => f.endsWith('.json')).sort();
12
- const latestFile = files[files.length - 1];
13
- const auditPath = auditsDir + '/' + latestFile;
14
- const audit = JSON.parse(fs.readFileSync(auditPath, 'utf8'));
15
- let conformance_reference = null;
16
- try { conformance_reference = JSON.parse(fs.readFileSync('.project/conformance-reference.json', 'utf8')); } catch {}
17
- console.log(JSON.stringify({ audit, conformance_reference, audit_path: auditPath }));
18
- "
6
+ load-audits:
7
+ block:
8
+ readDir: audits
9
+ output:
10
+ format: json
11
+
12
+ load-conformance:
13
+ block:
14
+ read: conformance-reference
15
+ optional: [conformance-reference]
19
16
  output:
20
17
  format: json
21
18
 
19
+ select-audit:
20
+ transform:
21
+ mapping:
22
+ audit: "${{ steps.load-audits.output | last }}"
23
+ conformance_reference: "${{ steps.load-conformance.output }}"
24
+
22
25
  cluster:
23
26
  command: |
24
- node --experimental-strip-types -e "
25
- import fs from 'fs';
26
- import { execSync } from 'child_process';
27
- const audit = JSON.parse(fs.readFileSync('.project/audits/' + fs.readdirSync('.project/audits').filter(f => f.endsWith('.json')).sort().pop(), 'utf8'));
27
+ node -e "
28
+ const audit = JSON.parse(process.argv[1]);
28
29
 
29
- // Filter to findings that still need fixing (code is source of truth)
30
+ // Filter to findings that have fixes and are not already resolved
30
31
  const findings = audit.findings.filter(f => {
31
32
  if (!f.fix) return false;
32
- if (f.fix.verify_method === 'grep' && f.fix.verify_pattern) {
33
- try {
34
- execSync('grep -rn ' + JSON.stringify(f.fix.verify_pattern) + ' ' + f.locations.map(l => l.file).join(' '), { stdio: 'pipe' });
35
- return true; // pattern still matches — needs fixing
36
- } catch { return false; } // pattern gone — already fixed
37
- }
38
- // inspect findings: check resolution field, skip if already resolved
39
33
  if (f.resolution && f.resolution.status === 'passed') return false;
40
- return true; // no automated check — include it
34
+ return true;
41
35
  });
42
36
 
43
37
  if (findings.length === 0) {
@@ -67,14 +61,11 @@ steps:
67
61
  principle: cluster[0].principle,
68
62
  category: cluster[0].category,
69
63
  severity: cluster.some(c => c.severity === 'error') ? 'error' : cluster[0].severity,
70
- acceptance_criteria: cluster.map(c => {
71
- if (c.fix && c.fix.verify_method === 'grep' && c.fix.verify_pattern) return 'grep pattern should not match: ' + c.fix.verify_pattern + ' in ' + c.locations.map(l=>l.file).join(', ');
72
- return 'Fix: ' + (c.fix?.suggestion || c.description);
73
- })
64
+ acceptance_criteria: cluster.map(c => 'Fix: ' + (c.fix?.suggestion || c.description))
74
65
  });
75
66
  }
76
67
  console.log(JSON.stringify({ tasks }));
77
- "
68
+ " '${{ steps.select-audit.output.audit | json }}'
78
69
  output:
79
70
  format: json
80
71
 
@@ -84,121 +75,92 @@ steps:
84
75
  agent: audit-fixer
85
76
  input:
86
77
  task: "${{ task }}"
87
- conformance_reference: "${{ steps.load.output.conformance_reference }}"
78
+ conformance_reference: "${{ steps.select-audit.output.conformance_reference }}"
88
79
  output:
89
80
  format: json
90
81
  schema: ../schemas/execution-results.schema.json
91
82
  path: .project/fix-audit-results.json
92
83
 
93
- route-results:
94
- command: |
95
- node --experimental-strip-types -e "
96
- import fs from 'fs';
97
- const raw = JSON.parse(fs.readFileSync('.project/fix-audit-results.json', 'utf8'));
98
- const items = Array.isArray(raw) ? raw : [raw];
99
-
100
- const decisionsPath = '.project/decisions.json';
101
- if (fs.existsSync(decisionsPath)) {
102
- const d = JSON.parse(fs.readFileSync(decisionsPath, 'utf8'));
103
- const existingIds = new Set(d.decisions.map(x => x.id));
104
- for (const r of items) {
105
- for (const dec of r.decisions || []) {
106
- if (!existingIds.has(dec.id)) { d.decisions.push(dec); existingIds.add(dec.id); }
107
- }
108
- }
109
- fs.writeFileSync(decisionsPath, JSON.stringify(d, null, 2) + '\n');
110
- }
111
-
112
- const gapsPath = '.project/gaps.json';
113
- if (fs.existsSync(gapsPath)) {
114
- const g = JSON.parse(fs.readFileSync(gapsPath, 'utf8'));
115
- const existingIds = new Set(g.gaps.map(x => x.id));
116
- for (const r of items) {
117
- for (const issue of r.issues || []) {
118
- const id = 'issue-' + issue.description.toLowerCase().replace(/[^a-z0-9]+/g, '-').slice(0, 50);
119
- if (!existingIds.has(id)) {
120
- g.gaps.push({ id, description: issue.description, status: 'open', category: 'issue', priority: issue.severity === 'critical' ? 'high' : issue.severity === 'error' ? 'medium' : 'low', source: 'agent' });
121
- existingIds.add(id);
122
- }
123
- }
124
- }
125
- fs.writeFileSync(gapsPath, JSON.stringify(g, null, 2) + '\n');
126
- }
127
-
128
- const invPath = '.project/inventory.json';
129
- if (fs.existsSync(invPath)) {
130
- const inv = JSON.parse(fs.readFileSync(invPath, 'utf8'));
131
- let maxTests = inv.test_count || 0;
132
- for (const r of items) { if (r.test_count && r.test_count > maxTests) maxTests = r.test_count; }
133
- if (maxTests > inv.test_count) { inv.test_count = maxTests; fs.writeFileSync(invPath, JSON.stringify(inv, null, 2) + '\n'); }
134
- }
135
-
136
- const statePath = '.project/state.json';
137
- if (fs.existsSync(statePath)) {
138
- const s = JSON.parse(fs.readFileSync(statePath, 'utf8'));
139
- const lastCommit = items.filter(r => r.commit_hash).pop()?.commit_hash;
140
- if (lastCommit) s.last_commit = lastCommit;
141
- const maxTests = Math.max(...items.map(r => r.test_count || 0));
142
- if (maxTests > 0) s.test_count = maxTests;
143
- s.session.last_action = 'fix-audit workflow: ' + items.length + ' tasks completed';
144
- fs.writeFileSync(statePath, JSON.stringify(s, null, 2) + '\n');
145
- }
84
+ verify-findings:
85
+ agent: audit-finding-verifier
86
+ input:
87
+ audit: "${{ steps.select-audit.output.audit }}"
88
+ implementation_results: "${{ steps.implement.output }}"
89
+ conformance_reference: "${{ steps.select-audit.output.conformance_reference }}"
90
+ context: [implement]
91
+ output:
92
+ format: json
93
+ schema: ../schemas/finding-verification.schema.json
94
+ path: .project/fix-audit-verify.json
146
95
 
147
- console.log(JSON.stringify({ routed: items.length, decisions_added: items.reduce((s,r) => s + (r.decisions?.length || 0), 0), issues_added: items.reduce((s,r) => s + (r.issues?.length || 0), 0) }));
148
- "
96
+ prepare-routing:
97
+ agent: audit-results-router
98
+ input:
99
+ implementation_results: "${{ steps.implement.output }}"
100
+ verification: "${{ steps.verify-findings.output }}"
101
+ context: [implement, verify-findings]
102
+ output:
103
+ format: json
104
+ schema: ../schemas/audit-routing-manifest.schema.json
105
+
106
+ route-decisions:
107
+ when: "${{ steps.prepare-routing.output.decisions | length }}"
108
+ forEach: "${{ steps.prepare-routing.output.decisions }}"
109
+ as: decision
110
+ block:
111
+ append:
112
+ name: decisions
113
+ key: decisions
114
+ item: "${{ decision }}"
149
115
  output:
150
116
  format: json
151
117
 
152
- verify:
153
- command: |
154
- node --experimental-strip-types -e "
155
- import fs from 'fs';
156
- import { execSync } from 'child_process';
157
- const auditPath = '.project/audits/' + fs.readdirSync('.project/audits').filter(f => f.endsWith('.json')).sort().pop();
158
- const audit = JSON.parse(fs.readFileSync(auditPath, 'utf8'));
159
- const results = [];
160
- for (const finding of audit.findings) {
161
- const fix = finding.fix;
162
- if (!fix) { results.push({ id: finding.id, status: 'skipped', reason: 'no fix defined' }); continue; }
163
- if (fix.verify_method === 'grep' && fix.verify_pattern) {
164
- try {
165
- execSync('grep -rn ' + JSON.stringify(fix.verify_pattern) + ' ' + finding.locations.map(l => l.file).join(' '), { encoding: 'utf8' });
166
- results.push({ id: finding.id, status: 'failed', reason: 'pattern still matches' });
167
- } catch { results.push({ id: finding.id, status: 'passed' }); }
168
- } else {
169
- results.push({ id: finding.id, status: 'needs_inspect', reason: fix.verify_method });
170
- }
171
- }
172
- const passed = results.filter(r => r.status === 'passed').length;
173
- const failed = results.filter(r => r.status === 'failed').length;
174
- const inspect = results.filter(r => r.status === 'needs_inspect').length;
175
- const total = results.length;
176
- const status = failed === 0 ? (inspect > 0 ? 'passed_with_inspect' : 'passed') : 'gaps_found';
177
- console.log(JSON.stringify({ status, score: passed + '/' + total, passed, failed, needs_inspect: inspect, results }));
178
- "
118
+ route-issues:
119
+ when: "${{ steps.prepare-routing.output.new_gaps | length }}"
120
+ forEach: "${{ steps.prepare-routing.output.new_gaps }}"
121
+ as: gap_item
122
+ block:
123
+ append:
124
+ name: gaps
125
+ key: gaps
126
+ item: "${{ gap_item }}"
179
127
  output:
180
128
  format: json
181
- path: .project/fix-audit-verify.json
182
129
 
183
130
  update-audit:
184
131
  command: |
185
- node --experimental-strip-types -e "
186
- import fs from 'fs';
187
- const auditPath = '.project/audits/' + fs.readdirSync('.project/audits').filter(f => f.endsWith('.json')).sort().pop();
132
+ node -e "
133
+ const fs = require('fs');
134
+ const auditsDir = '.project/audits';
135
+ const auditFile = fs.readdirSync(auditsDir).filter(f => f.endsWith('.json')).sort().pop();
136
+ if (!auditFile) { console.error('No audit files found'); process.exit(1); }
137
+ const auditPath = auditsDir + '/' + auditFile;
188
138
  const verifyPath = '.project/fix-audit-verify.json';
189
139
  let verifyResults;
190
- try { verifyResults = JSON.parse(fs.readFileSync(verifyPath, 'utf8')); } catch { console.log(JSON.stringify({ updated: null, reason: 'cannot read verify output' })); process.exit(0); }
140
+ try {
141
+ verifyResults = JSON.parse(fs.readFileSync(verifyPath, 'utf8'));
142
+ } catch (e) {
143
+ console.error('Cannot read verification output: ' + e.message);
144
+ process.exit(1);
145
+ }
146
+
191
147
  const audit = JSON.parse(fs.readFileSync(auditPath, 'utf8'));
192
- for (const vr of verifyResults.results || []) {
148
+ let resolved = 0;
149
+ for (const vr of verifyResults.findings || []) {
193
150
  const finding = audit.findings.find(f => f.id === vr.id);
194
151
  if (finding) {
195
- finding.resolution = { status: vr.status, verified_at: new Date().toISOString() };
152
+ finding.resolution = {
153
+ status: vr.status,
154
+ verified_at: new Date().toISOString(),
155
+ verified_by: 'agent',
156
+ evidence: vr.evidence || null
157
+ };
158
+ if (vr.status === 'passed') resolved++;
196
159
  }
197
160
  }
198
- const passed = (verifyResults.results || []).filter(r => r.status === 'passed').length;
199
- audit.summary.resolved = passed;
161
+ audit.summary.resolved = resolved;
200
162
  fs.writeFileSync(auditPath, JSON.stringify(audit, null, 2) + '\n');
201
- console.log(JSON.stringify({ updated: auditPath, resolved: passed, total: audit.findings.length }));
163
+ console.log(JSON.stringify({ updated: auditPath, resolved, total: audit.findings.length }));
202
164
  "
203
165
  output:
204
166
  format: json
@@ -206,12 +168,12 @@ steps:
206
168
  check:
207
169
  when: "${{ steps.update-audit.output.updated }}"
208
170
  gate:
209
- check: "test '${{ steps.verify.output.failed }}' = '0'"
171
+ check: "test '${{ steps.verify-findings.output.status }}' != 'gaps_found'"
210
172
  onFail: fail
211
173
 
212
174
  completion:
213
175
  message: |
214
176
  Audit fix workflow completed.
215
- Verification: ${{ steps.verify.output.status }} (${{ steps.verify.output.score }})
177
+ Verification: ${{ steps.verify-findings.output.status }} (${{ steps.verify-findings.output.score }})
216
178
  include:
217
- - steps.verify.output.results
179
+ - steps.verify-findings.output.findings
@@ -17,45 +17,45 @@ input:
17
17
  value: id
18
18
 
19
19
  steps:
20
+ load-gap-blocks:
21
+ block:
22
+ read: gaps
23
+ output:
24
+ format: json
25
+
20
26
  load-gap:
21
27
  command: |
22
- node --experimental-strip-types -e "
23
- import fs from 'fs';
24
- const gapId = process.argv[1];
25
- const data = JSON.parse(fs.readFileSync('.project/gaps.json', 'utf8'));
26
- const gap = data.gaps.find(g => g.id === gapId);
28
+ node -e "
29
+ const gaps = JSON.parse(process.argv[1]);
30
+ const gapId = process.argv[2];
31
+ const gap = gaps.gaps.find(g => g.id === gapId);
27
32
  if (!gap) { console.error('Gap not found: ' + gapId); process.exit(1); }
28
- if (gap.status === 'resolved') { console.error('Gap already resolved: ' + gapId); process.exit(1); }
33
+ if (gap.status === 'resolved') { console.error('Gap already resolved'); process.exit(1); }
29
34
  console.log(JSON.stringify({ gap }));
30
- " '${{ input.gap_id }}'
35
+ " '${{ steps.load-gap-blocks.output | json }}' '${{ input.gap_id }}'
31
36
  output:
32
37
  format: json
33
38
 
34
- load-context:
35
- command: |
36
- node --experimental-strip-types -e "
37
- import fs from 'fs';
38
- import path from 'path';
39
-
40
- const phases = [];
41
- const phasesDir = '.project/phases';
42
- if (fs.existsSync(phasesDir)) {
43
- for (const f of fs.readdirSync(phasesDir).filter(f => f.endsWith('.json')).sort()) {
44
- phases.push(JSON.parse(fs.readFileSync(path.join(phasesDir, f), 'utf8')));
45
- }
46
- }
39
+ load-phases:
40
+ block:
41
+ readDir: phases
42
+ output:
43
+ format: json
47
44
 
48
- let architecture = {};
49
- try { architecture = JSON.parse(fs.readFileSync('.project/architecture.json', 'utf8')); } catch {}
50
- let conventions = {};
51
- try { conventions = JSON.parse(fs.readFileSync('.project/conventions.json', 'utf8')); } catch {}
52
- let gaps = { gaps: [] };
53
- try { gaps = JSON.parse(fs.readFileSync('.project/gaps.json', 'utf8')); } catch {}
54
- let inventory = {};
55
- try { inventory = JSON.parse(fs.readFileSync('.project/inventory.json', 'utf8')); } catch {}
45
+ load-context:
46
+ block:
47
+ read: [architecture, conventions, gaps, inventory]
48
+ optional: [architecture, conventions, inventory]
49
+ output:
50
+ format: json
56
51
 
57
- console.log(JSON.stringify({ phases, architecture, conventions, gaps: gaps.gaps.filter(g => g.status === 'open'), inventory }));
58
- "
52
+ filter-gaps:
53
+ command: |
54
+ node -e "
55
+ const data = JSON.parse(process.argv[1]);
56
+ const open = (data.gaps || []).filter(g => g.status === 'open');
57
+ console.log(JSON.stringify(open));
58
+ " '${{ steps.load-context.output.gaps | json }}'
59
59
  output:
60
60
  format: json
61
61
 
@@ -64,34 +64,39 @@ steps:
64
64
  input:
65
65
  intent: "Implement gap ${{ steps.load-gap.output.gap.id }}: ${{ steps.load-gap.output.gap.description }}"
66
66
  gap: "${{ steps.load-gap.output.gap }}"
67
- phases: "${{ steps.load-context.output.phases }}"
67
+ phases: "${{ steps.load-phases.output }}"
68
68
  architecture: "${{ steps.load-context.output.architecture }}"
69
69
  conventions: "${{ steps.load-context.output.conventions }}"
70
- gaps: "${{ steps.load-context.output.gaps }}"
70
+ gaps: "${{ steps.filter-gaps.output }}"
71
71
  inventory: "${{ steps.load-context.output.inventory }}"
72
72
  output:
73
73
  format: json
74
74
  schema: block:phase
75
75
 
76
- write-phase:
76
+ compute-phase-path:
77
77
  command: |
78
- node --experimental-strip-types -e "
79
- import fs from 'fs';
78
+ node -e "
80
79
  const phase = JSON.parse(process.argv[1]);
81
80
  const num = String(phase.number).padStart(2, '0');
82
- const name = phase.name.toLowerCase().replace(/\s+/g, '-');
83
- const filename = '.project/phases/' + num + '-' + name + '.json';
84
- fs.mkdirSync('.project/phases', { recursive: true });
85
- fs.writeFileSync(filename, JSON.stringify(phase, null, 2));
86
- console.log(JSON.stringify({ path: filename, phase_number: phase.number, spec_count: (phase.specs || []).length }));
81
+ const name = phase.name.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/-+$/g, '');
82
+ console.log(JSON.stringify({ path: 'phases/' + num + '-' + name }));
87
83
  " '${{ steps.author.output | json }}'
88
84
  output:
89
85
  format: json
90
86
 
87
+ write-phase:
88
+ block:
89
+ write:
90
+ name: phase
91
+ data: "${{ steps.author.output }}"
92
+ path: "${{ steps.compute-phase-path.output.path }}"
93
+ output:
94
+ format: json
95
+
91
96
  completion:
92
97
  message: |
93
- Phase ${{ steps.write-phase.output.phase_number }} created: ${{ steps.write-phase.output.path }}
94
- Specs: ${{ steps.write-phase.output.spec_count }}
98
+ Phase ${{ steps.author.output.number }} created: ${{ steps.write-phase.output.path }}
99
+ Specs: ${{ steps.author.output.specs | length }}
95
100
  From gap: ${{ input.gap_id }}
96
101
  include:
97
102
  - steps.author.output
@@ -25,38 +25,50 @@ steps:
25
25
  analyze-structure:
26
26
  agent: structure-analyzer
27
27
  input:
28
- exploration: ${{ steps.explore.textOutput }}
28
+ exploration: ${{ steps.explore.output }}
29
29
  path: ${{ input.path }}
30
+ output:
31
+ format: json
32
+ schema: ../schemas/structure-analysis.schema.json
30
33
 
31
34
  analyze-quality:
32
35
  agent: quality-analyzer
33
36
  input:
34
- exploration: ${{ steps.explore.textOutput }}
37
+ exploration: ${{ steps.explore.output }}
35
38
  path: ${{ input.path }}
39
+ output:
40
+ format: json
41
+ schema: ../schemas/quality-analysis.schema.json
36
42
 
37
43
  analyze-patterns:
38
44
  agent: pattern-analyzer
39
45
  input:
40
- exploration: ${{ steps.explore.textOutput }}
46
+ exploration: ${{ steps.explore.output }}
41
47
  path: ${{ input.path }}
48
+ output:
49
+ format: json
50
+ schema: ../schemas/pattern-analysis.schema.json
42
51
 
43
52
  # This step depends on all three analyzers — it must wait for all to complete
44
53
  synthesize:
45
54
  agent: synthesizer
46
55
  input:
47
- structure: ${{ steps.analyze-structure.textOutput }}
48
- quality: ${{ steps.analyze-quality.textOutput }}
49
- patterns: ${{ steps.analyze-patterns.textOutput }}
56
+ structure: ${{ steps.analyze-structure.output }}
57
+ quality: ${{ steps.analyze-quality.output }}
58
+ patterns: ${{ steps.analyze-patterns.output }}
50
59
  focus: ${{ input.focus }}
60
+ output:
61
+ format: json
62
+ schema: ../schemas/synthesis.schema.json
51
63
 
52
64
  artifacts:
53
65
  report:
54
66
  path: latest-analysis.md
55
- from: steps.synthesize.textOutput
67
+ from: steps.synthesize.output
56
68
 
57
69
  completion:
58
70
  message: |
59
71
  Present this code analysis report to the user. Highlight the most important
60
72
  findings and any areas that need immediate attention.
61
73
  include:
62
- - steps.synthesize.textOutput
74
+ - steps.synthesize.output
@@ -3,33 +3,22 @@ description: Decompose accepted requirements into phases and tasks
3
3
  version: "1"
4
4
 
5
5
  steps:
6
- load-context:
7
- command: |
8
- node -e "
9
- const fs = require('fs');
10
- const path = require('path');
11
- const projectDir = '.project';
12
- const context = {};
13
-
14
- // Read requirements
15
- try { context.requirements = JSON.parse(fs.readFileSync(path.join(projectDir, 'requirements.json'), 'utf-8')); } catch { context.requirements = { requirements: [] }; }
16
-
17
- // Read architecture
18
- try { context.architecture = JSON.parse(fs.readFileSync(path.join(projectDir, 'architecture.json'), 'utf-8')); } catch { context.architecture = null; }
19
-
20
- // Read project identity
21
- try { context.project = JSON.parse(fs.readFileSync(path.join(projectDir, 'project.json'), 'utf-8')); } catch { context.project = null; }
6
+ load-requirements:
7
+ block:
8
+ read: requirements
9
+ output:
10
+ format: json
22
11
 
23
- // Read existing phases
24
- try {
25
- const phasesDir = path.join(projectDir, 'phases');
26
- context.existing_phases = fs.readdirSync(phasesDir).filter(f => f.endsWith('.json')).map(f => {
27
- try { return JSON.parse(fs.readFileSync(path.join(phasesDir, f), 'utf-8')); } catch { return null; }
28
- }).filter(Boolean);
29
- } catch { context.existing_phases = []; }
12
+ load-context:
13
+ block:
14
+ read: [architecture, project]
15
+ optional: [architecture, project]
16
+ output:
17
+ format: json
30
18
 
31
- console.log(JSON.stringify(context));
32
- "
19
+ load-phases:
20
+ block:
21
+ readDir: phases
33
22
  output:
34
23
  format: json
35
24
 
@@ -37,9 +26,9 @@ steps:
37
26
  agent: plan-creator
38
27
  input:
39
28
  project: ${{ steps.load-context.output.project }}
40
- requirements: ${{ steps.load-context.output.requirements }}
29
+ requirements: ${{ steps.load-requirements.output }}
41
30
  architecture: ${{ steps.load-context.output.architecture }}
42
- existing_phases: ${{ steps.load-context.output.existing_phases }}
31
+ existing_phases: ${{ steps.load-phases.output }}
43
32
  output:
44
33
  format: json
45
34
  schema: block:tasks