@dtlabs/ai-toolkit 0.13.0 → 0.14.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.
- package/CHANGELOG.md +52 -0
- package/bin/cli.js +371 -2
- package/docs/task-executor-bootstrap.md +195 -0
- package/docs/task-executor-cli.md +319 -0
- package/docs/workflow-control-plane.md +102 -0
- package/lib/execution-ledger.js +186 -4
- package/lib/task-executor/claude-process.js +563 -0
- package/lib/task-executor/git.js +1362 -0
- package/lib/task-executor/index.js +3729 -0
- package/lib/task-executor/ownership.js +590 -0
- package/lib/task-executor/plan.js +725 -0
- package/lib/task-executor/store.js +1164 -0
- package/lib/workflow-artifacts.js +102 -0
- package/lib/workflow-control.js +225 -0
- package/lib/workflow-runner.js +39 -0
- package/package.json +1 -1
- package/src/claude/agents/gaia-intervention-documentation-standard.md +9 -9
- package/src/claude/agents/gaia-validate-feature-docs.md +31 -28
- package/src/claude/skills/assess-codebase/SKILL.md +9 -103
- package/src/claude/skills/implement-feature/SKILL.md +74 -159
- package/src/claude/workflows/am-phase1.js +43 -294
- package/src/claude/workflows/am-phase2.js +10 -164
- package/src/claude/workflows/pm-phase1.js +45 -358
- package/src/claude/workflows/pm-phase2.js +50 -571
- package/src/claude/workflows/pm-phase3.js +5 -824
|
@@ -1,301 +1,50 @@
|
|
|
1
1
|
export const meta = {
|
|
2
|
+
controlPlaneVersion: 1,
|
|
2
3
|
name: 'am-phase1',
|
|
3
|
-
description: '
|
|
4
|
-
phases: [
|
|
5
|
-
{ title: 'Discovery', detail: 'Run ai-toolkit list-assets --category agents --format json, apply scope filter' },
|
|
6
|
-
{ title: 'Assessment', detail: 'Dispatch all assessment agents in parallel' },
|
|
7
|
-
{ title: 'Interventions', detail: 'Run intervention-documentation-standard on all findings' },
|
|
8
|
-
{ title: 'Estimates', detail: 'Write Token-Estimate and Effort-Estimate files' },
|
|
9
|
-
],
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
// args: "<target_path> [--scope=area1,area2] --prefix ASSESS-NNN [--force]"
|
|
13
|
-
const argStr = typeof args === 'string' ? args : '.'
|
|
14
|
-
const parts = argStr.trim().split(/\s+/)
|
|
15
|
-
|
|
16
|
-
let targetPath = '.'
|
|
17
|
-
let scope = null
|
|
18
|
-
let prefix = null
|
|
19
|
-
let force = false
|
|
20
|
-
|
|
21
|
-
for (const part of parts) {
|
|
22
|
-
if (part.startsWith('--scope=')) { scope = part.slice('--scope='.length).split(',').map(s => s.trim()) }
|
|
23
|
-
else if (part === '--force') { force = true }
|
|
24
|
-
else if (part === '--prefix') { /* next part is value */ }
|
|
25
|
-
else if (prefix === null && parts[parts.indexOf(part) - 1] === '--prefix') { prefix = part }
|
|
26
|
-
else if (!part.startsWith('--')) { targetPath = part }
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
// Re-parse prefix properly
|
|
30
|
-
const prefixMatch = argStr.match(/--prefix\s+(\S+)/)
|
|
31
|
-
if (prefixMatch) prefix = prefixMatch[1]
|
|
32
|
-
|
|
33
|
-
if (!prefix) prefix = 'ASSESS-001'
|
|
34
|
-
|
|
35
|
-
const outputDir = `docs/assessments/${prefix}`
|
|
36
|
-
|
|
37
|
-
// ── Discovery ─────────────────────────────────────────────────────────────────
|
|
38
|
-
phase('Discovery')
|
|
39
|
-
|
|
40
|
-
const DISCOVERY_SCHEMA = {
|
|
41
|
-
type: 'object',
|
|
42
|
-
properties: {
|
|
43
|
-
assessment_agents: {
|
|
44
|
-
type: 'array',
|
|
45
|
-
items: {
|
|
46
|
-
type: 'object',
|
|
47
|
-
properties: {
|
|
48
|
-
name: { type: 'string' },
|
|
49
|
-
description: { type: 'string' },
|
|
50
|
-
},
|
|
51
|
-
required: ['name'],
|
|
52
|
-
},
|
|
53
|
-
},
|
|
54
|
-
},
|
|
55
|
-
required: ['assessment_agents'],
|
|
4
|
+
description: 'Read-only assessment using verified assessors and deterministic artifact recording.',
|
|
5
|
+
phases: [{ title: 'Discovery' }, { title: 'Assessment' }, { title: 'Interventions' }, { title: 'Estimates' }],
|
|
56
6
|
}
|
|
57
7
|
|
|
8
|
+
const c = control
|
|
9
|
+
const option = key => { const i = args.indexOf(key); return i < 0 ? null : args[i + 1] }
|
|
10
|
+
const prefix = option('--prefix')
|
|
11
|
+
if (!/^ASSESS-\d+$/.test(prefix || '')) throw new Error('Explicit ASSESS-NNN prefix required')
|
|
12
|
+
const targetPath = args[0] && !args[0].startsWith('--') ? args[0] : '.'
|
|
13
|
+
const scopeArg = args.find(a => a.startsWith('--scope='))
|
|
14
|
+
const scope = scopeArg ? scopeArg.slice(8).split(',') : ['quality', 'architecture', 'concurrency']
|
|
58
15
|
const SCOPE_AGENT_MAP = {
|
|
59
|
-
architecture:
|
|
60
|
-
|
|
61
|
-
quality:
|
|
62
|
-
|
|
63
|
-
devops:
|
|
64
|
-
'domain-model':
|
|
65
|
-
dependencies:
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
const
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
`Exclude orchestrators (files containing "manager" in their name).\n` +
|
|
75
|
-
`Exclude remediation-only agents (files containing "refactoring", "hardening", "decomposition" in their name UNLESS they also contain "assessment" or "audit").\n\n` +
|
|
76
|
-
`Return { "assessment_agents": [ { "name": "...", "description": "..." }, ... ] }`,
|
|
77
|
-
{
|
|
78
|
-
label: 'discover-assessment-agents',
|
|
79
|
-
phase: 'Discovery',
|
|
80
|
-
schema: DISCOVERY_SCHEMA,
|
|
81
|
-
}
|
|
82
|
-
)
|
|
83
|
-
|
|
84
|
-
let assessmentAgents = discovery.assessment_agents || []
|
|
85
|
-
|
|
86
|
-
// Apply scope filter
|
|
87
|
-
if (scope && scope.length > 0) {
|
|
88
|
-
const allowed = new Set(scope.flatMap(s => SCOPE_AGENT_MAP[s] || []))
|
|
89
|
-
assessmentAgents = assessmentAgents.filter(a => allowed.has(a.name))
|
|
90
|
-
log(`Scope filter applied (${scope.join(', ')}): ${assessmentAgents.length} agents selected`)
|
|
91
|
-
} else {
|
|
92
|
-
log(`No scope filter: ${assessmentAgents.length} assessment agents discovered`)
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
if (assessmentAgents.length === 0) {
|
|
96
|
-
log('WARNING: No assessment agents to run — proceeding with empty assessment')
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
// ── Assessment (parallel) ─────────────────────────────────────────────────────
|
|
100
|
-
phase('Assessment')
|
|
101
|
-
|
|
102
|
-
log(`Dispatching ${assessmentAgents.length} assessment agents in parallel`)
|
|
103
|
-
|
|
104
|
-
const tokenLedger = []
|
|
105
|
-
const beforeAssess = budget.spent()
|
|
106
|
-
|
|
107
|
-
const assessmentResults = await parallel(
|
|
108
|
-
assessmentAgents.map(a => () =>
|
|
109
|
-
agent(
|
|
110
|
-
`${targetPath} --prefix ${prefix} --output-dir ${outputDir}`,
|
|
111
|
-
{
|
|
112
|
-
agentType: a.name,
|
|
113
|
-
label: a.name,
|
|
114
|
-
phase: 'Assessment',
|
|
115
|
-
}
|
|
116
|
-
).then(result => ({ agent: a.name, success: true, result }))
|
|
117
|
-
.catch(err => ({ agent: a.name, success: false, error: String(err) }))
|
|
118
|
-
)
|
|
119
|
-
)
|
|
120
|
-
|
|
121
|
-
const assessTokens = budget.spent() - beforeAssess
|
|
122
|
-
log(`Assessment phase complete — ${assessmentResults.filter(r => r?.success).length}/${assessmentAgents.length} succeeded | total phase delta: ${assessTokens} tokens`)
|
|
123
|
-
|
|
124
|
-
// Proportional token distribution across agents
|
|
125
|
-
const perAgentTokens = assessmentAgents.length > 0
|
|
126
|
-
? Math.round(assessTokens / assessmentAgents.length)
|
|
127
|
-
: 0
|
|
128
|
-
|
|
129
|
-
for (const r of assessmentResults.filter(Boolean)) {
|
|
130
|
-
tokenLedger.push({
|
|
131
|
-
agent: r.agent,
|
|
132
|
-
model: 'sonnet',
|
|
133
|
-
phase_delta_tokens: perAgentTokens,
|
|
134
|
-
note: 'proportional distribution of assessment phase total',
|
|
135
|
-
success: r.success,
|
|
136
|
-
error: r.error || null,
|
|
137
|
-
})
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
const failedAgents = assessmentResults.filter(r => r && !r.success)
|
|
141
|
-
const completedAgents = assessmentResults.filter(r => r && r.success)
|
|
142
|
-
|
|
143
|
-
// ── Interventions ─────────────────────────────────────────────────────────────
|
|
144
|
-
phase('Interventions')
|
|
145
|
-
|
|
146
|
-
log('Running intervention-documentation-standard')
|
|
147
|
-
|
|
148
|
-
const beforeInt = budget.spent()
|
|
149
|
-
await agent(
|
|
150
|
-
`--prefix ${prefix} --output-dir ${outputDir} --target ${targetPath}`,
|
|
151
|
-
{
|
|
152
|
-
agentType: 'intervention-documentation-standard',
|
|
153
|
-
label: 'intervention-documentation-standard',
|
|
154
|
-
phase: 'Interventions',
|
|
16
|
+
architecture: 'gaia.agent.assessment.layered-architecture',
|
|
17
|
+
concurrency: 'gaia.agent.assessment.concurrency',
|
|
18
|
+
quality: 'gaia.agent.assessment.generic',
|
|
19
|
+
security: 'gaia.agent.assessment.generic',
|
|
20
|
+
devops: 'gaia.agent.assessment.generic',
|
|
21
|
+
'domain-model': 'gaia.agent.assessment.generic',
|
|
22
|
+
dependencies: 'gaia.agent.assessment.generic',
|
|
23
|
+
}
|
|
24
|
+
for (const area of scope) if (!Object.hasOwn(SCOPE_AGENT_MAP, area)) throw new Error('Unknown assessment scope: ' + area)
|
|
25
|
+
const outputDir = `docs/assessments/${prefix}`
|
|
26
|
+
return c.run(meta.name, outputDir, prefix, async () => {
|
|
27
|
+
const groups = new Map()
|
|
28
|
+
for (const area of scope) {
|
|
29
|
+
const id = SCOPE_AGENT_MAP[area]
|
|
30
|
+
groups.set(id, [...(groups.get(id) || []), area])
|
|
155
31
|
}
|
|
156
|
-
)
|
|
157
|
-
const
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
32
|
+
const assessmentResults = await c.parallel([...groups].map(([id, areas]) => async () => {
|
|
33
|
+
const result = await c.worker(id,
|
|
34
|
+
`${targetPath} --prefix ${prefix} --output-dir ${outputDir}\n` +
|
|
35
|
+
`Read-only assessment. Explicit scope: ${areas.join(', ')}. Inspect risks, evidence, severity and recommendations for EACH listed scope. ` +
|
|
36
|
+
'For security inspect trust boundaries and authorization; devops: build/deploy/observability; domain-model: invariants and ownership; dependencies: versions, provenance and supply-chain risks. ' +
|
|
37
|
+
'Do not remediate code or dispatch remediation agents. Write your assessment report in the output directory.')
|
|
38
|
+
const suffix = { 'gaia.agent.assessment.generic': 'Generic', 'gaia.agent.assessment.layered-architecture': 'Layer', 'gaia.agent.assessment.concurrency': 'Concurrency' }[id]
|
|
39
|
+
const output_file = `${outputDir}/${prefix}-${suffix}-Assessment.md`
|
|
40
|
+
if (!c.read(output_file).trim()) throw new Error('Assessment report empty: ' + output_file)
|
|
41
|
+
return { agent: id, scopes: areas, result, output_file }
|
|
42
|
+
}))
|
|
43
|
+
await c.worker('gaia.agent.assessment.intervention-documentation',
|
|
44
|
+
`--prefix ${prefix} --output-dir ${outputDir} --target ${targetPath}\nUse only the reports from these assessors: ${[...groups.keys()].join(', ')}.\n` +
|
|
45
|
+
assessmentResults.map(r => `${r.agent}: ${typeof r.result === 'string' ? r.result : JSON.stringify(r.result)}`).join('\n'))
|
|
46
|
+
const metrics = c.assessmentEstimates(outputDir, prefix)
|
|
47
|
+
return { prefix, output_dir: outputDir, target_path: targetPath,
|
|
48
|
+
assessment_summaries: assessmentResults.map(({ agent, scopes, output_file }) => ({ agent, scopes, output_file })),
|
|
49
|
+
...metrics, token_ledger: c.tokenLedger, errors: [] }
|
|
163
50
|
})
|
|
164
|
-
log(`intervention-documentation-standard done — phase delta: ${intTokens} tokens`)
|
|
165
|
-
|
|
166
|
-
// ── Estimates ─────────────────────────────────────────────────────────────────
|
|
167
|
-
phase('Estimates')
|
|
168
|
-
|
|
169
|
-
const ESTIMATES_SCHEMA = {
|
|
170
|
-
type: 'object',
|
|
171
|
-
properties: {
|
|
172
|
-
severity_counts: {
|
|
173
|
-
type: 'object',
|
|
174
|
-
properties: {
|
|
175
|
-
CRITICAL: { type: 'number' },
|
|
176
|
-
HIGH: { type: 'number' },
|
|
177
|
-
MEDIUM: { type: 'number' },
|
|
178
|
-
LOW: { type: 'number' },
|
|
179
|
-
},
|
|
180
|
-
},
|
|
181
|
-
total_interventions: { type: 'number' },
|
|
182
|
-
interventions_index_path: { type: 'string' },
|
|
183
|
-
token_estimate_path: { type: 'string' },
|
|
184
|
-
effort_estimate_path: { type: 'string' },
|
|
185
|
-
remediation_hours_est: { type: 'number' },
|
|
186
|
-
},
|
|
187
|
-
required: ['severity_counts', 'total_interventions'],
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
const totalPhaseTokens = tokenLedger.reduce((s, e) => s + (e.phase_delta_tokens || 0), 0)
|
|
191
|
-
|
|
192
|
-
const estimatesResult = await agent(
|
|
193
|
-
`You are the estimates recorder for the assessment pipeline.
|
|
194
|
-
|
|
195
|
-
Prefix: ${prefix}
|
|
196
|
-
Output directory: ${outputDir}
|
|
197
|
-
Assessment agents that ran: ${assessmentAgents.map(a => a.name).join(', ') || 'none'}
|
|
198
|
-
Failed agents: ${failedAgents.map(r => r.agent).join(', ') || 'none'}
|
|
199
|
-
|
|
200
|
-
Token ledger (phase-level measurements):
|
|
201
|
-
${JSON.stringify(tokenLedger, null, 2)}
|
|
202
|
-
|
|
203
|
-
TASK 1 — Read ${prefix}-Interventions-Index.md from ${outputDir}.
|
|
204
|
-
Count rows by Criticality column: CRITICAL, HIGH, MEDIUM, LOW.
|
|
205
|
-
Compute remediation_hours_est = CRITICAL×8 + HIGH×4 + MEDIUM×2 + LOW×1.
|
|
206
|
-
|
|
207
|
-
TASK 2 — Write Token-Estimate file at ${outputDir}/${prefix}-Token-Estimate.md.
|
|
208
|
-
|
|
209
|
-
Use this template (fill in all values):
|
|
210
|
-
|
|
211
|
-
# Token Estimate — ${prefix} — Assessment Pipeline
|
|
212
|
-
|
|
213
|
-
> Phase totals are exact measurements from budget tracking.
|
|
214
|
-
> Per-agent values within the assessment phase are proportional distributions of the phase total.
|
|
215
|
-
> Pricing model: docs/pricing.md (80% input / 20% output split).
|
|
216
|
-
> ⚠️ Per-agent values marked (proportional) are estimated distributions, not exact measurements.
|
|
217
|
-
|
|
218
|
-
## Estimation model
|
|
219
|
-
|
|
220
|
-
| Parameter | Value |
|
|
221
|
-
|-----------|-------|
|
|
222
|
-
| Avg chars per token | 4 |
|
|
223
|
-
| Sonnet system prompt | ~3,000 tokens |
|
|
224
|
-
| Base overhead per call | ~5,000 tokens |
|
|
225
|
-
| Input/output split | 80% / 20% |
|
|
226
|
-
|
|
227
|
-
## Assessment agents (Phase 1)
|
|
228
|
-
|
|
229
|
-
| Agent | Model | Phase delta tokens | Note | Status |
|
|
230
|
-
|-------|-------|--------------------|------|--------|
|
|
231
|
-
{one row per agent from token ledger — use phase_delta_tokens and note fields}
|
|
232
|
-
|
|
233
|
-
## Phase subtotals
|
|
234
|
-
|
|
235
|
-
| Phase | Tokens (exact) |
|
|
236
|
-
|-------|---------------|
|
|
237
|
-
| Assessment agents (parallel) | {sum of assessment agent phase_delta_tokens} |
|
|
238
|
-
| intervention-documentation-standard | {its phase_delta_tokens} |
|
|
239
|
-
| Total am-phase1 | ${totalPhaseTokens} |
|
|
240
|
-
|
|
241
|
-
## Grand total
|
|
242
|
-
|
|
243
|
-
> Updated at pipeline end by assess-codebase skill.
|
|
244
|
-
|
|
245
|
-
| Metric | Value |
|
|
246
|
-
|--------|-------|
|
|
247
|
-
| Total tokens (am-phase1) | ${totalPhaseTokens} (exact) |
|
|
248
|
-
|
|
249
|
-
TASK 3 — Write Effort-Estimate file at ${outputDir}/${prefix}-Effort-Estimate.md.
|
|
250
|
-
|
|
251
|
-
Use this template:
|
|
252
|
-
|
|
253
|
-
# Effort Estimate — ${prefix} — Assessment Pipeline
|
|
254
|
-
|
|
255
|
-
> Wall-clock effort tracking for the assessment pipeline.
|
|
256
|
-
> Remediation effort: CRITICAL=8h, HIGH=4h, MEDIUM=2h, LOW=1h (human hours, sequential).
|
|
257
|
-
|
|
258
|
-
## Assessment phase
|
|
259
|
-
|
|
260
|
-
| Agent | Status |
|
|
261
|
-
|-------|--------|
|
|
262
|
-
{one row per assessment agent}
|
|
263
|
-
| intervention-documentation-standard | complete |
|
|
264
|
-
|
|
265
|
-
## Remediation effort estimate
|
|
266
|
-
|
|
267
|
-
| Severity | Count | Rate | Subtotal |
|
|
268
|
-
|----------|-------|------|---------|
|
|
269
|
-
| CRITICAL | {count} | 8h | {subtotal}h |
|
|
270
|
-
| HIGH | {count} | 4h | {subtotal}h |
|
|
271
|
-
| MEDIUM | {count} | 2h | {subtotal}h |
|
|
272
|
-
| LOW | {count} | 1h | {subtotal}h |
|
|
273
|
-
| **Total** | **{total}** | — | **{total_hours}h** |
|
|
274
|
-
|
|
275
|
-
Human sequential total: {total_hours}h
|
|
276
|
-
|
|
277
|
-
Return the structured metrics.`,
|
|
278
|
-
{
|
|
279
|
-
label: 'write-estimates',
|
|
280
|
-
phase: 'Estimates',
|
|
281
|
-
schema: ESTIMATES_SCHEMA,
|
|
282
|
-
}
|
|
283
|
-
)
|
|
284
|
-
|
|
285
|
-
log(`Estimates written — ${estimatesResult.total_interventions} interventions: CRITICAL=${estimatesResult.severity_counts?.CRITICAL} HIGH=${estimatesResult.severity_counts?.HIGH} MEDIUM=${estimatesResult.severity_counts?.MEDIUM} LOW=${estimatesResult.severity_counts?.LOW}`)
|
|
286
|
-
log(`Remediation estimate: ${estimatesResult.remediation_hours_est}h human sequential`)
|
|
287
|
-
|
|
288
|
-
return {
|
|
289
|
-
prefix,
|
|
290
|
-
output_dir: outputDir,
|
|
291
|
-
target_path: targetPath,
|
|
292
|
-
assessment_summaries: completedAgents.map(r => ({ agent: r.agent })),
|
|
293
|
-
interventions_index_path: estimatesResult.interventions_index_path || `${outputDir}/${prefix}-Interventions-Index.md`,
|
|
294
|
-
severity_counts: estimatesResult.severity_counts || { CRITICAL: 0, HIGH: 0, MEDIUM: 0, LOW: 0 },
|
|
295
|
-
total_interventions: estimatesResult.total_interventions || 0,
|
|
296
|
-
remediation_hours_est: estimatesResult.remediation_hours_est || 0,
|
|
297
|
-
effort_estimate_path: estimatesResult.effort_estimate_path || `${outputDir}/${prefix}-Effort-Estimate.md`,
|
|
298
|
-
token_estimate_path: estimatesResult.token_estimate_path || `${outputDir}/${prefix}-Token-Estimate.md`,
|
|
299
|
-
token_ledger: tokenLedger,
|
|
300
|
-
errors: failedAgents.map(r => ({ agent: r.agent, error: r.error })),
|
|
301
|
-
}
|
|
@@ -1,166 +1,12 @@
|
|
|
1
1
|
export const meta = {
|
|
2
|
+
controlPlaneVersion: 1,
|
|
2
3
|
name: 'am-phase2',
|
|
3
|
-
description: '
|
|
4
|
-
phases: [
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
const argStr = typeof args === 'string' ? args : ''
|
|
13
|
-
|
|
14
|
-
const prefixMatch = argStr.match(/--prefix\s+(\S+)/)
|
|
15
|
-
const outputDirMatch = argStr.match(/--output-dir\s+(\S+)/)
|
|
16
|
-
const flaggedMatch = argStr.match(/--flagged\s+([^\s"]+)/)
|
|
17
|
-
const ackMatch = argStr.match(/--ack\s+"([^"]*)"/)
|
|
18
|
-
|
|
19
|
-
const prefix = prefixMatch ? prefixMatch[1] : 'ASSESS-001'
|
|
20
|
-
const outputDir = outputDirMatch ? outputDirMatch[1] : `docs/assessments/${prefix}`
|
|
21
|
-
const flaggedRaw = flaggedMatch ? flaggedMatch[1] : 'none'
|
|
22
|
-
const ackText = ackMatch ? ackMatch[1] : 'Acknowledged'
|
|
23
|
-
|
|
24
|
-
const flaggedIds = flaggedRaw.toLowerCase() === 'none'
|
|
25
|
-
? []
|
|
26
|
-
: flaggedRaw.split(',').map(s => s.trim().toUpperCase())
|
|
27
|
-
|
|
28
|
-
// ── Approvals ─────────────────────────────────────────────────────────────────
|
|
29
|
-
phase('Approvals')
|
|
30
|
-
|
|
31
|
-
const APPROVALS_SCHEMA = {
|
|
32
|
-
type: 'object',
|
|
33
|
-
properties: {
|
|
34
|
-
approvals_path: { type: 'string' },
|
|
35
|
-
written: { type: 'boolean' },
|
|
36
|
-
},
|
|
37
|
-
required: ['approvals_path', 'written'],
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
log(`Writing Approvals file for ${prefix} — flagged: ${flaggedIds.join(', ') || 'none'}`)
|
|
41
|
-
|
|
42
|
-
const approvalsResult = await agent(
|
|
43
|
-
`Write the Approvals file for assessment ${prefix}.
|
|
44
|
-
|
|
45
|
-
Output path: ${outputDir}/${prefix}-Approvals.md
|
|
46
|
-
|
|
47
|
-
Read the Interventions Index at: ${outputDir}/${prefix}-Interventions-Index.md
|
|
48
|
-
Extract all INT-NNN identifiers and their titles.
|
|
49
|
-
|
|
50
|
-
Flagged interventions (to mark as Yes): ${flaggedIds.join(', ') || 'none'}
|
|
51
|
-
Acknowledgement text: "${ackText}"
|
|
52
|
-
Today's date: use the current date in YYYY-MM-DD format.
|
|
53
|
-
|
|
54
|
-
Write the file with this exact structure:
|
|
55
|
-
|
|
56
|
-
# Assessment Approvals — ${prefix}
|
|
57
|
-
|
|
58
|
-
## Findings Gate Acknowledgement
|
|
59
|
-
|
|
60
|
-
| Field | Value |
|
|
61
|
-
|-------|-------|
|
|
62
|
-
| Acknowledged by | Toolkit user |
|
|
63
|
-
| Date | {today} |
|
|
64
|
-
| Acknowledgement | ${ackText} |
|
|
65
|
-
|
|
66
|
-
## Interventions Flagged for Feature Delivery
|
|
67
|
-
|
|
68
|
-
| Intervention | Flagged | Date | Notes |
|
|
69
|
-
|---|---|---|---|
|
|
70
|
-
{one row per intervention: | INT-NNN — {title} | Yes/No | {today} | — or "Not selected" |}
|
|
71
|
-
|
|
72
|
-
Rules:
|
|
73
|
-
- Mark "Yes" for interventions whose ID is in: ${flaggedIds.join(', ') || '(none)'}
|
|
74
|
-
- Mark "No" for all others, Notes = "Not selected"
|
|
75
|
-
- "Flagged" column values must be exactly "Yes" or "No" (case-sensitive)
|
|
76
|
-
|
|
77
|
-
After writing, verify the file exists and is non-empty.
|
|
78
|
-
Return { "approvals_path": "<full path>", "written": true }.`,
|
|
79
|
-
{
|
|
80
|
-
label: 'write-approvals',
|
|
81
|
-
phase: 'Approvals',
|
|
82
|
-
schema: APPROVALS_SCHEMA,
|
|
83
|
-
}
|
|
84
|
-
)
|
|
85
|
-
|
|
86
|
-
log(`Approvals written: ${approvalsResult.approvals_path}`)
|
|
87
|
-
|
|
88
|
-
// ── Registry ──────────────────────────────────────────────────────────────────
|
|
89
|
-
phase('Registry')
|
|
90
|
-
|
|
91
|
-
const REGISTRY_SCHEMA = {
|
|
92
|
-
type: 'object',
|
|
93
|
-
properties: {
|
|
94
|
-
registry_result: { type: 'string' },
|
|
95
|
-
},
|
|
96
|
-
required: ['registry_result'],
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
log(`Updating Assessment Registry`)
|
|
100
|
-
|
|
101
|
-
const registryResult = await agent(
|
|
102
|
-
`Update the Assessment Registry at: docs/assessments/registry.md
|
|
103
|
-
|
|
104
|
-
Prefix: ${prefix}
|
|
105
|
-
Output directory: ${outputDir}
|
|
106
|
-
|
|
107
|
-
STEP 1 — Read severity counts from ${outputDir}/${prefix}-Interventions-Index.md.
|
|
108
|
-
Count rows by Criticality column: CRITICAL, HIGH, MEDIUM, LOW, compute total.
|
|
109
|
-
|
|
110
|
-
STEP 2 — Count flagged interventions from ${outputDir}/${prefix}-Approvals.md.
|
|
111
|
-
Find the "## Interventions Flagged for Feature Delivery" section.
|
|
112
|
-
Count rows where the Flagged column value is exactly "Yes" (case-sensitive).
|
|
113
|
-
|
|
114
|
-
STEP 3 — Build the registry row:
|
|
115
|
-
| {today-YYYY-MM-DD} | [${prefix}](${prefix}/) | {total} | {CRITICAL} | {HIGH} | {MEDIUM} | {LOW} | {flagged_count} |
|
|
116
|
-
|
|
117
|
-
STEP 4 — Write to docs/assessments/registry.md:
|
|
118
|
-
- If the file does not exist: create it with header + the new row:
|
|
119
|
-
# Assessment Registry
|
|
120
|
-
| Date | Prefix | Total | CRITICAL | HIGH | MEDIUM | LOW | Flagged |
|
|
121
|
-
|------|--------|-------|----------|------|--------|-----|---------|
|
|
122
|
-
{row}
|
|
123
|
-
- If the file exists: read it, strip trailing whitespace, append the new row, write back.
|
|
124
|
-
|
|
125
|
-
STEP 5 — Verify the file is readable after writing.
|
|
126
|
-
|
|
127
|
-
Return { "registry_result": "docs/assessments/registry.md [created]" }
|
|
128
|
-
or { "registry_result": "docs/assessments/registry.md [updated — row N appended]" }
|
|
129
|
-
or { "registry_result": "docs/assessments/registry.md [ERROR — {reason}]" }`,
|
|
130
|
-
{
|
|
131
|
-
label: 'update-registry',
|
|
132
|
-
phase: 'Registry',
|
|
133
|
-
schema: REGISTRY_SCHEMA,
|
|
134
|
-
}
|
|
135
|
-
)
|
|
136
|
-
|
|
137
|
-
log(`Registry: ${registryResult.registry_result}`)
|
|
138
|
-
|
|
139
|
-
// ── Summary ───────────────────────────────────────────────────────────────────
|
|
140
|
-
phase('Summary')
|
|
141
|
-
|
|
142
|
-
const summaryText = [
|
|
143
|
-
`Assessment Manager — Run Summary`,
|
|
144
|
-
`─────────────────────────────────────────────────────`,
|
|
145
|
-
`Target: ${argStr.split('--')[0].trim() || '.'} | Prefix: ${prefix}`,
|
|
146
|
-
`─────────────────────────────────────────────────────`,
|
|
147
|
-
`Approvals: ${approvalsResult.approvals_path}`,
|
|
148
|
-
`Registry: ${registryResult.registry_result}`,
|
|
149
|
-
`Effort Estimate: ${outputDir}/${prefix}-Effort-Estimate.md`,
|
|
150
|
-
`Token Estimate: ${outputDir}/${prefix}-Token-Estimate.md`,
|
|
151
|
-
`Process log: ${outputDir}/${prefix}-process-log.txt`,
|
|
152
|
-
`─────────────────────────────────────────────────────`,
|
|
153
|
-
`Flagged interventions (${flaggedIds.length}): ${flaggedIds.join(', ') || 'none'}`,
|
|
154
|
-
`Flagged interventions can be actioned via /define-feature`,
|
|
155
|
-
`referencing the INT-NNN document.`,
|
|
156
|
-
`─────────────────────────────────────────────────────`,
|
|
157
|
-
].join('\n')
|
|
158
|
-
|
|
159
|
-
log('am-phase2 complete')
|
|
160
|
-
|
|
161
|
-
return {
|
|
162
|
-
approvals_path: approvalsResult.approvals_path,
|
|
163
|
-
registry_updated: registryResult.registry_result,
|
|
164
|
-
flagged_count: flaggedIds.length,
|
|
165
|
-
summary: summaryText,
|
|
166
|
-
}
|
|
4
|
+
description: 'Record explicit Findings Gate acknowledgement and registry deterministically.',
|
|
5
|
+
phases: [{ title: 'Approvals' }, { title: 'Registry' }],
|
|
6
|
+
}
|
|
7
|
+
const c = control
|
|
8
|
+
const option = key => { const i = args.indexOf(key); return i < 0 ? null : args[i + 1] }
|
|
9
|
+
const prefix = option('--prefix'), ack = option('--ack'), flagged = option('--flagged')
|
|
10
|
+
if (!/^ASSESS-\d+$/.test(prefix || '') || !ack?.trim() || flagged == null) throw new Error('Explicit prefix, acknowledgement and flagged selection required')
|
|
11
|
+
const outputDir = option('--output-dir') || `docs/assessments/${prefix}`
|
|
12
|
+
return c.run(meta.name, outputDir, prefix, async () => c.recordAssessmentApproval(outputDir, prefix, ack, flagged))
|