@dtlabs/ai-toolkit 0.1.6 → 0.2.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/.claude/agents/define-feature.md +1 -0
- package/.claude/agents/dependency-injection-refactoring.md +1 -0
- package/.claude/agents/dependency-supply-chain-security.md +1 -0
- package/.claude/agents/developer-backend.md +1 -0
- package/.claude/agents/developer-frontend.md +1 -0
- package/.claude/agents/developer-testing.md +1 -0
- package/.claude/agents/domain-model-refactoring.md +1 -0
- package/.claude/agents/god-class-decomposition.md +1 -0
- package/.claude/agents/init-agents-md.md +1 -0
- package/.claude/agents/install-toolkit.md +134 -3
- package/.claude/agents/intervention-documentation-standard.md +1 -0
- package/.claude/agents/review-solution.md +1 -0
- package/.claude/agents/security-hardening.md +1 -0
- package/.claude/commands/check-interventions.md +152 -0
- package/.claude/commands/next-intervention.md +113 -0
- package/.claude/settings.json +5 -0
- package/.claude/skills/assess-codebase/SKILL.md +139 -60
- package/.claude/skills/implement-feature/SKILL.md +208 -26
- package/.claude/workflows/am-phase1.js +304 -0
- package/.claude/workflows/am-phase2.js +166 -0
- package/.claude/workflows/pm-phase1.js +163 -0
- package/.claude/workflows/pm-phase2.js +108 -0
- package/.claude/workflows/pm-phase3.js +309 -0
- package/CHANGELOG.md +101 -3
- package/CLAUDE.md +34 -0
- package/README.md +26 -0
- package/bin/cli.js +64 -5
- package/docs/token-optimization-backlog.md +115 -0
- package/docs/token-optimization.md +274 -0
- package/package.json +1 -1
- package/.claude/agents/assessment-manager.md +0 -416
- package/.claude/agents/project-manager.md +0 -309
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
export const meta = {
|
|
2
|
+
name: 'am-phase1',
|
|
3
|
+
description: 'Assessment pipeline phase 1: discover assessment agents → scope filter → parallel assessment → intervention-documentation-standard → Token-Estimate → Effort-Estimate → findings_gate_payload.',
|
|
4
|
+
phases: [
|
|
5
|
+
{ title: 'Discovery', detail: 'Scan .claude/agents/ for assessment agents, 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'],
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const SCOPE_AGENT_MAP = {
|
|
59
|
+
architecture: ['layered-architecture-assessment'],
|
|
60
|
+
security: ['security-hardening'],
|
|
61
|
+
quality: ['generic-software-assessment'],
|
|
62
|
+
concurrency: ['concurrency-safety-assessment'],
|
|
63
|
+
devops: [],
|
|
64
|
+
'domain-model': ['domain-model-refactoring'],
|
|
65
|
+
dependencies: ['dependency-supply-chain-security'],
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const discovery = await agent(
|
|
69
|
+
`Scan .claude/agents/ and list all assessment agents.
|
|
70
|
+
|
|
71
|
+
An assessment agent is a .md file whose name or description contains one of: "assessment", "audit", "analysis".
|
|
72
|
+
Exclude orchestrators (files containing "manager" in their name).
|
|
73
|
+
Exclude remediation-only agents (files containing "refactoring", "hardening", "decomposition" in their name UNLESS they also contain "assessment" or "audit").
|
|
74
|
+
|
|
75
|
+
For each matching agent, read its frontmatter and extract:
|
|
76
|
+
- name (from the name: field in frontmatter)
|
|
77
|
+
- description (from the description: field)
|
|
78
|
+
|
|
79
|
+
Return { "assessment_agents": [ { "name": "...", "description": "..." }, ... ] }`,
|
|
80
|
+
{
|
|
81
|
+
label: 'discover-assessment-agents',
|
|
82
|
+
phase: 'Discovery',
|
|
83
|
+
schema: DISCOVERY_SCHEMA,
|
|
84
|
+
}
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
let assessmentAgents = discovery.assessment_agents || []
|
|
88
|
+
|
|
89
|
+
// Apply scope filter
|
|
90
|
+
if (scope && scope.length > 0) {
|
|
91
|
+
const allowed = new Set(scope.flatMap(s => SCOPE_AGENT_MAP[s] || []))
|
|
92
|
+
assessmentAgents = assessmentAgents.filter(a => allowed.has(a.name))
|
|
93
|
+
log(`Scope filter applied (${scope.join(', ')}): ${assessmentAgents.length} agents selected`)
|
|
94
|
+
} else {
|
|
95
|
+
log(`No scope filter: ${assessmentAgents.length} assessment agents discovered`)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (assessmentAgents.length === 0) {
|
|
99
|
+
log('WARNING: No assessment agents to run — proceeding with empty assessment')
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// ── Assessment (parallel) ─────────────────────────────────────────────────────
|
|
103
|
+
phase('Assessment')
|
|
104
|
+
|
|
105
|
+
log(`Dispatching ${assessmentAgents.length} assessment agents in parallel`)
|
|
106
|
+
|
|
107
|
+
const tokenLedger = []
|
|
108
|
+
const beforeAssess = budget.spent()
|
|
109
|
+
|
|
110
|
+
const assessmentResults = await parallel(
|
|
111
|
+
assessmentAgents.map(a => () =>
|
|
112
|
+
agent(
|
|
113
|
+
`${targetPath} --prefix ${prefix} --output-dir ${outputDir}`,
|
|
114
|
+
{
|
|
115
|
+
agentType: a.name,
|
|
116
|
+
label: a.name,
|
|
117
|
+
phase: 'Assessment',
|
|
118
|
+
}
|
|
119
|
+
).then(result => ({ agent: a.name, success: true, result }))
|
|
120
|
+
.catch(err => ({ agent: a.name, success: false, error: String(err) }))
|
|
121
|
+
)
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
const assessTokens = budget.spent() - beforeAssess
|
|
125
|
+
log(`Assessment phase complete — ${assessmentResults.filter(r => r?.success).length}/${assessmentAgents.length} succeeded | total phase delta: ${assessTokens} tokens`)
|
|
126
|
+
|
|
127
|
+
// Proportional token distribution across agents
|
|
128
|
+
const perAgentTokens = assessmentAgents.length > 0
|
|
129
|
+
? Math.round(assessTokens / assessmentAgents.length)
|
|
130
|
+
: 0
|
|
131
|
+
|
|
132
|
+
for (const r of assessmentResults.filter(Boolean)) {
|
|
133
|
+
tokenLedger.push({
|
|
134
|
+
agent: r.agent,
|
|
135
|
+
model: 'sonnet',
|
|
136
|
+
phase_delta_tokens: perAgentTokens,
|
|
137
|
+
note: 'proportional distribution of assessment phase total',
|
|
138
|
+
success: r.success,
|
|
139
|
+
error: r.error || null,
|
|
140
|
+
})
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const failedAgents = assessmentResults.filter(r => r && !r.success)
|
|
144
|
+
const completedAgents = assessmentResults.filter(r => r && r.success)
|
|
145
|
+
|
|
146
|
+
// ── Interventions ─────────────────────────────────────────────────────────────
|
|
147
|
+
phase('Interventions')
|
|
148
|
+
|
|
149
|
+
log('Running intervention-documentation-standard')
|
|
150
|
+
|
|
151
|
+
const beforeInt = budget.spent()
|
|
152
|
+
await agent(
|
|
153
|
+
`--prefix ${prefix} --output-dir ${outputDir} --target ${targetPath}`,
|
|
154
|
+
{
|
|
155
|
+
agentType: 'intervention-documentation-standard',
|
|
156
|
+
label: 'intervention-documentation-standard',
|
|
157
|
+
phase: 'Interventions',
|
|
158
|
+
}
|
|
159
|
+
)
|
|
160
|
+
const intTokens = budget.spent() - beforeInt
|
|
161
|
+
tokenLedger.push({
|
|
162
|
+
agent: 'intervention-documentation-standard',
|
|
163
|
+
model: 'sonnet',
|
|
164
|
+
phase_delta_tokens: intTokens,
|
|
165
|
+
note: 'exact phase delta',
|
|
166
|
+
})
|
|
167
|
+
log(`intervention-documentation-standard done — phase delta: ${intTokens} tokens`)
|
|
168
|
+
|
|
169
|
+
// ── Estimates ─────────────────────────────────────────────────────────────────
|
|
170
|
+
phase('Estimates')
|
|
171
|
+
|
|
172
|
+
const ESTIMATES_SCHEMA = {
|
|
173
|
+
type: 'object',
|
|
174
|
+
properties: {
|
|
175
|
+
severity_counts: {
|
|
176
|
+
type: 'object',
|
|
177
|
+
properties: {
|
|
178
|
+
CRITICAL: { type: 'number' },
|
|
179
|
+
HIGH: { type: 'number' },
|
|
180
|
+
MEDIUM: { type: 'number' },
|
|
181
|
+
LOW: { type: 'number' },
|
|
182
|
+
},
|
|
183
|
+
},
|
|
184
|
+
total_interventions: { type: 'number' },
|
|
185
|
+
interventions_index_path: { type: 'string' },
|
|
186
|
+
token_estimate_path: { type: 'string' },
|
|
187
|
+
effort_estimate_path: { type: 'string' },
|
|
188
|
+
remediation_hours_est: { type: 'number' },
|
|
189
|
+
},
|
|
190
|
+
required: ['severity_counts', 'total_interventions'],
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const totalPhaseTokens = tokenLedger.reduce((s, e) => s + (e.phase_delta_tokens || 0), 0)
|
|
194
|
+
|
|
195
|
+
const estimatesResult = await agent(
|
|
196
|
+
`You are the estimates recorder for the assessment pipeline.
|
|
197
|
+
|
|
198
|
+
Prefix: ${prefix}
|
|
199
|
+
Output directory: ${outputDir}
|
|
200
|
+
Assessment agents that ran: ${assessmentAgents.map(a => a.name).join(', ') || 'none'}
|
|
201
|
+
Failed agents: ${failedAgents.map(r => r.agent).join(', ') || 'none'}
|
|
202
|
+
|
|
203
|
+
Token ledger (phase-level measurements):
|
|
204
|
+
${JSON.stringify(tokenLedger, null, 2)}
|
|
205
|
+
|
|
206
|
+
TASK 1 — Read ${prefix}-Interventions-Index.md from ${outputDir}.
|
|
207
|
+
Count rows by Criticality column: CRITICAL, HIGH, MEDIUM, LOW.
|
|
208
|
+
Compute remediation_hours_est = CRITICAL×8 + HIGH×4 + MEDIUM×2 + LOW×1.
|
|
209
|
+
|
|
210
|
+
TASK 2 — Write Token-Estimate file at ${outputDir}/${prefix}-Token-Estimate.md.
|
|
211
|
+
|
|
212
|
+
Use this template (fill in all values):
|
|
213
|
+
|
|
214
|
+
# Token Estimate — ${prefix} — Assessment Pipeline
|
|
215
|
+
|
|
216
|
+
> Phase totals are exact measurements from budget tracking.
|
|
217
|
+
> Per-agent values within the assessment phase are proportional distributions of the phase total.
|
|
218
|
+
> Pricing model: docs/pricing.md (80% input / 20% output split).
|
|
219
|
+
> ⚠️ Per-agent values marked (proportional) are estimated distributions, not exact measurements.
|
|
220
|
+
|
|
221
|
+
## Estimation model
|
|
222
|
+
|
|
223
|
+
| Parameter | Value |
|
|
224
|
+
|-----------|-------|
|
|
225
|
+
| Avg chars per token | 4 |
|
|
226
|
+
| Sonnet system prompt | ~3,000 tokens |
|
|
227
|
+
| Base overhead per call | ~5,000 tokens |
|
|
228
|
+
| Input/output split | 80% / 20% |
|
|
229
|
+
|
|
230
|
+
## Assessment agents (Phase 1)
|
|
231
|
+
|
|
232
|
+
| Agent | Model | Phase delta tokens | Note | Status |
|
|
233
|
+
|-------|-------|--------------------|------|--------|
|
|
234
|
+
{one row per agent from token ledger — use phase_delta_tokens and note fields}
|
|
235
|
+
|
|
236
|
+
## Phase subtotals
|
|
237
|
+
|
|
238
|
+
| Phase | Tokens (exact) |
|
|
239
|
+
|-------|---------------|
|
|
240
|
+
| Assessment agents (parallel) | {sum of assessment agent phase_delta_tokens} |
|
|
241
|
+
| intervention-documentation-standard | {its phase_delta_tokens} |
|
|
242
|
+
| Total am-phase1 | ${totalPhaseTokens} |
|
|
243
|
+
|
|
244
|
+
## Grand total
|
|
245
|
+
|
|
246
|
+
> Updated at pipeline end by assess-codebase skill.
|
|
247
|
+
|
|
248
|
+
| Metric | Value |
|
|
249
|
+
|--------|-------|
|
|
250
|
+
| Total tokens (am-phase1) | ${totalPhaseTokens} (exact) |
|
|
251
|
+
|
|
252
|
+
TASK 3 — Write Effort-Estimate file at ${outputDir}/${prefix}-Effort-Estimate.md.
|
|
253
|
+
|
|
254
|
+
Use this template:
|
|
255
|
+
|
|
256
|
+
# Effort Estimate — ${prefix} — Assessment Pipeline
|
|
257
|
+
|
|
258
|
+
> Wall-clock effort tracking for the assessment pipeline.
|
|
259
|
+
> Remediation effort: CRITICAL=8h, HIGH=4h, MEDIUM=2h, LOW=1h (human hours, sequential).
|
|
260
|
+
|
|
261
|
+
## Assessment phase
|
|
262
|
+
|
|
263
|
+
| Agent | Status |
|
|
264
|
+
|-------|--------|
|
|
265
|
+
{one row per assessment agent}
|
|
266
|
+
| intervention-documentation-standard | complete |
|
|
267
|
+
|
|
268
|
+
## Remediation effort estimate
|
|
269
|
+
|
|
270
|
+
| Severity | Count | Rate | Subtotal |
|
|
271
|
+
|----------|-------|------|---------|
|
|
272
|
+
| CRITICAL | {count} | 8h | {subtotal}h |
|
|
273
|
+
| HIGH | {count} | 4h | {subtotal}h |
|
|
274
|
+
| MEDIUM | {count} | 2h | {subtotal}h |
|
|
275
|
+
| LOW | {count} | 1h | {subtotal}h |
|
|
276
|
+
| **Total** | **{total}** | — | **{total_hours}h** |
|
|
277
|
+
|
|
278
|
+
Human sequential total: {total_hours}h
|
|
279
|
+
|
|
280
|
+
Return the structured metrics.`,
|
|
281
|
+
{
|
|
282
|
+
label: 'write-estimates',
|
|
283
|
+
phase: 'Estimates',
|
|
284
|
+
schema: ESTIMATES_SCHEMA,
|
|
285
|
+
}
|
|
286
|
+
)
|
|
287
|
+
|
|
288
|
+
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}`)
|
|
289
|
+
log(`Remediation estimate: ${estimatesResult.remediation_hours_est}h human sequential`)
|
|
290
|
+
|
|
291
|
+
return {
|
|
292
|
+
prefix,
|
|
293
|
+
output_dir: outputDir,
|
|
294
|
+
target_path: targetPath,
|
|
295
|
+
assessment_summaries: completedAgents.map(r => ({ agent: r.agent })),
|
|
296
|
+
interventions_index_path: estimatesResult.interventions_index_path || `${outputDir}/${prefix}-Interventions-Index.md`,
|
|
297
|
+
severity_counts: estimatesResult.severity_counts || { CRITICAL: 0, HIGH: 0, MEDIUM: 0, LOW: 0 },
|
|
298
|
+
total_interventions: estimatesResult.total_interventions || 0,
|
|
299
|
+
remediation_hours_est: estimatesResult.remediation_hours_est || 0,
|
|
300
|
+
effort_estimate_path: estimatesResult.effort_estimate_path || `${outputDir}/${prefix}-Effort-Estimate.md`,
|
|
301
|
+
token_estimate_path: estimatesResult.token_estimate_path || `${outputDir}/${prefix}-Token-Estimate.md`,
|
|
302
|
+
token_ledger: tokenLedger,
|
|
303
|
+
errors: failedAgents.map(r => ({ agent: r.agent, error: r.error })),
|
|
304
|
+
}
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
export const meta = {
|
|
2
|
+
name: 'am-phase2',
|
|
3
|
+
description: 'Assessment pipeline phase 2: write Approvals file → update Assessment Registry → return summary. Runs after Findings Gate is completed in the main loop.',
|
|
4
|
+
phases: [
|
|
5
|
+
{ title: 'Approvals', detail: 'Write {PREFIX}-Approvals.md with gate acknowledgement and flagged interventions' },
|
|
6
|
+
{ title: 'Registry', detail: 'Append row to docs/assessments/registry.md' },
|
|
7
|
+
{ title: 'Summary', detail: 'Build and return run summary' },
|
|
8
|
+
],
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// args: "--prefix ASSESS-NNN --output-dir <path> --flagged INT-001,INT-003 --ack \"text\""
|
|
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
|
+
}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
export const meta = {
|
|
2
|
+
name: 'pm-phase1',
|
|
3
|
+
description: 'Feature delivery phase 1: discovery → generate-requirements → generate-tech-spec → validate-feature-docs (revision loop) → Effort-Estimate stub. Returns gate1_payload.',
|
|
4
|
+
phases: [
|
|
5
|
+
{ title: 'Discovery', detail: 'Read feature.md, check existing outputs' },
|
|
6
|
+
{ title: 'Requirements', detail: 'Run generate-requirements' },
|
|
7
|
+
{ title: 'Tech-Spec', detail: 'Run generate-tech-spec' },
|
|
8
|
+
{ title: 'Validation', detail: 'Run validate-feature-docs (revision loop, max 3×)' },
|
|
9
|
+
],
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// ── Parse args ────────────────────────────────────────────────────────────────
|
|
13
|
+
// args is the raw prompt string: "<path-to-feature.md> [--force]"
|
|
14
|
+
const featurePath = args.split(/\s+/)[0]
|
|
15
|
+
const force = typeof args === 'string' && args.includes('--force')
|
|
16
|
+
|
|
17
|
+
// ── Discovery ─────────────────────────────────────────────────────────────────
|
|
18
|
+
phase('Discovery')
|
|
19
|
+
|
|
20
|
+
const discoveryResult = await agent(
|
|
21
|
+
`You are a discovery agent for the feature delivery pipeline.
|
|
22
|
+
|
|
23
|
+
Read the feature.md file at: ${featurePath}
|
|
24
|
+
|
|
25
|
+
Extract:
|
|
26
|
+
1. The feature PREFIX from the directory name (pattern [A-Z]+-[0-9]+, e.g. FTR-009)
|
|
27
|
+
2. The feature directory path (parent of feature.md)
|
|
28
|
+
3. Whether these files exist and are NOT stale vs feature.md (stale = older mtime):
|
|
29
|
+
- {PREFIX}-Requirements.md
|
|
30
|
+
- {PREFIX}-Tech-Spec.md
|
|
31
|
+
- {PREFIX}-Validation-Report.md
|
|
32
|
+
|
|
33
|
+
Return a JSON object:
|
|
34
|
+
{
|
|
35
|
+
"prefix": "FTR-009",
|
|
36
|
+
"feature_dir": "internal_docs/features/FTR-009-workflow-orchestrators",
|
|
37
|
+
"needs_requirements": true,
|
|
38
|
+
"needs_tech_spec": true,
|
|
39
|
+
"needs_validation": true,
|
|
40
|
+
"force": ${force}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
If force=true, set all needs_* to true regardless of file state.
|
|
44
|
+
If feature.md does not exist, return { "error": "feature.md not found: ${featurePath}" }.`,
|
|
45
|
+
{
|
|
46
|
+
label: 'discovery',
|
|
47
|
+
phase: 'Discovery',
|
|
48
|
+
schema: {
|
|
49
|
+
type: 'object',
|
|
50
|
+
properties: {
|
|
51
|
+
prefix: { type: 'string' },
|
|
52
|
+
feature_dir: { type: 'string' },
|
|
53
|
+
needs_requirements: { type: 'boolean' },
|
|
54
|
+
needs_tech_spec: { type: 'boolean' },
|
|
55
|
+
needs_validation: { type: 'boolean' },
|
|
56
|
+
force: { type: 'boolean' },
|
|
57
|
+
error: { type: 'string' },
|
|
58
|
+
},
|
|
59
|
+
required: ['prefix', 'feature_dir'],
|
|
60
|
+
},
|
|
61
|
+
}
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
if (discoveryResult.error) {
|
|
65
|
+
return { error: discoveryResult.error }
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const { prefix, feature_dir } = discoveryResult
|
|
69
|
+
|
|
70
|
+
log(`Prefix: ${prefix} | Dir: ${feature_dir}`)
|
|
71
|
+
log(`needs_requirements=${discoveryResult.needs_requirements} needs_tech_spec=${discoveryResult.needs_tech_spec}`)
|
|
72
|
+
|
|
73
|
+
// ── generate-requirements ─────────────────────────────────────────────────────
|
|
74
|
+
phase('Requirements')
|
|
75
|
+
|
|
76
|
+
const tokenLedger = []
|
|
77
|
+
const errors = []
|
|
78
|
+
|
|
79
|
+
if (discoveryResult.needs_requirements) {
|
|
80
|
+
const beforeReq = budget.spent()
|
|
81
|
+
const reqResult = await agent(featurePath, { agentType: 'generate-requirements', label: 'generate-requirements', phase: 'Requirements' })
|
|
82
|
+
const reqTokens = budget.spent() - beforeReq
|
|
83
|
+
tokenLedger.push({ agent: 'generate-requirements', model: 'haiku', phase_delta_tokens: reqTokens })
|
|
84
|
+
log(`generate-requirements done — phase delta: ${reqTokens} tokens`)
|
|
85
|
+
} else {
|
|
86
|
+
log('generate-requirements: fresh — skipped')
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// ── generate-tech-spec ────────────────────────────────────────────────────────
|
|
90
|
+
phase('Tech-Spec')
|
|
91
|
+
|
|
92
|
+
if (discoveryResult.needs_tech_spec) {
|
|
93
|
+
const beforeSpec = budget.spent()
|
|
94
|
+
const specResult = await agent(featurePath, { agentType: 'generate-tech-spec', label: 'generate-tech-spec', phase: 'Tech-Spec' })
|
|
95
|
+
const specTokens = budget.spent() - beforeSpec
|
|
96
|
+
tokenLedger.push({ agent: 'generate-tech-spec', model: 'haiku', phase_delta_tokens: specTokens })
|
|
97
|
+
log(`generate-tech-spec done — phase delta: ${specTokens} tokens`)
|
|
98
|
+
} else {
|
|
99
|
+
log('generate-tech-spec: fresh — skipped')
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// ── validate-feature-docs (revision loop, max 3 cycles) ──────────────────────
|
|
103
|
+
phase('Validation')
|
|
104
|
+
|
|
105
|
+
let validationSummary = 'skipped'
|
|
106
|
+
const MAX_CYCLES = 3
|
|
107
|
+
|
|
108
|
+
for (let cycle = 1; cycle <= MAX_CYCLES; cycle++) {
|
|
109
|
+
log(`validate-feature-docs cycle ${cycle}`)
|
|
110
|
+
const beforeVal = budget.spent()
|
|
111
|
+
const valResult = await agent(featurePath, { agentType: 'validate-feature-docs', label: `validate-feature-docs (cycle ${cycle})`, phase: 'Validation' })
|
|
112
|
+
const valTokens = budget.spent() - beforeVal
|
|
113
|
+
tokenLedger.push({ agent: `validate-feature-docs (cycle ${cycle})`, model: 'haiku', phase_delta_tokens: valTokens })
|
|
114
|
+
|
|
115
|
+
// valResult is the agent's text output — check for gap indicators
|
|
116
|
+
const resultText = typeof valResult === 'string' ? valResult : JSON.stringify(valResult)
|
|
117
|
+
const hasGaps = resultText.includes('MISSING:') || resultText.toLowerCase().includes('gaps found')
|
|
118
|
+
|
|
119
|
+
if (!hasGaps) {
|
|
120
|
+
validationSummary = `0 gaps (clean on cycle ${cycle})`
|
|
121
|
+
log(`Validation clean on cycle ${cycle}`)
|
|
122
|
+
break
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
log(`Validation cycle ${cycle}: gaps found`)
|
|
126
|
+
|
|
127
|
+
if (cycle < MAX_CYCLES) {
|
|
128
|
+
// Re-run failing docs before next validation cycle
|
|
129
|
+
if (resultText.includes('Requirements')) {
|
|
130
|
+
await agent(featurePath, { agentType: 'generate-requirements', label: `generate-requirements (revision ${cycle})`, phase: 'Validation' })
|
|
131
|
+
}
|
|
132
|
+
if (resultText.includes('Tech-Spec')) {
|
|
133
|
+
await agent(featurePath, { agentType: 'generate-tech-spec', label: `generate-tech-spec (revision ${cycle})`, phase: 'Validation' })
|
|
134
|
+
}
|
|
135
|
+
} else {
|
|
136
|
+
validationSummary = 'gaps remain after 3 cycles'
|
|
137
|
+
errors.push('validate-feature-docs: gaps remain after 3 revision cycles')
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// ── Build gate1_payload ───────────────────────────────────────────────────────
|
|
142
|
+
const gate1Payload = {
|
|
143
|
+
prefix,
|
|
144
|
+
feature_dir,
|
|
145
|
+
feature_path: featurePath,
|
|
146
|
+
requirements: {
|
|
147
|
+
path: `${feature_dir}/${prefix}-Requirements.md`,
|
|
148
|
+
summary: `${prefix}-Requirements.md — generated`,
|
|
149
|
+
},
|
|
150
|
+
tech_spec: {
|
|
151
|
+
path: `${feature_dir}/${prefix}-Tech-Spec.md`,
|
|
152
|
+
summary: `${prefix}-Tech-Spec.md — generated`,
|
|
153
|
+
},
|
|
154
|
+
validation: {
|
|
155
|
+
path: `${feature_dir}/${prefix}-Validation-Report.md`,
|
|
156
|
+
summary: validationSummary,
|
|
157
|
+
},
|
|
158
|
+
token_ledger: tokenLedger,
|
|
159
|
+
errors,
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
log(`pm-phase1 complete — gate1_payload ready`)
|
|
163
|
+
return gate1Payload
|