@davidorex/pi-workflows 0.6.1 → 0.9.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 (69) 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/dag.d.ts +13 -2
  6. package/dist/dag.d.ts.map +1 -1
  7. package/dist/dag.js +43 -15
  8. package/dist/dag.js.map +1 -1
  9. package/dist/expression.d.ts.map +1 -1
  10. package/dist/expression.js +6 -0
  11. package/dist/expression.js.map +1 -1
  12. package/dist/index.d.ts.map +1 -1
  13. package/dist/index.js +10 -3
  14. package/dist/index.js.map +1 -1
  15. package/dist/state.d.ts.map +1 -1
  16. package/dist/state.js +14 -1
  17. package/dist/state.js.map +1 -1
  18. package/dist/step-agent.d.ts.map +1 -1
  19. package/dist/step-agent.js +7 -2
  20. package/dist/step-agent.js.map +1 -1
  21. package/dist/step-block.d.ts +18 -0
  22. package/dist/step-block.d.ts.map +1 -0
  23. package/dist/step-block.js +204 -0
  24. package/dist/step-block.js.map +1 -0
  25. package/dist/step-foreach.d.ts.map +1 -1
  26. package/dist/step-foreach.js +5 -0
  27. package/dist/step-foreach.js.map +1 -1
  28. package/dist/step-monitor.d.ts.map +1 -1
  29. package/dist/step-monitor.js +15 -4
  30. package/dist/step-monitor.js.map +1 -1
  31. package/dist/template-validation.d.ts +25 -0
  32. package/dist/template-validation.d.ts.map +1 -0
  33. package/dist/template-validation.js +341 -0
  34. package/dist/template-validation.js.map +1 -0
  35. package/dist/tui.d.ts.map +1 -1
  36. package/dist/tui.js +14 -0
  37. package/dist/tui.js.map +1 -1
  38. package/dist/types.d.ts +33 -0
  39. package/dist/types.d.ts.map +1 -1
  40. package/dist/workflow-discovery.js +2 -1
  41. package/dist/workflow-discovery.js.map +1 -1
  42. package/dist/workflow-executor.d.ts +12 -0
  43. package/dist/workflow-executor.d.ts.map +1 -1
  44. package/dist/workflow-executor.js +32 -6
  45. package/dist/workflow-executor.js.map +1 -1
  46. package/dist/workflow-sdk.d.ts +1 -0
  47. package/dist/workflow-sdk.d.ts.map +1 -1
  48. package/dist/workflow-sdk.js +12 -0
  49. package/dist/workflow-sdk.js.map +1 -1
  50. package/dist/workflow-spec.d.ts.map +1 -1
  51. package/dist/workflow-spec.js +147 -60
  52. package/dist/workflow-spec.js.map +1 -1
  53. package/package.json +7 -8
  54. package/schemas/audit-routing-manifest.schema.json +38 -0
  55. package/schemas/finding-verification.schema.json +34 -0
  56. package/schemas/resolution-assessment.schema.json +36 -0
  57. package/skills/pi-workflows/SKILL.md +1 -1
  58. package/skills/pi-workflows/references/bundled-resources.md +12 -3
  59. package/templates/audit-finding-verifier/task.md +59 -0
  60. package/templates/audit-results-router/task.md +37 -0
  61. package/templates/gap-resolution-assessor/task.md +48 -0
  62. package/templates/handoff-writer/task.md +16 -6
  63. package/templates/phase-author/task.md +8 -0
  64. package/workflows/create-handoff.workflow.yaml +56 -35
  65. package/workflows/create-phase.workflow.yaml +27 -32
  66. package/workflows/do-gap.workflow.yaml +47 -27
  67. package/workflows/fix-audit.workflow.yaml +89 -127
  68. package/workflows/gap-to-phase.workflow.yaml +47 -42
  69. package/workflows/plan-from-requirements.workflow.yaml +16 -27
@@ -3,52 +3,73 @@ description: Capture current work context for session resume
3
3
  version: "1"
4
4
 
5
5
  steps:
