@dtlabs/ai-toolkit 0.5.0 → 0.7.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 +103 -16
- package/.claude/agents/developer-testing.md +4 -4
- package/.claude/agents/validate-feature-docs.md +5 -2
- package/.claude/skills/define-feature/SKILL.md +6 -4
- package/.claude/workflows/pm-phase1.js +52 -0
- package/.claude/workflows/pm-phase3.js +276 -127
- package/CHANGELOG.md +22 -0
- package/README.md +1 -1
- package/package.json +1 -1
|
@@ -6,7 +6,75 @@ model: sonnet
|
|
|
6
6
|
|
|
7
7
|
# Define Feature
|
|
8
8
|
|
|
9
|
-
Helps the user define a new feature by asking
|
|
9
|
+
Helps the user define a new feature by first **reading everything the repository already knows**, then asking only the questions whose answers are genuinely missing — and finally synthesizing the result into a `feature.md` file ready for the `/implement-feature` pipeline.
|
|
10
|
+
|
|
11
|
+
**Core principle: never ask what the repo can already answer.** Every question you ask must be one you could not resolve by reading `AGENTS.md`, an existing backlog/ticket, the existing feature docs, or the code itself. Redundant questions are a defect.
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## Input
|
|
16
|
+
|
|
17
|
+
The user MAY provide a **source reference** as an argument — treat it as authoritative input, not as a starting point to re-derive from scratch:
|
|
18
|
+
|
|
19
|
+
- A path to a backlog entry or section (e.g. `docs/token-optimization-backlog.md → OPT-09`)
|
|
20
|
+
- A path to a ticket, spec, or design note
|
|
21
|
+
- A free-text description of the feature
|
|
22
|
+
- Nothing at all (fully interactive definition)
|
|
23
|
+
|
|
24
|
+
If a source reference is given, your job is to **ingest it, pre-fill the feature draft, and grill only on the real gaps** — not to ask the user to restate what the source already says.
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## Phase 0 — Context Ingestion (MANDATORY — before any question)
|
|
29
|
+
|
|
30
|
+
Do NOT ask the user anything until you have read the available context. Silently gather, then build a pre-filled draft.
|
|
31
|
+
|
|
32
|
+
### 0a. Read project conventions
|
|
33
|
+
|
|
34
|
+
- Read `AGENTS.md` from the project root if it exists (tech stack, directory structure, patterns, constraints). This answers most stack/structure/permission questions without asking.
|
|
35
|
+
- If `AGENTS.md` is absent, note it — you will infer conventions from the code and may confirm the stack in one consolidated question.
|
|
36
|
+
|
|
37
|
+
### 0b. Ingest the source reference (if provided)
|
|
38
|
+
|
|
39
|
+
- Read the referenced file/section fully. Extract every field it already provides: objective, scope, out-of-scope, affected files, acceptance criteria, dependencies, effort/risk.
|
|
40
|
+
- Follow obvious pointers (e.g. a backlog entry that says "deriva da `docs/token-optimization.md`" → read that too).
|
|
41
|
+
|
|
42
|
+
### 0c. Study existing features and code
|
|
43
|
+
|
|
44
|
+
- List `docs/features/` and `internal_docs/features/` (whichever exists) to learn the house style of prior `feature.md` files and to detect overlap/dependencies with existing work.
|
|
45
|
+
- Read the code the feature will touch (files named in the source, or found via Grep/Glob) enough to understand the current behavior being changed.
|
|
46
|
+
|
|
47
|
+
### 0d. Classify the feature type — this drives which questions are asked
|
|
48
|
+
|
|
49
|
+
Decide the primary type from the evidence gathered:
|
|
50
|
+
|
|
51
|
+
| Type | Signals | Rounds that DON'T apply |
|
|
52
|
+
|------|---------|-------------------------|
|
|
53
|
+
| **business/UI** | end-user pages, forms, actors, CRUD on domain entities | (all rounds apply) |
|
|
54
|
+
| **internal/technical** | refactor, tooling, script, workflow, agent, build/infra, optimization | Actors, Core-flow-as-page, Data model, Roles/permissions |
|
|
55
|
+
| **library/API** | public functions/endpoints, no UI, consumed by other code | Core-flow-as-page, Roles/permissions (unless auth is the point) |
|
|
56
|
+
|
|
57
|
+
Record the type. In Phase 2 you will **skip inapplicable rounds entirely** and mark their template sections `N/A — <type> feature`.
|
|
58
|
+
|
|
59
|
+
### 0e. Build and present the pre-filled draft
|
|
60
|
+
|
|
61
|
+
Assemble a draft of the feature fields from 0a–0c and show it to the user **before grilling**:
|
|
62
|
+
|
|
63
|
+
```
|
|
64
|
+
📋 Pre-filled from: <source reference or "no source — interactive">
|
|
65
|
+
Feature type: <business/UI | internal/technical | library/API>
|
|
66
|
+
──────────────────────────────────────────────────
|
|
67
|
+
Objective : <from source, or "❓ unknown">
|
|
68
|
+
Scope : <from source, or "❓ unknown">
|
|
69
|
+
Out of scope : <from source, or "❓ unknown">
|
|
70
|
+
Files affected : <from source/code, or "❓ unknown">
|
|
71
|
+
Acceptance : <from source, or "❓ unknown">
|
|
72
|
+
Dependencies : <from source, or "❓ unknown">
|
|
73
|
+
──────────────────────────────────────────────────
|
|
74
|
+
Gaps I still need to resolve: <list only the ❓ items + genuine ambiguities>
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Ask the user to **confirm or correct the draft**. Then grill ONLY on the `❓` gaps and ambiguities — skip every field already answered by the source.
|
|
10
78
|
|
|
11
79
|
---
|
|
12
80
|
|
|
@@ -30,11 +98,16 @@ Example: "Supplier Onboarding" → `FTR-003-supplier-onboarding`
|
|
|
30
98
|
|
|
31
99
|
---
|
|
32
100
|
|
|
33
|
-
## Phase 2 — Grilling
|
|
101
|
+
## Phase 2 — Grilling (only the gaps)
|
|
102
|
+
|
|
103
|
+
**Objective**: resolve the `❓` gaps and ambiguities identified in Phase 0 — nothing already answered.
|
|
34
104
|
|
|
35
|
-
**
|
|
105
|
+
**Hard rules before asking any question:**
|
|
106
|
+
1. **Never ask what Phase 0 already answered.** If the source, `AGENTS.md`, existing features, or the code already provides a field, do NOT ask about it — at most ask a single confirmation if genuinely ambiguous.
|
|
107
|
+
2. **Skip rounds that don't apply to the feature type** (from Phase 0d). For an internal/technical feature, skip Actors, page-based Core Flow, Data model, and Roles/permissions entirely and mark those template sections `N/A — technical feature`.
|
|
108
|
+
3. **One dimension at a time** — use `AskUserQuestion` per round; adapt follow-ups to prior answers.
|
|
36
109
|
|
|
37
|
-
|
|
110
|
+
Below are the possible rounds. Run a round **only if** it applies to the feature type AND it contains at least one unresolved gap. If a round has no open gap, skip it silently.
|
|
38
111
|
|
|
39
112
|
### Round 1 — The problem
|
|
40
113
|
|
|
@@ -49,15 +122,21 @@ Follow-up if vague:
|
|
|
49
122
|
|
|
50
123
|
### Round 2 — The core flow
|
|
51
124
|
|
|
52
|
-
|
|
125
|
+
**Applies to:** all types — but phrase it to the type.
|
|
126
|
+
- business/UI: *"Walk me through the main scenario step by step — from the moment the user opens the page to the moment they're done."*
|
|
127
|
+
- internal/technical or library/API: *"Walk me through the main execution path — what triggers it, what it does step by step, and what the end state is."* (There is no page; do not ask page questions.)
|
|
53
128
|
|
|
54
|
-
Goal: extract the happy path in concrete
|
|
129
|
+
Goal: extract the happy path in concrete steps.
|
|
55
130
|
|
|
56
|
-
Follow-up:
|
|
131
|
+
Follow-up (business/UI):
|
|
57
132
|
- "What does the user see first?"
|
|
58
133
|
- "What data do they need to input or select?"
|
|
59
134
|
- "What happens after they submit/save?"
|
|
60
|
-
|
|
135
|
+
|
|
136
|
+
Follow-up (technical/API):
|
|
137
|
+
- "What is the entry point (function, script, command, event)?"
|
|
138
|
+
- "What are the inputs and where do they come from?"
|
|
139
|
+
- "What is the observable output or side effect when it completes?"
|
|
61
140
|
|
|
62
141
|
### Round 3 — Boundaries
|
|
63
142
|
|
|
@@ -81,6 +160,8 @@ Follow-up:
|
|
|
81
160
|
|
|
82
161
|
### Round 5 — Data and entities
|
|
83
162
|
|
|
163
|
+
**Applies to:** business/UI and library/API features that manage domain data. **Skip for internal/technical/refactor/tooling features that create no domain entities** — mark the Data Model section `N/A — technical feature`.
|
|
164
|
+
|
|
84
165
|
> "What data does this feature create, read, update, or delete?"
|
|
85
166
|
|
|
86
167
|
Follow-up:
|
|
@@ -90,6 +171,8 @@ Follow-up:
|
|
|
90
171
|
|
|
91
172
|
### Round 6 — Roles and permissions
|
|
92
173
|
|
|
174
|
+
**Applies to:** business/UI features (and API features where authorization is in scope). **Skip for internal/technical features with no end-user roles** — mark the Roles and Permissions section `N/A — technical feature`.
|
|
175
|
+
|
|
93
176
|
> "Who can do what? Are there different permission levels within this feature?"
|
|
94
177
|
|
|
95
178
|
Follow-up:
|
|
@@ -226,9 +309,10 @@ For each entity: name, key fields (name, type, constraints), relationships.
|
|
|
226
309
|
### Writing rules
|
|
227
310
|
|
|
228
311
|
- Write in **English** (regardless of the language used during grilling)
|
|
229
|
-
- Derive content **
|
|
312
|
+
- Derive content from **the source reference (Phase 0) plus the user's answers** — do not invent requirements
|
|
313
|
+
- For sections that don't apply to the feature type, write `N/A — <type> feature` instead of leaving them blank or inventing content (e.g. Actors / Data Model / Roles for an internal/technical feature)
|
|
230
314
|
- Mark open questions explicitly rather than guessing
|
|
231
|
-
- Acceptance criteria must be **testable** (Given/When/Then)
|
|
315
|
+
- Acceptance criteria must be **testable** (Given/When/Then) — for technical features, express them as observable/verifiable checks (e.g. "grep returns nothing", "build passes", "output file exists")
|
|
232
316
|
- Be concrete and specific — avoid vague statements like "the system should be fast"
|
|
233
317
|
|
|
234
318
|
---
|
|
@@ -258,9 +342,12 @@ Next step: run /implement-feature docs/features/{PREFIX}-{slug}/feature.md
|
|
|
258
342
|
|
|
259
343
|
## Guidelines
|
|
260
344
|
|
|
261
|
-
- **
|
|
262
|
-
- **
|
|
263
|
-
- **
|
|
264
|
-
- **
|
|
265
|
-
- **
|
|
266
|
-
- **
|
|
345
|
+
- **Read before you ask** — Phase 0 is mandatory; never ask what `AGENTS.md`, the source reference, existing feature docs, or the code already answers. A redundant question is a defect.
|
|
346
|
+
- **Match questions to the feature type** — skip Actors / Data model / Roles / page-flow rounds for internal/technical features; do not force business-feature questions onto a refactor or tooling task.
|
|
347
|
+
- **Confirm the draft, then grill the gaps** — present the pre-filled draft first; only unresolved `❓` items and genuine ambiguities become questions.
|
|
348
|
+
- **Grill relentlessly on real gaps** — vague answers to open questions get follow-ups, not acceptance.
|
|
349
|
+
- **One dimension at a time** — do not overwhelm the user with all questions at once.
|
|
350
|
+
- **Challenge contradictions** — if answers conflict with each other or with the source, surface it before writing.
|
|
351
|
+
- **Do not invent** — if something is unclear and the user can't answer, record it as an open question.
|
|
352
|
+
- **Create the folder** if it doesn't exist — do not ask the user to do it manually.
|
|
353
|
+
- **Always write in English** — feature.md is input to English-language downstream agents.
|
|
@@ -78,7 +78,7 @@ Follow all conventions EXACTLY.
|
|
|
78
78
|
4. **Create test project/file** if it doesn't exist (following project conventions)
|
|
79
79
|
5. **Write test helpers** (custom factories, builders, mocks) if needed
|
|
80
80
|
6. **Implement tests** following AAA pattern
|
|
81
|
-
7. **
|
|
81
|
+
7. **Syntax check only (optional)** — you MAY run a build to confirm the test files compile (e.g. `dotnet build`, `npx tsc --noEmit`). **Do NOT run the test suite** (`dotnet test`, `npm test`, etc.) — the orchestrator runs the full suite once, after all agents finish. Running it here duplicates a multi-minute build per agent and contends for build artifacts.
|
|
82
82
|
|
|
83
83
|
---
|
|
84
84
|
|
|
@@ -102,13 +102,13 @@ When all assigned tasks are complete, return a **structured summary** to the orc
|
|
|
102
102
|
Tasks completed: [list]
|
|
103
103
|
Files created: [list with path only]
|
|
104
104
|
Files modified: [list with path only]
|
|
105
|
-
|
|
105
|
+
Build check: ✅ compiles / ❌ compile error — [first error line if any] / ⏭️ not run
|
|
106
106
|
Notes: [blocking issues, skipped cases, TODO items]
|
|
107
107
|
─────────────────────────────────────────────
|
|
108
108
|
```
|
|
109
109
|
|
|
110
110
|
- **Never dump test code** into the summary
|
|
111
|
-
-
|
|
111
|
+
- Do NOT report test-run results — you do not run the suite; the orchestrator runs it once after all agents finish. Report only whether the test files compile.
|
|
112
112
|
|
|
113
113
|
---
|
|
114
114
|
|
|
@@ -119,4 +119,4 @@ Notes: [blocking issues, skipped cases, TODO items]
|
|
|
119
119
|
- **No testing of trivial code** — getters/setters, data classes, config files
|
|
120
120
|
- **Mock external dependencies** — databases, HTTP clients, file systems
|
|
121
121
|
- **Use the project's preferred mocking approach** — as defined in AGENTS.md
|
|
122
|
-
- **
|
|
122
|
+
- **Do NOT run the test suite** — the orchestrator runs the full suite once, after every agent has finished writing its files. A build for a quick syntax check is fine; running the tests here wastes minutes on a duplicate build and can lock build artifacts.
|
|
@@ -121,9 +121,11 @@ For each document with gaps, invoke the corresponding agent with a **targeted re
|
|
|
121
121
|
|
|
122
122
|
---
|
|
123
123
|
|
|
124
|
-
## Phase 6 — Write Validation Report
|
|
124
|
+
## Phase 6 — Write Validation Report (MANDATORY — your final action)
|
|
125
125
|
|
|
126
|
-
**
|
|
126
|
+
**This is not optional and not the same as the Phase 4 gap report.** The coverage report you produced in Phase 4 is on-screen text; it is NOT the deliverable. Your job is not complete until the file `{PREFIX}-Validation-Report.md` exists **on disk**. You MUST call the `Write` tool to create it — even when validation is clean and zero gaps were found. Returning a summary without having called `Write` is a failure.
|
|
127
|
+
|
|
128
|
+
Execute this phase **last, always**, regardless of outcome. Downstream agents read this file as a hard precondition; if it is missing, the entire pipeline aborts.
|
|
127
129
|
|
|
128
130
|
Write `{PREFIX}-Validation-Report.md` in the same directory:
|
|
129
131
|
|
|
@@ -166,6 +168,7 @@ Do NOT guess how to fill ambiguous gaps. Ask first, then revise only with a clea
|
|
|
166
168
|
|
|
167
169
|
## Guidelines
|
|
168
170
|
|
|
171
|
+
- **Always call `Write` for the Validation Report before finishing** (Phase 6) — the on-screen gap report is not the deliverable; the file on disk is. This is your last action, clean or not.
|
|
169
172
|
- **Do not regenerate documents from scratch** — targeted revisions only
|
|
170
173
|
- **Be specific about gaps** — point to the exact section, claim, and what is missing
|
|
171
174
|
- **Stop after 3 revision cycles** — if coverage is still incomplete, report remaining gaps and exit
|
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
---
|
|
2
|
-
description: "Define Feature —
|
|
2
|
+
description: "Define Feature — reads existing repo context, then grills only on the real gaps and writes feature.md in docs/features/FTR-XXX-slug/. Usage: /define-feature [source reference]"
|
|
3
3
|
disable-model-invocation: true
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
Spawn the `define-feature` agent
|
|
6
|
+
Spawn the `define-feature` agent, passing through whatever the user provided as a source reference (a backlog entry, ticket path, spec path, or free-text description). If the user gave nothing, pass an empty source.
|
|
7
7
|
|
|
8
8
|
```
|
|
9
9
|
subagent_type: define-feature
|
|
10
|
-
prompt:
|
|
10
|
+
prompt: |
|
|
11
|
+
Start the feature definition session.
|
|
12
|
+
Source reference (may be empty): {{user arguments verbatim}}
|
|
11
13
|
```
|
|
12
14
|
|
|
13
|
-
|
|
15
|
+
The agent runs Phase 0 (context ingestion) first — it reads AGENTS.md, the source reference, existing feature docs, and the relevant code, then pre-fills a draft and grills ONLY on the genuine gaps. Do not add instructions telling it to skip discovery.
|
|
@@ -103,6 +103,7 @@ if (discoveryResult.needs_tech_spec) {
|
|
|
103
103
|
phase('Validation')
|
|
104
104
|
|
|
105
105
|
let validationSummary = 'skipped'
|
|
106
|
+
let lastValText = ''
|
|
106
107
|
const MAX_CYCLES = 3
|
|
107
108
|
|
|
108
109
|
for (let cycle = 1; cycle <= MAX_CYCLES; cycle++) {
|
|
@@ -115,6 +116,7 @@ for (let cycle = 1; cycle <= MAX_CYCLES; cycle++) {
|
|
|
115
116
|
// valResult is the agent's text output — check for gap indicators
|
|
116
117
|
// Use only 'MISSING:' as the gap signal: 'gaps found' also matches 'ZERO GAPS FOUND'
|
|
117
118
|
const resultText = typeof valResult === 'string' ? valResult : JSON.stringify(valResult)
|
|
119
|
+
lastValText = resultText
|
|
118
120
|
const hasGaps = resultText.includes('MISSING:')
|
|
119
121
|
|
|
120
122
|
if (!hasGaps) {
|
|
@@ -139,6 +141,56 @@ for (let cycle = 1; cycle <= MAX_CYCLES; cycle++) {
|
|
|
139
141
|
}
|
|
140
142
|
}
|
|
141
143
|
|
|
144
|
+
// ── Guarantee the Validation Report exists on disk ───────────────────────────
|
|
145
|
+
// The validate-feature-docs agent (haiku) is instructed to write the report in its
|
|
146
|
+
// Phase 6, but this is LLM-dependent and unreliable when validation is clean. Downstream
|
|
147
|
+
// pm-phase2 (generate-work-breakdown) treats the report as a hard precondition, so its
|
|
148
|
+
// absence aborts the whole pipeline. This step deterministically ensures the file exists.
|
|
149
|
+
if (validationSummary !== 'skipped') {
|
|
150
|
+
const validationReportPath = `${feature_dir}/${prefix}-Validation-Report.md`
|
|
151
|
+
await agent(
|
|
152
|
+
`Ensure the Validation Report file exists on disk for this feature delivery run.
|
|
153
|
+
|
|
154
|
+
File path: ${validationReportPath}
|
|
155
|
+
|
|
156
|
+
Steps:
|
|
157
|
+
1. Check whether the file already exists (use Read or Glob).
|
|
158
|
+
2. If it ALREADY EXISTS: do nothing, return "exists".
|
|
159
|
+
3. If it DOES NOT EXIST: write it now using the Write tool, based on the validation outcome below.
|
|
160
|
+
Get the current date via Bash: run \`date -u +"%Y-%m-%d"\`.
|
|
161
|
+
|
|
162
|
+
Validation outcome summary: ${validationSummary}
|
|
163
|
+
|
|
164
|
+
Validation agent output (source of truth for gaps found/resolved):
|
|
165
|
+
──────────────────────────────────────────────────
|
|
166
|
+
${lastValText.slice(0, 4000)}
|
|
167
|
+
──────────────────────────────────────────────────
|
|
168
|
+
|
|
169
|
+
File format to write (fill from the outcome above; if the run was clean, mark both documents Clean with 0 gaps):
|
|
170
|
+
|
|
171
|
+
# Validation Report — ${prefix}
|
|
172
|
+
|
|
173
|
+
## Summary
|
|
174
|
+
| Document | Gaps found | Gaps resolved | Status |
|
|
175
|
+
|----------|-----------|--------------|--------|
|
|
176
|
+
| ${prefix}-Requirements.md | N | N | Clean / Gaps remain |
|
|
177
|
+
| ${prefix}-Tech-Spec.md | N | N | Clean / Gaps remain |
|
|
178
|
+
|
|
179
|
+
## Gaps found and resolved
|
|
180
|
+
(derive from the validation output above; write "(none)" if clean)
|
|
181
|
+
|
|
182
|
+
## Remaining gaps (if any)
|
|
183
|
+
(list any unresolved gaps, or "(none)")
|
|
184
|
+
|
|
185
|
+
## Validation date
|
|
186
|
+
{date from Bash}
|
|
187
|
+
|
|
188
|
+
Return "written" if you created the file, or "exists" if it was already present.`,
|
|
189
|
+
{ label: 'ensure-validation-report', phase: 'Validation' }
|
|
190
|
+
)
|
|
191
|
+
log(`Validation report ensured at ${validationReportPath}`)
|
|
192
|
+
}
|
|
193
|
+
|
|
142
194
|
// ── Write process-log (phase 1 snapshot) ─────────────────────────────────────
|
|
143
195
|
const reqEntry = tokenLedger.find(e => e.agent === 'generate-requirements')
|
|
144
196
|
const specEntry = tokenLedger.find(e => e.agent === 'generate-tech-spec')
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
export const meta = {
|
|
2
2
|
name: 'pm-phase3',
|
|
3
|
-
description: 'Feature delivery phase 3: parse WB → dispatch specialist agents per US (commit per
|
|
3
|
+
description: 'Feature delivery phase 3: parse WB → dispatch specialist agents per US (developer-backend/frontend/testing + review-solution per US, max 2 rework cycles, git commit per US) → remediation → PR → Token-Estimate actuals → Effort-Estimate actuals.',
|
|
4
4
|
phases: [
|
|
5
|
-
{ title: 'Parse', detail: 'Parse Work Breakdown
|
|
6
|
-
{ title: 'Implementation', detail: 'Dispatch developer agents per
|
|
7
|
-
{ title: '
|
|
5
|
+
{ title: 'Parse', detail: 'Parse Work Breakdown into structured implementation phases' },
|
|
6
|
+
{ title: 'Implementation', detail: 'Dispatch developer agents per US, review, commit' },
|
|
7
|
+
{ title: 'Test', detail: 'Single test run after all phases complete' },
|
|
8
|
+
{ title: 'Remediation', detail: 'Fix OPEN issues from Issues Register' },
|
|
8
9
|
{ title: 'PR', detail: 'Push branch, create PR, update registry' },
|
|
9
10
|
{ title: 'Actuals', detail: 'Write Token-Estimate and Effort-Estimate actuals' },
|
|
10
11
|
],
|
|
@@ -17,10 +18,12 @@ const branchMatch = argStr.match(/--branch\s+(\S+)/)
|
|
|
17
18
|
const branch = branchMatch ? branchMatch[1] : null
|
|
18
19
|
|
|
19
20
|
const tokenLedger = []
|
|
21
|
+
const escalations = []
|
|
20
22
|
|
|
21
23
|
// ── Parse Work Breakdown CSV (deterministic, no AI) ───────────────────────────
|
|
22
24
|
phase('Parse')
|
|
23
25
|
|
|
26
|
+
// Derive feature_dir and prefix from featurePath (e.g. "docs/features/FTR-004-.../feature.md")
|
|
24
27
|
const featureDir = featurePath.replace(/\/[^/]+$/, '')
|
|
25
28
|
const prefixMatch = featureDir.match(/([A-Z]+-\d+)/)
|
|
26
29
|
const prefix = prefixMatch ? prefixMatch[1] : 'FTR-000'
|
|
@@ -37,7 +40,7 @@ const csvContent = await agent(
|
|
|
37
40
|
const rows = csvContent
|
|
38
41
|
.split('\n')
|
|
39
42
|
.map(l => l.trim())
|
|
40
|
-
.filter(l => l && !l.startsWith('phase_id'))
|
|
43
|
+
.filter(l => l && !l.startsWith('phase_id')) // skip header and empty lines
|
|
41
44
|
.map(l => {
|
|
42
45
|
const [phase_id, phase_title, commit_message, depends_on, task_id, task_title, domain, agent_type] = l.split('|')
|
|
43
46
|
return { phase_id, phase_title, commit_message, depends_on: depends_on || '', task_id, task_title, domain, agent_type }
|
|
@@ -52,7 +55,7 @@ for (const row of rows) {
|
|
|
52
55
|
phase_id: row.phase_id,
|
|
53
56
|
title: row.phase_title,
|
|
54
57
|
commit_message: row.commit_message,
|
|
55
|
-
depends_on: row.depends_on.split(' ').filter(Boolean),
|
|
58
|
+
depends_on: row.depends_on.split(' ').filter(Boolean), // [] for empty
|
|
56
59
|
impl_tasks: [],
|
|
57
60
|
test_tasks: [],
|
|
58
61
|
})
|
|
@@ -85,9 +88,11 @@ const allPhases = Array.from(phaseMap.values()).map(p => ({
|
|
|
85
88
|
}))
|
|
86
89
|
|
|
87
90
|
const wb = { prefix, feature_dir: featureDir, phases: allPhases }
|
|
91
|
+
|
|
88
92
|
log(`CSV parsed: ${rows.length} tasks → ${wb.phases.length} phases for ${wb.prefix}`)
|
|
89
93
|
|
|
90
|
-
// Build execution waves: phases whose dependencies are all satisfied run in parallel
|
|
94
|
+
// Build execution waves: phases whose dependencies are all satisfied can run in parallel.
|
|
95
|
+
// Wave 0 = no deps, Wave 1 = depends only on Wave 0 phases, etc.
|
|
91
96
|
const buildWaves = (phases) => {
|
|
92
97
|
const done = new Set()
|
|
93
98
|
const waves = []
|
|
@@ -95,6 +100,7 @@ const buildWaves = (phases) => {
|
|
|
95
100
|
while (remaining.length > 0) {
|
|
96
101
|
const ready = remaining.filter(p => p.depends_on.every(d => done.has(d)))
|
|
97
102
|
if (ready.length === 0) {
|
|
103
|
+
// Circular or unresolvable deps — run the rest sequentially to avoid deadlock
|
|
98
104
|
waves.push(remaining)
|
|
99
105
|
break
|
|
100
106
|
}
|
|
@@ -108,71 +114,140 @@ const buildWaves = (phases) => {
|
|
|
108
114
|
const waves = buildWaves(wb.phases)
|
|
109
115
|
log(`Execution plan: ${waves.length} wave(s) — ${waves.map((w, i) => `wave${i+1}:[${w.map(p => p.phase_id).join(',')}]`).join(' ')}`)
|
|
110
116
|
|
|
111
|
-
let phasesDone
|
|
112
|
-
const usPassed
|
|
117
|
+
let phasesDone = 0
|
|
118
|
+
const usPassed = []
|
|
119
|
+
const usEscalated = []
|
|
120
|
+
let issuesPath = ''
|
|
121
|
+
let issuesOpen = 0
|
|
113
122
|
|
|
114
123
|
// ── Implementation ────────────────────────────────────────────────────────────
|
|
115
124
|
phase('Implementation')
|
|
116
125
|
|
|
117
|
-
|
|
118
|
-
|
|
126
|
+
const REVIEW_SCHEMA = {
|
|
127
|
+
type: 'object',
|
|
128
|
+
properties: {
|
|
129
|
+
has_critical: { type: 'boolean' },
|
|
130
|
+
findings: { type: 'string' },
|
|
131
|
+
issues_md: { type: 'string' },
|
|
132
|
+
},
|
|
133
|
+
required: ['has_critical'],
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// executePhase: runs impl_groups → test_groups → review → rework → commit for one phase
|
|
119
137
|
const executePhase = async (implPhase) => {
|
|
120
138
|
log(`Starting phase ${implPhase.phase_id}: ${implPhase.title}`)
|
|
121
139
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
)
|
|
134
|
-
|
|
135
|
-
|
|
140
|
+
let reworkCycle = 0
|
|
141
|
+
let passed = false
|
|
142
|
+
let lastFindings = ''
|
|
143
|
+
|
|
144
|
+
while (reworkCycle <= 2 && !passed) {
|
|
145
|
+
const reworkSuffix = reworkCycle > 0
|
|
146
|
+
? ` -- REWORK CYCLE ${reworkCycle}: address these critical findings:\n${lastFindings}`
|
|
147
|
+
: ''
|
|
148
|
+
|
|
149
|
+
// Step 1 — impl groups in parallel (INFRA / BE / FE)
|
|
150
|
+
if (implPhase.impl_groups.length > 0) {
|
|
151
|
+
const beforeImpl = budget.spent()
|
|
152
|
+
await parallel(implPhase.impl_groups.map(group => () =>
|
|
153
|
+
agent(
|
|
154
|
+
`${featurePath} ${group.task_ids.join(',')}${reworkSuffix}`,
|
|
155
|
+
{
|
|
156
|
+
agentType: group.agent_type,
|
|
157
|
+
label: `${group.agent_type}:${implPhase.phase_id}${reworkCycle > 0 ? ':rework' + reworkCycle : ''}`,
|
|
158
|
+
phase: 'Implementation',
|
|
159
|
+
}
|
|
160
|
+
)
|
|
161
|
+
))
|
|
162
|
+
const implTokens = budget.spent() - beforeImpl
|
|
163
|
+
tokenLedger.push({
|
|
164
|
+
agent: `${implPhase.impl_groups.map(g => g.agent_type).join('+')}:${implPhase.phase_id}${reworkCycle > 0 ? ':rework' + reworkCycle : ''}`,
|
|
165
|
+
model: 'sonnet',
|
|
166
|
+
phase_delta_tokens: implTokens,
|
|
167
|
+
})
|
|
168
|
+
log(`Phase ${implPhase.phase_id} impl groups done — ${implTokens} tokens`)
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Step 2 — test groups in parallel (TEST) — after impl groups
|
|
172
|
+
if (implPhase.test_groups.length > 0) {
|
|
173
|
+
const beforeTest = budget.spent()
|
|
174
|
+
await parallel(implPhase.test_groups.map(group => () =>
|
|
175
|
+
agent(
|
|
176
|
+
`${featurePath} ${group.task_ids.join(',')}${reworkSuffix}`,
|
|
177
|
+
{
|
|
178
|
+
agentType: group.agent_type,
|
|
179
|
+
label: `${group.agent_type}:${implPhase.phase_id}${reworkCycle > 0 ? ':rework' + reworkCycle : ''}`,
|
|
180
|
+
phase: 'Implementation',
|
|
181
|
+
}
|
|
182
|
+
)
|
|
183
|
+
))
|
|
184
|
+
const testTokens = budget.spent() - beforeTest
|
|
185
|
+
tokenLedger.push({
|
|
186
|
+
agent: `developer-testing:${implPhase.phase_id}${reworkCycle > 0 ? ':rework' + reworkCycle : ''}`,
|
|
187
|
+
model: 'sonnet',
|
|
188
|
+
phase_delta_tokens: testTokens,
|
|
189
|
+
})
|
|
190
|
+
log(`Phase ${implPhase.phase_id} test groups done — ${testTokens} tokens`)
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Step 3 — review-solution
|
|
194
|
+
const beforeReview = budget.spent()
|
|
195
|
+
const review = await agent(
|
|
196
|
+
`${featurePath} --scope ${implPhase.phase_id}`,
|
|
197
|
+
{
|
|
198
|
+
agentType: 'review-solution',
|
|
199
|
+
label: `review-solution:${implPhase.phase_id}`,
|
|
200
|
+
phase: 'Implementation',
|
|
201
|
+
schema: REVIEW_SCHEMA,
|
|
202
|
+
}
|
|
203
|
+
)
|
|
204
|
+
const reviewTokens = budget.spent() - beforeReview
|
|
136
205
|
tokenLedger.push({
|
|
137
|
-
agent:
|
|
138
|
-
model:
|
|
139
|
-
phase_delta_tokens:
|
|
206
|
+
agent: `review-solution:${implPhase.phase_id}`,
|
|
207
|
+
model: 'sonnet',
|
|
208
|
+
phase_delta_tokens: reviewTokens,
|
|
140
209
|
})
|
|
141
|
-
|
|
210
|
+
|
|
211
|
+
if (review && !review.has_critical) {
|
|
212
|
+
passed = true
|
|
213
|
+
if (review.issues_md) {
|
|
214
|
+
await agent(
|
|
215
|
+
`Append the following WARNING/INFO findings to ${wb.feature_dir}/${prefix}-Issues.md (create the file if it does not exist, with a header "# Issues Register — ${prefix}"):\n\n${review.issues_md}`,
|
|
216
|
+
{ label: `log-issues:${implPhase.phase_id}`, phase: 'Implementation' }
|
|
217
|
+
)
|
|
218
|
+
}
|
|
219
|
+
} else {
|
|
220
|
+
lastFindings = review?.findings || 'unspecified critical issues'
|
|
221
|
+
reworkCycle++
|
|
222
|
+
log(`Phase ${implPhase.phase_id}: critical findings — rework cycle ${reworkCycle}`)
|
|
223
|
+
}
|
|
142
224
|
}
|
|
143
225
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
))
|
|
157
|
-
const testTokens = budget.spent() - beforeTest
|
|
158
|
-
tokenLedger.push({
|
|
159
|
-
agent: `developer-testing:${implPhase.phase_id}`,
|
|
160
|
-
model: 'sonnet',
|
|
161
|
-
phase_delta_tokens: testTokens,
|
|
162
|
-
})
|
|
163
|
-
log(`Phase ${implPhase.phase_id} tests done — ${testTokens} tokens`)
|
|
226
|
+
if (!passed) {
|
|
227
|
+
log(`Phase ${implPhase.phase_id} ESCALATED after 2 rework cycles`)
|
|
228
|
+
escalations.push({ phaseId: implPhase.phase_id, reason: 'CRITICAL review findings unresolved after 2 rework cycles' })
|
|
229
|
+
usEscalated.push(implPhase.phase_id)
|
|
230
|
+
} else {
|
|
231
|
+
const commitMsg = implPhase.commit_message || `feat(${prefix}): implement ${implPhase.phase_id}`
|
|
232
|
+
await agent(
|
|
233
|
+
`Run these git commands in the repository root:\ngit add -A\ngit commit -m "${commitMsg}"\nIf there is nothing to commit, that is fine — just report success.`,
|
|
234
|
+
{ label: `commit:${implPhase.phase_id}`, phase: 'Implementation' }
|
|
235
|
+
)
|
|
236
|
+
if (implPhase.phase_id !== 'INFRA') usPassed.push(implPhase.phase_id)
|
|
237
|
+
log(`Phase ${implPhase.phase_id} passed review and committed`)
|
|
164
238
|
}
|
|
165
239
|
|
|
166
|
-
|
|
167
|
-
|
|
240
|
+
phasesDone++
|
|
241
|
+
|
|
242
|
+
// Persist the cumulative token ledger to disk after every completed phase.
|
|
243
|
+
// budget.spent() is an in-memory counter — if the workflow is stopped (manual
|
|
244
|
+
// stop, crash, timeout) before the Actuals phase, all token data would be lost.
|
|
245
|
+
// Writing the full ledger here means the file always reflects real consumption
|
|
246
|
+
// up to the last completed phase, recoverable manually or by a resumed run.
|
|
168
247
|
await agent(
|
|
169
|
-
`
|
|
170
|
-
{ label: `
|
|
248
|
+
`Write the following JSON to ${featureDir}/${prefix}-token-ledger.json (overwrite the file if it exists):\n${JSON.stringify(tokenLedger, null, 2)}`,
|
|
249
|
+
{ label: `persist-ledger:${implPhase.phase_id}`, phase: 'Implementation', model: 'haiku' }
|
|
171
250
|
)
|
|
172
|
-
|
|
173
|
-
if (implPhase.phase_id !== 'INFRA') usPassed.push(implPhase.phase_id)
|
|
174
|
-
phasesDone++
|
|
175
|
-
log(`Phase ${implPhase.phase_id} committed`)
|
|
176
251
|
}
|
|
177
252
|
|
|
178
253
|
// Execute waves: phases in the same wave run in parallel, waves are sequential
|
|
@@ -185,57 +260,91 @@ for (const wave of waves) {
|
|
|
185
260
|
}
|
|
186
261
|
}
|
|
187
262
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
// ── Review — single pass over the full feature diff ───────────────────────────
|
|
191
|
-
phase('Review')
|
|
192
|
-
|
|
193
|
-
const REVIEW_SCHEMA = {
|
|
263
|
+
// Count open issues
|
|
264
|
+
const ISSUES_SCHEMA = {
|
|
194
265
|
type: 'object',
|
|
195
266
|
properties: {
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
267
|
+
exists: { type: 'boolean' },
|
|
268
|
+
open_count: { type: 'number' },
|
|
269
|
+
path: { type: 'string' },
|
|
199
270
|
},
|
|
200
|
-
required: ['
|
|
271
|
+
required: ['exists', 'open_count'],
|
|
201
272
|
}
|
|
273
|
+
const issuesResult = await agent(
|
|
274
|
+
`Check if ${wb.feature_dir}/${prefix}-Issues.md exists. If it does, count rows with status OPEN. Return exists, open_count, and the full path.`,
|
|
275
|
+
{ label: 'count-open-issues', phase: 'Implementation', schema: ISSUES_SCHEMA }
|
|
276
|
+
)
|
|
277
|
+
issuesPath = issuesResult?.path || ''
|
|
278
|
+
issuesOpen = issuesResult?.open_count || 0
|
|
279
|
+
log(`Implementation complete — ${phasesDone} phases, ${usPassed.length} US passed, ${usEscalated.length} escalated, ${issuesOpen} open issues`)
|
|
280
|
+
|
|
281
|
+
// ── Final test run ────────────────────────────────────────────────────────────
|
|
282
|
+
// Developer agents no longer run the test suite themselves (they do a syntax-check
|
|
283
|
+
// build at most). Running the suite once here, after all files are written, replaces
|
|
284
|
+
// N parallel per-agent test runs — cutting ~1h of duplicated builds and artifact
|
|
285
|
+
// contention down to a single centralized run with one consolidated failure report.
|
|
286
|
+
phase('Test')
|
|
287
|
+
|
|
288
|
+
const beforeTest = budget.spent()
|
|
289
|
+
await agent(
|
|
290
|
+
`Run the full test suite in the repository root and report the result.
|
|
202
291
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
phase: 'Review',
|
|
210
|
-
schema: REVIEW_SCHEMA,
|
|
211
|
-
}
|
|
292
|
+
1. Read AGENTS.md in the repository root to find the exact test command for this project.
|
|
293
|
+
2. Run that command (e.g. \`dotnet test --no-restore --logger "console;verbosity=normal"\`, \`npm test\`).
|
|
294
|
+
3. Report the outcome: total passed/failed, and for any failures the test name and first error line.
|
|
295
|
+
|
|
296
|
+
Do NOT fix failing tests — only report them.`,
|
|
297
|
+
{ label: 'final-test-run', phase: 'Test', model: 'haiku' }
|
|
212
298
|
)
|
|
213
|
-
const
|
|
214
|
-
tokenLedger.push({ agent: '
|
|
299
|
+
const testTokens = budget.spent() - beforeTest
|
|
300
|
+
tokenLedger.push({ agent: 'final-test-run', model: 'haiku', phase_delta_tokens: testTokens })
|
|
301
|
+
log(`Test run complete — ${testTokens} tokens`)
|
|
215
302
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
let hasCritical = false
|
|
303
|
+
// ── Remediation ───────────────────────────────────────────────────────────────
|
|
304
|
+
phase('Remediation')
|
|
219
305
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
// Count OPEN issues from issues_md (rough count of "OPEN" occurrences)
|
|
228
|
-
issuesOpen = (review.issues_md.match(/\bOPEN\b/g) || []).length
|
|
229
|
-
log(`Review complete — has_critical: ${hasCritical}, open issues: ${issuesOpen}`)
|
|
230
|
-
} else {
|
|
231
|
-
log(`Review complete — no issues found`)
|
|
232
|
-
}
|
|
233
|
-
} else {
|
|
234
|
-
log(`Review agent returned null — skipping issues register`)
|
|
306
|
+
const REM_SCHEMA = {
|
|
307
|
+
type: 'object',
|
|
308
|
+
properties: {
|
|
309
|
+
issues_fixed: { type: 'number' },
|
|
310
|
+
issues_deferred: { type: 'number' },
|
|
311
|
+
},
|
|
312
|
+
required: ['issues_fixed', 'issues_deferred'],
|
|
235
313
|
}
|
|
236
314
|
|
|
237
|
-
|
|
238
|
-
|
|
315
|
+
let issuesFixed = 0
|
|
316
|
+
let issuesDeferred = 0
|
|
317
|
+
|
|
318
|
+
if (issuesOpen > 0 && issuesPath) {
|
|
319
|
+
log(`Remediation: ${issuesOpen} OPEN issues`)
|
|
320
|
+
const beforeRem = budget.spent()
|
|
321
|
+
|
|
322
|
+
const remResult = await agent(
|
|
323
|
+
`You are a remediation agent for the feature delivery pipeline.
|
|
324
|
+
|
|
325
|
+
Read the Issues Register at: ${issuesPath}
|
|
326
|
+
|
|
327
|
+
For each OPEN issue:
|
|
328
|
+
- If severity is INFO: mark it DEFERRED immediately (update Status column to DEFERRED). Do not dispatch a developer agent.
|
|
329
|
+
- If severity is WARNING: dispatch the appropriate developer agent (developer-backend for most cases) with the issue description. Then spawn review-solution to verify the fix. Max 1 retry per issue. If unresolved after 1 retry, mark DEFERRED.
|
|
330
|
+
|
|
331
|
+
After processing all issues, return: { "issues_fixed": N, "issues_deferred": N }
|
|
332
|
+
|
|
333
|
+
Update the Issues Register Status column in place for each resolved/deferred issue.`,
|
|
334
|
+
{
|
|
335
|
+
label: 'remediation',
|
|
336
|
+
phase: 'Remediation',
|
|
337
|
+
schema: REM_SCHEMA,
|
|
338
|
+
}
|
|
339
|
+
)
|
|
340
|
+
|
|
341
|
+
const remTokens = budget.spent() - beforeRem
|
|
342
|
+
tokenLedger.push({ agent: 'remediation', model: 'sonnet', phase_delta_tokens: remTokens })
|
|
343
|
+
issuesFixed = remResult.issues_fixed || 0
|
|
344
|
+
issuesDeferred = remResult.issues_deferred || 0
|
|
345
|
+
log(`Remediation done — fixed: ${issuesFixed}, deferred: ${issuesDeferred} — ${remTokens} tokens`)
|
|
346
|
+
} else {
|
|
347
|
+
log('Remediation: no OPEN issues — skipped')
|
|
239
348
|
}
|
|
240
349
|
|
|
241
350
|
// ── PR ────────────────────────────────────────────────────────────────────────
|
|
@@ -258,37 +367,39 @@ const prResult = await agent(
|
|
|
258
367
|
Feature path: ${featurePath}
|
|
259
368
|
Branch: ${branch || 'current branch'}
|
|
260
369
|
Prefix: ${prefix}
|
|
261
|
-
|
|
262
|
-
Open issues: ${issuesOpen}
|
|
370
|
+
Issues fixed: ${issuesFixed}, deferred: ${issuesDeferred}
|
|
263
371
|
|
|
264
372
|
Your tasks:
|
|
265
373
|
|
|
266
|
-
1.
|
|
374
|
+
1. If there are remediation fixes, commit them:
|
|
375
|
+
git add -A
|
|
376
|
+
git commit -m "fix(${prefix}): remediate review issues"
|
|
377
|
+
|
|
378
|
+
2. Push the branch:
|
|
267
379
|
git push -u origin ${branch || 'HEAD'}
|
|
268
380
|
|
|
269
|
-
|
|
381
|
+
3. Create a PR targeting develop:
|
|
270
382
|
gh pr create \\
|
|
271
383
|
--title "feat(${prefix}): <Feature Title from feature.md>" \\
|
|
272
384
|
--base develop \\
|
|
273
385
|
--body "## Summary
|
|
274
|
-
- Implements <Feature Title
|
|
386
|
+
- Implements <Feature Title> (<N> User Stories, <N> tasks)
|
|
275
387
|
- Source docs: ${prefix}-Requirements.md, ${prefix}-Tech-Spec.md
|
|
276
388
|
|
|
277
389
|
## Implementation
|
|
278
|
-
-
|
|
279
|
-
-
|
|
280
|
-
- Open issues: ${issuesOpen} → see ${prefix}-Issues.md
|
|
390
|
+
- Architect review: all US passed
|
|
391
|
+
- Issues Register: ${issuesFixed} fixed, ${issuesDeferred} deferred → see ${prefix}-Issues.md
|
|
281
392
|
|
|
282
393
|
## Test plan
|
|
283
394
|
- [ ] Build passes
|
|
284
395
|
- [ ] Tests pass
|
|
285
396
|
- [ ] Manual smoke test of key flows"
|
|
286
397
|
|
|
287
|
-
|
|
288
|
-
Find or append the entry for ${prefix}. Set Status to "
|
|
398
|
+
4. Update the Feature Registry (internal_docs/features/REGISTRY.md):
|
|
399
|
+
Find or append the entry for ${prefix}. Set Status to "completed".
|
|
289
400
|
|
|
290
401
|
Return { "pr_url": "<url>", "registry_updated": true/false }.
|
|
291
|
-
If the PR
|
|
402
|
+
If the PR creation fails, return { "pr_url": "(PR creation failed)", "registry_updated": false }.`,
|
|
292
403
|
{
|
|
293
404
|
label: 'pr-and-registry',
|
|
294
405
|
phase: 'PR',
|
|
@@ -303,8 +414,41 @@ log(`PR done: ${prResult.pr_url} — ${prTokens} tokens`)
|
|
|
303
414
|
// ── Actuals ───────────────────────────────────────────────────────────────────
|
|
304
415
|
phase('Actuals')
|
|
305
416
|
|
|
417
|
+
// Fallback: merge in any ledger entries that were persisted to disk but are missing
|
|
418
|
+
// from the in-memory ledger (e.g. this is a resumed run and earlier phases ran in a
|
|
419
|
+
// previous, interrupted invocation). The in-memory ledger has priority for entries
|
|
420
|
+
// present in both — it reflects the most recent measurements.
|
|
421
|
+
const persistedLedgerRaw = await agent(
|
|
422
|
+
`Read ${featureDir}/${prefix}-token-ledger.json and return its contents as a raw JSON string, nothing else. If the file does not exist, return exactly "[]".`,
|
|
423
|
+
{ label: 'read-ledger', phase: 'Actuals', model: 'haiku' }
|
|
424
|
+
)
|
|
425
|
+
try {
|
|
426
|
+
const persisted = JSON.parse(typeof persistedLedgerRaw === 'string' ? persistedLedgerRaw.trim() : '[]')
|
|
427
|
+
if (Array.isArray(persisted)) {
|
|
428
|
+
const seen = new Set(tokenLedger.map(e => e.agent))
|
|
429
|
+
for (const entry of persisted) {
|
|
430
|
+
if (entry && entry.agent && !seen.has(entry.agent)) {
|
|
431
|
+
tokenLedger.push(entry)
|
|
432
|
+
seen.add(entry.agent)
|
|
433
|
+
log(`Recovered ledger entry from disk: ${entry.agent} (${entry.phase_delta_tokens} tokens)`)
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
} catch (e) {
|
|
438
|
+
log(`Could not parse persisted token ledger — using in-memory ledger only`)
|
|
439
|
+
}
|
|
440
|
+
|
|
306
441
|
const totalPhase3Tokens = tokenLedger.reduce((s, e) => s + (e.phase_delta_tokens || 0), 0)
|
|
307
442
|
|
|
443
|
+
const ACTUALS_SCHEMA = {
|
|
444
|
+
type: 'object',
|
|
445
|
+
properties: {
|
|
446
|
+
token_estimate_path: { type: 'string' },
|
|
447
|
+
effort_estimate_path: { type: 'string' },
|
|
448
|
+
},
|
|
449
|
+
required: [],
|
|
450
|
+
}
|
|
451
|
+
|
|
308
452
|
const beforeActuals = budget.spent()
|
|
309
453
|
|
|
310
454
|
await agent(
|
|
@@ -348,17 +492,14 @@ ${tokenLedger.map(e => `| ${e.agent} | — | ${e.model} | — | ${e.phase_delta_
|
|
|
348
492
|
| Metric | Value |
|
|
349
493
|
|--------|-------|
|
|
350
494
|
| Implementation phases done | ${phasesDone} |
|
|
351
|
-
| US
|
|
352
|
-
|
|
|
353
|
-
| Open issues | ${issuesOpen} |
|
|
495
|
+
| US passed | ${usPassed.join(', ') || 'N/A'} |
|
|
496
|
+
| US escalated | ${usEscalated.join(', ') || 'none'} |
|
|
354
497
|
|
|
355
498
|
TASK 2 — Update actuals in ${prefix}-Effort-Estimate.md:
|
|
356
|
-
Find the file in the feature directory. Read the existing Per-Phase Breakdown table and update
|
|
357
|
-
|
|
358
|
-
- Phases
|
|
359
|
-
-
|
|
360
|
-
|
|
361
|
-
Also append this section at the end of the file:
|
|
499
|
+
Find the file in the feature directory. Read the existing Per-Phase Breakdown table and update the "Actual Agent" column for each phase that was implemented. Use the token ledger to infer which phases completed:
|
|
500
|
+
- Phases in usPassed: ${usPassed.join(', ') || 'none'} — mark Actual Agent as completed (write the wall-clock duration if available, otherwise write "done (agent)")
|
|
501
|
+
- Phases escalated: ${usEscalated.join(', ') || 'none'} — mark Actual Agent as "escalated"
|
|
502
|
+
- Also append this summary section at the end of the file:
|
|
362
503
|
|
|
363
504
|
---
|
|
364
505
|
|
|
@@ -367,14 +508,14 @@ Also append this section at the end of the file:
|
|
|
367
508
|
| Metric | Estimated | Actual |
|
|
368
509
|
|--------|-----------|--------|
|
|
369
510
|
| Implementation phases | ${phasesDone} | ${phasesDone} |
|
|
370
|
-
|
|
|
371
|
-
|
|
|
511
|
+
| Issues fixed | — | ${issuesFixed} |
|
|
512
|
+
| Issues deferred | — | ${issuesDeferred} |
|
|
372
513
|
|
|
373
514
|
Return { "token_estimate_path": "<path>", "effort_estimate_path": "<path>" }.`,
|
|
374
515
|
{
|
|
375
516
|
label: 'write-actuals',
|
|
376
517
|
phase: 'Actuals',
|
|
377
|
-
schema:
|
|
518
|
+
schema: ACTUALS_SCHEMA,
|
|
378
519
|
}
|
|
379
520
|
)
|
|
380
521
|
|
|
@@ -383,22 +524,28 @@ tokenLedger.push({ agent: 'write-actuals', model: 'sonnet', phase_delta_tokens:
|
|
|
383
524
|
log(`Actuals written — ${actualsTokens} tokens`)
|
|
384
525
|
|
|
385
526
|
// ── Append to process-log ─────────────────────────────────────────────────────
|
|
527
|
+
const usPassed3 = usPassed.join(', ') || 'none'
|
|
528
|
+
const usEscalated3 = usEscalated.join(', ') || 'none'
|
|
386
529
|
const phase3Events = [
|
|
387
530
|
`APPROVAL GRANTED by user — Gate 2`,
|
|
388
531
|
`pm-phase3 START — implementation phase — branch: ${branch || '(current)'}`,
|
|
389
532
|
`WB parsed: ${wb.phases.length} phases`,
|
|
390
|
-
`Implementation DONE — ${phasesDone} phases
|
|
391
|
-
|
|
533
|
+
`Implementation DONE — ${phasesDone} phases | US passed: ${usPassed3} | US escalated: ${usEscalated3}`,
|
|
534
|
+
issuesFixed + issuesDeferred > 0
|
|
535
|
+
? `Remediation: ${issuesFixed} fixed, ${issuesDeferred} deferred`
|
|
536
|
+
: `Remediation: skipped (no open issues)`,
|
|
392
537
|
`PR created: ${prResult.pr_url}`,
|
|
393
538
|
`Actuals written: Token-Estimate.md + Effort-Estimate.md`,
|
|
394
539
|
`RUN COMPLETE`,
|
|
395
540
|
`════════════════════════════════════════════════════════`,
|
|
396
541
|
].join('\n')
|
|
397
542
|
|
|
543
|
+
const featureDir3 = featurePath.replace(/\/[^/]+$/, '')
|
|
544
|
+
|
|
398
545
|
await agent(
|
|
399
546
|
`Append phase 3 events to the process-log for this feature delivery run.
|
|
400
547
|
|
|
401
|
-
Process-log path: ${
|
|
548
|
+
Process-log path: ${featureDir3}/${prefix}-process-log.txt
|
|
402
549
|
|
|
403
550
|
Steps:
|
|
404
551
|
1. Get current UTC datetime via Bash: run \`date -u +"%Y-%m-%dT%H:%M:%S"\`
|
|
@@ -408,14 +555,16 @@ Steps:
|
|
|
408
555
|
|
|
409
556
|
Events to append:
|
|
410
557
|
${phase3Events}`,
|
|
411
|
-
{ label: 'finalize-process-log', phase: 'Actuals'
|
|
558
|
+
{ label: 'finalize-process-log', phase: 'Actuals' }
|
|
412
559
|
)
|
|
413
560
|
|
|
414
561
|
return {
|
|
415
562
|
pr_url: prResult.pr_url,
|
|
416
563
|
token_ledger: tokenLedger,
|
|
417
564
|
issues_summary: {
|
|
418
|
-
|
|
419
|
-
|
|
565
|
+
escalations: escalations.length,
|
|
566
|
+
escalation_details: escalations,
|
|
567
|
+
issues_fixed: issuesFixed,
|
|
568
|
+
issues_deferred: issuesDeferred,
|
|
420
569
|
},
|
|
421
570
|
}
|
package/CHANGELOG.md
CHANGED
|
@@ -1,4 +1,26 @@
|
|
|
1
1
|
## 0.1.4 (2026-07-21)
|
|
2
|
+
## [0.7.0](https://github.com/dtlab-tech/ai-toolkit/compare/v0.6.0...v0.7.0) (2026-07-29)
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
### Features
|
|
6
|
+
|
|
7
|
+
* **pipeline:** centralized test run + persistent token ledger + validation report fix + smarter define-feature ([c9fad6e](https://github.com/dtlab-tech/ai-toolkit/commit/c9fad6e4e5dfe25823df1d6534b4e597baa97001))
|
|
8
|
+
* **pm-phase3:** add centralized test run phase to streamline testing process ([972fac5](https://github.com/dtlab-tech/ai-toolkit/commit/972fac55d0754743859a7d75379a41acd49a890d))
|
|
9
|
+
* **pm-phase3:** persist token ledger to disk after every phase ([b6ad681](https://github.com/dtlab-tech/ai-toolkit/commit/b6ad6814770fae31cda60f25298dab9b2739eeaa))
|
|
10
|
+
|
|
11
|
+
## [0.6.0](https://github.com/dtlab-tech/ai-toolkit/compare/v0.5.0...v0.6.0) (2026-07-29)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
### Features
|
|
15
|
+
|
|
16
|
+
* **define-feature:** context ingestion + adaptive grilling; add AGENTS.md ([581b783](https://github.com/dtlab-tech/ai-toolkit/commit/581b783a18626796ae764ce69f658556b83ab490))
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
### Bug Fixes
|
|
20
|
+
|
|
21
|
+
* **pipeline:** persist Validation Report + smarter define-feature + AGENTS.md ([a5bae6a](https://github.com/dtlab-tech/ai-toolkit/commit/a5bae6aab65ca78aa5daa08a4a731b7c16051754))
|
|
22
|
+
* **pm-phase1:** guarantee Validation Report is persisted to disk ([53e10c0](https://github.com/dtlab-tech/ai-toolkit/commit/53e10c0e8f20021da9408d6b9e0c077142a449ae))
|
|
23
|
+
|
|
2
24
|
## [0.5.0](https://github.com/dtlab-tech/ai-toolkit/compare/v0.4.0...v0.5.0) (2026-07-29)
|
|
3
25
|
|
|
4
26
|
|
package/README.md
CHANGED
|
@@ -104,7 +104,7 @@ npm run test:coverage # run all tests and generate a coverage report
|
|
|
104
104
|
The test suite covers:
|
|
105
105
|
|
|
106
106
|
- **`tests/cli/`** — unit tests for pure functions in `bin/cli.js`
|
|
107
|
-
(`fileHash`, `walkDir`, `expandMappings`, `categorize`, `readInstalledVersion
|
|
107
|
+
(`fileHash`, `walkDir`, `expandMappings`, `categorize`, `readInstalledVersion`)
|
|
108
108
|
- **`tests/frontmatter/`** — structural validation of all `.claude/agents/*.md`
|
|
109
109
|
and `.claude/skills/**/SKILL.md` frontmatter fields
|
|
110
110
|
|