@dtlabs/ai-toolkit 0.1.7 → 0.2.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/.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/agents/validate-feature-docs.md +2 -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 +164 -0
- package/.claude/workflows/pm-phase2.js +108 -0
- package/.claude/workflows/pm-phase3.js +309 -0
- package/CHANGELOG.md +29 -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 -672
- package/.claude/agents/project-manager.md +0 -309
|
@@ -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,164 @@
|
|
|
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
|
+
// Use only 'MISSING:' as the gap signal: 'gaps found' also matches 'ZERO GAPS FOUND'
|
|
117
|
+
const resultText = typeof valResult === 'string' ? valResult : JSON.stringify(valResult)
|
|
118
|
+
const hasGaps = resultText.includes('MISSING:')
|
|
119
|
+
|
|
120
|
+
if (!hasGaps) {
|
|
121
|
+
validationSummary = `0 gaps (clean on cycle ${cycle})`
|
|
122
|
+
log(`Validation clean on cycle ${cycle}`)
|
|
123
|
+
break
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
log(`Validation cycle ${cycle}: gaps found`)
|
|
127
|
+
|
|
128
|
+
if (cycle < MAX_CYCLES) {
|
|
129
|
+
// Re-run failing docs before next validation cycle
|
|
130
|
+
if (resultText.includes('Requirements')) {
|
|
131
|
+
await agent(featurePath, { agentType: 'generate-requirements', label: `generate-requirements (revision ${cycle})`, phase: 'Validation' })
|
|
132
|
+
}
|
|
133
|
+
if (resultText.includes('Tech-Spec')) {
|
|
134
|
+
await agent(featurePath, { agentType: 'generate-tech-spec', label: `generate-tech-spec (revision ${cycle})`, phase: 'Validation' })
|
|
135
|
+
}
|
|
136
|
+
} else {
|
|
137
|
+
validationSummary = 'gaps remain after 3 cycles'
|
|
138
|
+
errors.push('validate-feature-docs: gaps remain after 3 revision cycles')
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// ── Build gate1_payload ───────────────────────────────────────────────────────
|
|
143
|
+
const gate1Payload = {
|
|
144
|
+
prefix,
|
|
145
|
+
feature_dir,
|
|
146
|
+
feature_path: featurePath,
|
|
147
|
+
requirements: {
|
|
148
|
+
path: `${feature_dir}/${prefix}-Requirements.md`,
|
|
149
|
+
summary: `${prefix}-Requirements.md — generated`,
|
|
150
|
+
},
|
|
151
|
+
tech_spec: {
|
|
152
|
+
path: `${feature_dir}/${prefix}-Tech-Spec.md`,
|
|
153
|
+
summary: `${prefix}-Tech-Spec.md — generated`,
|
|
154
|
+
},
|
|
155
|
+
validation: {
|
|
156
|
+
path: `${feature_dir}/${prefix}-Validation-Report.md`,
|
|
157
|
+
summary: validationSummary,
|
|
158
|
+
},
|
|
159
|
+
token_ledger: tokenLedger,
|
|
160
|
+
errors,
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
log(`pm-phase1 complete — gate1_payload ready`)
|
|
164
|
+
return gate1Payload
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
export const meta = {
|
|
2
|
+
name: 'pm-phase2',
|
|
3
|
+
description: 'Feature delivery phase 2: generate-work-breakdown → Effort-Estimate → gate2_payload. Precondition: Gate 1 approved.',
|
|
4
|
+
phases: [
|
|
5
|
+
{ title: 'Work Breakdown', detail: 'Run generate-work-breakdown' },
|
|
6
|
+
{ title: 'Effort Estimate', detail: 'Parse WB metrics, write Effort-Estimate.md' },
|
|
7
|
+
],
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
// args: "<path-to-feature.md>"
|
|
11
|
+
const featurePath = (typeof args === 'string' ? args : '').trim().split(/\s+/)[0]
|
|
12
|
+
|
|
13
|
+
// ── generate-work-breakdown ───────────────────────────────────────────────────
|
|
14
|
+
phase('Work Breakdown')
|
|
15
|
+
|
|
16
|
+
const tokenLedger = []
|
|
17
|
+
|
|
18
|
+
log(`Running generate-work-breakdown for ${featurePath}`)
|
|
19
|
+
const beforeWB = budget.spent()
|
|
20
|
+
await agent(featurePath, {
|
|
21
|
+
agentType: 'generate-work-breakdown',
|
|
22
|
+
label: 'generate-work-breakdown',
|
|
23
|
+
phase: 'Work Breakdown',
|
|
24
|
+
})
|
|
25
|
+
const wbTokens = budget.spent() - beforeWB
|
|
26
|
+
tokenLedger.push({ agent: 'generate-work-breakdown', model: 'haiku', phase_delta_tokens: wbTokens })
|
|
27
|
+
log(`generate-work-breakdown done — phase delta: ${wbTokens} tokens`)
|
|
28
|
+
|
|
29
|
+
// ── Parse WB + write Effort-Estimate ─────────────────────────────────────────
|
|
30
|
+
phase('Effort Estimate')
|
|
31
|
+
|
|
32
|
+
const PARSE_SCHEMA = {
|
|
33
|
+
type: 'object',
|
|
34
|
+
properties: {
|
|
35
|
+
prefix: { type: 'string' },
|
|
36
|
+
feature_dir: { type: 'string' },
|
|
37
|
+
user_stories: { type: 'number' },
|
|
38
|
+
total_tasks: { type: 'number' },
|
|
39
|
+
domain_breakdown: { type: 'string' },
|
|
40
|
+
implementation_phases: { type: 'number' },
|
|
41
|
+
human_estimate: { type: 'string' },
|
|
42
|
+
agent_estimate: { type: 'string' },
|
|
43
|
+
work_breakdown_path: { type: 'string' },
|
|
44
|
+
effort_estimate_path: { type: 'string' },
|
|
45
|
+
},
|
|
46
|
+
required: ['prefix', 'user_stories', 'total_tasks', 'work_breakdown_path'],
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const metrics = await agent(
|
|
50
|
+
`You have two tasks:
|
|
51
|
+
|
|
52
|
+
TASK 1 — Read the Work Breakdown file.
|
|
53
|
+
The feature.md is at: ${featurePath}
|
|
54
|
+
The directory containing feature.md also contains the Work Breakdown file named {PREFIX}-Work-Breakdown.md,
|
|
55
|
+
where PREFIX matches the pattern [A-Z]+-[0-9]+ from the directory name (e.g. FTR-009).
|
|
56
|
+
Use Glob or Read to find and read that file.
|
|
57
|
+
|
|
58
|
+
Extract from the Summary table:
|
|
59
|
+
- prefix (e.g. "FTR-009")
|
|
60
|
+
- feature_dir (directory containing feature.md)
|
|
61
|
+
- user_stories (integer)
|
|
62
|
+
- total_tasks (integer)
|
|
63
|
+
- domain_breakdown (e.g. "DB:0, BE:11, FE:0, INFRA:4, TEST:0")
|
|
64
|
+
- implementation_phases (integer)
|
|
65
|
+
- human_estimate (e.g. "~34h")
|
|
66
|
+
- agent_estimate (e.g. "~2h 6min")
|
|
67
|
+
- work_breakdown_path (full path to the Work Breakdown file)
|
|
68
|
+
|
|
69
|
+
TASK 2 — Write the Effort-Estimate.md file.
|
|
70
|
+
Write {feature_dir}/{PREFIX}-Effort-Estimate.md with the extracted metrics in this format:
|
|
71
|
+
|
|
72
|
+
# Effort Estimate — {PREFIX} — {Feature Title}
|
|
73
|
+
|
|
74
|
+
| Metric | Value |
|
|
75
|
+
|--------|-------|
|
|
76
|
+
| User Stories | {user_stories} |
|
|
77
|
+
| Total tasks | {total_tasks} ({domain_breakdown}) |
|
|
78
|
+
| Implementation phases | {implementation_phases} |
|
|
79
|
+
| Human estimate | {human_estimate} (sequential, no parallelism) |
|
|
80
|
+
| Agent estimate | {agent_estimate} (parallel dispatch, critical path only) |
|
|
81
|
+
|
|
82
|
+
Set effort_estimate_path to the full path of the written file.
|
|
83
|
+
|
|
84
|
+
Return the extracted metrics as structured output.`,
|
|
85
|
+
{
|
|
86
|
+
label: 'parse-wb-write-effort-estimate',
|
|
87
|
+
phase: 'Effort Estimate',
|
|
88
|
+
schema: PARSE_SCHEMA,
|
|
89
|
+
}
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
log(`WB parsed: ${metrics.user_stories} US, ${metrics.total_tasks} tasks, ${metrics.implementation_phases} phases`)
|
|
93
|
+
log(`Effort-Estimate written: ${metrics.effort_estimate_path}`)
|
|
94
|
+
|
|
95
|
+
return {
|
|
96
|
+
prefix: metrics.prefix,
|
|
97
|
+
feature_path: featurePath,
|
|
98
|
+
user_stories: metrics.user_stories,
|
|
99
|
+
total_tasks: metrics.total_tasks,
|
|
100
|
+
domain_breakdown: metrics.domain_breakdown || 'N/A',
|
|
101
|
+
implementation_phases: metrics.implementation_phases,
|
|
102
|
+
human_estimate: metrics.human_estimate || 'N/A',
|
|
103
|
+
agent_estimate: metrics.agent_estimate || 'N/A',
|
|
104
|
+
work_breakdown_path: metrics.work_breakdown_path,
|
|
105
|
+
effort_estimate_path: metrics.effort_estimate_path || '',
|
|
106
|
+
token_ledger: tokenLedger,
|
|
107
|
+
errors: [],
|
|
108
|
+
}
|