6
- load-state:
7
- command: |
8
- node -e "
9
- const fs = require('fs');
10
- const path = require('path');
11
- const projectDir = '.project';
12
- const state = {};
13
-
14
- // Read project identity
15
- try { state.project = JSON.parse(fs.readFileSync(path.join(projectDir, 'project.json'), 'utf-8')); } catch {}
6
+ load-blocks:
7
+ block:
8
+ read: [project, gaps, decisions, tasks]
9
+ optional: [project, gaps, decisions, tasks]
10
+ output:
11
+ format: json
16
12
 
17
- // Read phase files
18
- try {
19
- const phasesDir = path.join(projectDir, 'phases');
20
- state.phases = fs.readdirSync(phasesDir).filter(f => f.endsWith('.json')).map(f => {
21
- try { return JSON.parse(fs.readFileSync(path.join(phasesDir, f), 'utf-8')); } catch { return null; }
22
- }).filter(Boolean);
23
- } catch { state.phases = []; }
13
+ load-phases:
14
+ block:
15
+ readDir: phases
16
+ output:
17
+ format: json
24
18
 
25
- // Read block summaries
26
- try {
27
- state.gaps = JSON.parse(fs.readFileSync(path.join(projectDir, 'gaps.json'), 'utf-8'));
28
- } catch {}
29
- try {
30
- state.decisions = JSON.parse(fs.readFileSync(path.join(projectDir, 'decisions.json'), 'utf-8'));
31
- } catch {}
19
+ load-git:
20
+ command: |
21
+ node -e "
22
+ const { execSync } = require('child_process');
23
+ const git = {};
32
24
  try {
33
- state.tasks = JSON.parse(fs.readFileSync(path.join(projectDir, 'tasks.json'), 'utf-8'));
34
- } catch {}
35
-
36
- // Recent git activity
25
+ git.recent_commits = execSync('git log --oneline -10', { encoding: 'utf-8' }).trim().split('\n');
26
+ } catch (e) {
27
+ git.recent_commits = [];
28
+ git.commits_error = e.message || 'git log failed';
29
+ }
37
30
  try {
38
- const { execSync } = require('child_process');
39
- state.recent_commits = execSync('git log --oneline -10', { encoding: 'utf-8' }).trim().split('\n');
40
- state.changed_files = execSync('git diff --name-only HEAD~3 2>/dev/null || echo \"\"', { encoding: 'utf-8' }).trim().split('\n').filter(Boolean);
41
- } catch { state.recent_commits = []; state.changed_files = []; }
42
-
43
- console.log(JSON.stringify(state));
31
+ git.changed_files = execSync('git diff --name-only HEAD~3 2>/dev/null || echo \"\"', { encoding: 'utf-8' }).trim().split('\n').filter(Boolean);
32
+ } catch (e) {
33
+ git.changed_files = [];
34
+ git.files_error = e.message || 'git diff failed';
35
+ }
36
+ console.log(JSON.stringify(git));
44
37
  "
45
38
  output:
46
39
  format: json
47
40
 
41
+ assemble-state:
42
+ command: |
43
+ node -e "
44
+ const blocks = JSON.parse(process.argv[1]);
45
+ const phases = JSON.parse(process.argv[2]);
46
+ const git = JSON.parse(process.argv[3]);
47
+ console.log(JSON.stringify({
48
+ project: blocks.project,
49
+ phases: phases,
50
+ gaps: blocks.gaps,
51
+ decisions: blocks.decisions,
52
+ tasks: blocks.tasks,
53
+ recentCommits: git.recent_commits || [],
54
+ changedFiles: git.changed_files || [],
55
+ blocks_status: {
56
+ project: blocks.project != null,
57
+ gaps: blocks.gaps != null,
58
+ decisions: blocks.decisions != null,
59
+ tasks: blocks.tasks != null,
60
+ phases: phases.length > 0,
61
+ git_commits: !git.commits_error,
62
+ git_files: !git.files_error
63
+ }
64
+ }));
65
+ " '${{ steps.load-blocks.output | json }}' '${{ steps.load-phases.output | json }}' '${{ steps.load-git.output | json }}'
66
+ output:
67
+ format: json
68
+
48
69
  capture:
49
70
  agent: handoff-writer
50
71
  input:
51
- project_state: ${{ steps.load-state.output }}
72
+ project_state: ${{ steps.assemble-state.output }}
52
73
  path: "."
53
74
  output:
54
75
  format: json
@@ -11,28 +11,16 @@ input:
11
11
  description: Unstructured intent from conversation — what needs to be built and why
12
12
 
13
13
  steps:
14
+ load-phases:
15
+ block:
16
+ readDir: phases
17
+ output:
18
+ format: json
19
+
14
20
  load-context:
15
- command: |
16
- node --experimental-strip-types -e "
17
- import fs from 'fs';
18
- import path from 'path';
19
- const phasesDir = '.project/phases';
20
- let phases = [];
21
- try {
22
- phases = fs.readdirSync(phasesDir)
23
- .filter(f => f.endsWith('.json'))
24
- .map(f => JSON.parse(fs.readFileSync(path.join(phasesDir, f), 'utf8')));
25
- } catch {}
26
- let arch = {};
27
- try { arch = JSON.parse(fs.readFileSync('.project/architecture.json', 'utf8')); } catch {}
28
- let conv = {};
29
- try { conv = JSON.parse(fs.readFileSync('.project/conventions.json', 'utf8')); } catch {}
30
- let gapsData = { gaps: [] };
31
- try { gapsData = JSON.parse(fs.readFileSync('.project/gaps.json', 'utf8')); } catch {}
32
- let inv = {};
33
- try { inv = JSON.parse(fs.readFileSync('.project/inventory.json', 'utf8')); } catch {}
34
- console.log(JSON.stringify({ phases, architecture: arch, conventions: conv, gaps: gapsData.gaps, inventory: inv }));
35
- "
21
+ block:
22
+ read: [architecture, conventions, gaps, inventory]
23
+ optional: [architecture, conventions, gaps, inventory]
36
24
  output:
37
25
  format: json
38
26
 
@@ -40,32 +28,39 @@ steps:
40
28
  agent: phase-author
41
29
  input:
42
30
  intent: "${{ input.intent }}"
43
- phases: "${{ steps.load-context.output.phases }}"
31
+ phases: "${{ steps.load-phases.output }}"
44
32
  architecture: "${{ steps.load-context.output.architecture }}"
45
33
  conventions: "${{ steps.load-context.output.conventions }}"
46
- gaps: "${{ steps.load-context.output.gaps }}"
34
+ gaps: "${{ steps.load-context.output.gaps.gaps }}"
47
35
  inventory: "${{ steps.load-context.output.inventory }}"
48
36
  output:
49
37
  format: json
50
38
  schema: block:phase
51
39
 
52
- write-phase:
40
+ compute-phase-path:
53
41
  command: |
54
- node --experimental-strip-types -e "
55
- import fs from 'fs';
42
+ node -e "
56
43
  const phase = JSON.parse(process.argv[1]);
57
- const filename = '.project/phases/' + String(phase.number).padStart(2, '0') + '-' + phase.name.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/-+$/, '') + '.json';
58
- fs.mkdirSync('.project/phases', { recursive: true });
59
- fs.writeFileSync(filename, JSON.stringify(phase, null, 2) + '\n');
60
- console.log(JSON.stringify({ path: filename, phase_number: phase.number, spec_count: phase.specs.length }));
44
+ const num = String(phase.number).padStart(2, '0');
45
+ const name = phase.name.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/-+$/g, '');
46
+ console.log(JSON.stringify({ path: 'phases/' + num + '-' + name }));
61
47
  " '${{ steps.author.output | json }}'
62
48
  output:
63
49
  format: json
64
50
 
51
+ write-phase:
52
+ block:
53
+ write:
54
+ name: phase
55
+ data: "${{ steps.author.output }}"
56
+ path: "${{ steps.compute-phase-path.output.path }}"
57
+ output:
58
+ format: json
59
+
65
60
  completion:
66
61
  message: |
67
- Phase ${{ steps.write-phase.output.phase_number }} created at ${{ steps.write-phase.output.path }}
68
- Contains ${{ steps.write-phase.output.spec_count }} specs.
62
+ Phase ${{ steps.author.output.number }} created at ${{ steps.write-phase.output.path }}
63
+ Contains ${{ steps.author.output.specs | length }} specs.
69
64
  include:
70
65
  - steps.author.output.success_criteria
71
66
  - steps.author.output.specs
@@ -17,17 +17,22 @@ input:
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