@dtlabs/ai-toolkit 0.6.0 → 0.8.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 +67 -1
- package/.claude/agents/developer-testing.md +4 -4
- package/.claude/agents/install-toolkit.md +34 -0
- package/.claude/workflows/pm-phase1.js +78 -0
- package/.claude/workflows/pm-phase2.js +103 -26
- package/.claude/workflows/pm-phase3.js +152 -17
- package/CHANGELOG.md +51 -0
- package/bin/cli.js +428 -3
- package/docs/token-pricing.json +21 -0
- package/package.json +1 -1
- package/docs/token-optimization-backlog.md +0 -115
- package/docs/token-optimization.md +0 -274
|
@@ -96,6 +96,41 @@ Ask the user: **"What is the name of this feature?"** (short, descriptive, in th
|
|
|
96
96
|
|
|
97
97
|
Example: "Supplier Onboarding" → `FTR-003-supplier-onboarding`
|
|
98
98
|
|
|
99
|
+
### 1c. Create feature directory and initialize ledger
|
|
100
|
+
|
|
101
|
+
Once you know the PREFIX (from 1a) and the feature slug (from 1b), do the following **before asking any grilling questions**:
|
|
102
|
+
|
|
103
|
+
1. Compute the paths:
|
|
104
|
+
- `featureDir` = `docs/features/{PREFIX}-{slug}` (e.g. `docs/features/FTR-003-supplier-onboarding`)
|
|
105
|
+
- `ledgerPath` = `{featureDir}/{PREFIX}-token-ledger.json`
|
|
106
|
+
|
|
107
|
+
2. Create the feature directory via Bash:
|
|
108
|
+
```bash
|
|
109
|
+
mkdir -p {featureDir}
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
3. Capture the current UTC timestamp via Bash:
|
|
113
|
+
```bash
|
|
114
|
+
started_at=$(date -u +"%Y-%m-%dT%H:%M:%SZ") && echo $started_at
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
4. Write the initial ledger file at `{ledgerPath}` using the Write tool with exactly this content (substitute the actual PREFIX and timestamp):
|
|
118
|
+
```json
|
|
119
|
+
[
|
|
120
|
+
{
|
|
121
|
+
"agent": "define-feature:define",
|
|
122
|
+
"phase": "define",
|
|
123
|
+
"model": "sonnet",
|
|
124
|
+
"status": "running",
|
|
125
|
+
"phase_delta_tokens": 0,
|
|
126
|
+
"started_at": "<started_at from step 3>",
|
|
127
|
+
"completed_at": null
|
|
128
|
+
}
|
|
129
|
+
]
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
The ledger records that the define-feature agent is now running. Proceed to Phase 2.
|
|
133
|
+
|
|
99
134
|
---
|
|
100
135
|
|
|
101
136
|
## Phase 2 — Grilling (only the gaps)
|
|
@@ -224,7 +259,7 @@ docs/features/{PREFIX}-{slug}/feature.md
|
|
|
224
259
|
|
|
225
260
|
Example: `docs/features/FTR-003-supplier-onboarding/feature.md`
|
|
226
261
|
|
|
227
|
-
|
|
262
|
+
The directory was created in Phase 1c. If for any reason it doesn't exist, create it now.
|
|
228
263
|
|
|
229
264
|
### feature.md template
|
|
230
265
|
|
|
@@ -315,6 +350,37 @@ For each entity: name, key fields (name, type, constraints), relationships.
|
|
|
315
350
|
- 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")
|
|
316
351
|
- Be concrete and specific — avoid vague statements like "the system should be fast"
|
|
317
352
|
|
|
353
|
+
### 4b. Finalize ledger entry
|
|
354
|
+
|
|
355
|
+
After writing `feature.md`, update the ledger entry to record successful completion:
|
|
356
|
+
|
|
357
|
+
1. Capture the current UTC timestamp via Bash:
|
|
358
|
+
```bash
|
|
359
|
+
completed_at=$(date -u +"%Y-%m-%dT%H:%M:%SZ") && echo $completed_at
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
2. Read the ledger file at `{featureDir}/{PREFIX}-token-ledger.json` using the Read tool.
|
|
363
|
+
|
|
364
|
+
3. Write the updated ledger file using the Write tool, replacing the entry's fields:
|
|
365
|
+
- `"status"`: `"done"`
|
|
366
|
+
- `"completed_at"`: the timestamp captured in step 1
|
|
367
|
+
- `"phase_delta_tokens"`: leave as `0` — token measurement is not available to the agent directly
|
|
368
|
+
|
|
369
|
+
Example final ledger content:
|
|
370
|
+
```json
|
|
371
|
+
[
|
|
372
|
+
{
|
|
373
|
+
"agent": "define-feature:define",
|
|
374
|
+
"phase": "define",
|
|
375
|
+
"model": "sonnet",
|
|
376
|
+
"status": "done",
|
|
377
|
+
"phase_delta_tokens": 0,
|
|
378
|
+
"started_at": "<original started_at>",
|
|
379
|
+
"completed_at": "<completed_at from step 1>"
|
|
380
|
+
}
|
|
381
|
+
]
|
|
382
|
+
```
|
|
383
|
+
|
|
318
384
|
---
|
|
319
385
|
|
|
320
386
|
## Phase 5 — Confirm and handoff
|
|
@@ -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.
|
|
@@ -251,6 +251,39 @@ COMPACT_SECTION
|
|
|
251
251
|
|
|
252
252
|
---
|
|
253
253
|
|
|
254
|
+
## Step 6 — Bash Permission Allowlist opt-in
|
|
255
|
+
|
|
256
|
+
After the Compact Instructions step, offer an opt-in to create or update `.claude/settings.local.json` in the destination project with a pre-approved Bash command allowlist.
|
|
257
|
+
|
|
258
|
+
1. **Offer the opt-in** — inform the user and ask:
|
|
259
|
+
|
|
260
|
+
> "Would you like to create or update `.claude/settings.local.json` in this project with a pre-approved Bash command allowlist? This lets pm-phase3 worker agents run read-only commands (ls, git status, git log, npm test, dotnet build, etc.) without prompting you. Dangerous commands (git push, gh pr create, rm, git reset, git clean) will always require confirmation."
|
|
261
|
+
>
|
|
262
|
+
> Options: "Yes — write allowlist" / "No — skip"
|
|
263
|
+
|
|
264
|
+
2. If the user selects **No**: skip silently; set `allowlist_status = "skipped (user said No)"`.
|
|
265
|
+
|
|
266
|
+
3. If the user selects **Yes**:
|
|
267
|
+
|
|
268
|
+
a. **Invoke the merge CLI** from the toolkit root directory:
|
|
269
|
+
```bash
|
|
270
|
+
node bin/cli.js merge-allowlist "{dest}"
|
|
271
|
+
```
|
|
272
|
+
Capture stdout and the exit code.
|
|
273
|
+
- If exit code is non-zero: set `allowlist_status = "failed — see above"` and continue to Step 7 (allowlist is advisory, not blocking).
|
|
274
|
+
- If exit code is 0: the stdout line will be one of:
|
|
275
|
+
- `Allowlist: written` → set `allowlist_status = "written"`
|
|
276
|
+
- `Allowlist: merged (N rules preserved)` → set `allowlist_status = "merged (N rules preserved)"`
|
|
277
|
+
- `Allowlist: reset` → set `allowlist_status = "reset (file was malformed)"`
|
|
278
|
+
|
|
279
|
+
b. **Check and update `.gitignore`**:
|
|
280
|
+
```bash
|
|
281
|
+
node bin/cli.js update-gitignore "{dest}"
|
|
282
|
+
```
|
|
283
|
+
This ensures `.claude/settings.local.json` is gitignored in the destination project. The CLI is idempotent — it appends the line only if it is not already present.
|
|
284
|
+
|
|
285
|
+
---
|
|
286
|
+
|
|
254
287
|
## Step 7 — Report
|
|
255
288
|
|
|
256
289
|
```
|
|
@@ -262,6 +295,7 @@ Modified/kept: N (user chose to keep existing version)
|
|
|
262
295
|
Unchanged (same): N (skipped silently)
|
|
263
296
|
Spawn depth: ✅ CLAUDE_CODE_MAX_SUBAGENT_SPAWN_DEPTH=2+ present / ⚠️ MISSING — see action above
|
|
264
297
|
Compact instructions: {compact_instructions_status}
|
|
298
|
+
Allowlist: {allowlist_status}
|
|
265
299
|
──────────────────────────────────────────────────
|
|
266
300
|
Next steps:
|
|
267
301
|
1. Run /init-agents to generate AGENTS.md for this project
|
|
@@ -9,6 +9,26 @@ export const meta = {
|
|
|
9
9
|
],
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
+
// ── Ledger helper functions ───────────────────────────────────────────────────
|
|
13
|
+
|
|
14
|
+
async function appendLedgerEntry(featureDir, prefix, entry) {
|
|
15
|
+
const ledgerPath = `${featureDir}/${prefix}-token-ledger.json`
|
|
16
|
+
const entryJson = JSON.stringify(entry)
|
|
17
|
+
await agent(
|
|
18
|
+
`Append a JSON object to the ledger array at: ${ledgerPath}\n\n1. Read the file. If it does not exist or cannot be parsed as a JSON array, start with [].\n2. Push this object onto the array: ${entryJson}\n3. Write the full array back (JSON, 2-space indent). Return no output.`,
|
|
19
|
+
{ label: 'append-ledger', phase: 'Requirements', model: 'haiku' }
|
|
20
|
+
)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async function updateLedgerEntry(featureDir, prefix, agentKey, updates) {
|
|
24
|
+
const ledgerPath = `${featureDir}/${prefix}-token-ledger.json`
|
|
25
|
+
const updatesJson = JSON.stringify(updates)
|
|
26
|
+
await agent(
|
|
27
|
+
`Update an entry in the ledger array at: ${ledgerPath}\n\n1. Read the file. If it does not exist or cannot be parsed as a JSON array, do nothing.\n2. Search from the end for the last entry where agent === "${agentKey}".\n3. If found, merge these fields into that entry: ${updatesJson}\n4. Write the full array back (JSON, 2-space indent). Return no output.\n5. If not found, do nothing.`,
|
|
28
|
+
{ label: 'update-ledger', phase: 'Requirements', model: 'haiku' }
|
|
29
|
+
)
|
|
30
|
+
}
|
|
31
|
+
|
|
12
32
|
// ── Parse args ────────────────────────────────────────────────────────────────
|
|
13
33
|
// args is the raw prompt string: "<path-to-feature.md> [--force]"
|
|
14
34
|
const featurePath = args.split(/\s+/)[0]
|
|
@@ -70,6 +90,16 @@ const { prefix, feature_dir } = discoveryResult
|
|
|
70
90
|
log(`Prefix: ${prefix} | Dir: ${feature_dir}`)
|
|
71
91
|
log(`needs_requirements=${discoveryResult.needs_requirements} needs_tech_spec=${discoveryResult.needs_tech_spec}`)
|
|
72
92
|
|
|
93
|
+
// ── Ensure ledger file exists (US-02-T02) ─────────────────────────────────────
|
|
94
|
+
// If define-feature was not used, the ledger file will not exist yet.
|
|
95
|
+
// Touch it with an empty array so appendLedgerEntry can always assume a valid base.
|
|
96
|
+
const ledgerFilePath = `${feature_dir}/${prefix}-token-ledger.json`
|
|
97
|
+
await agent(
|
|
98
|
+
`Check whether the file ${ledgerFilePath} exists.\n\n1. Try to read the file using the Read tool.\n2. If the file does NOT exist: write it now using the Write tool with contents: []\n3. If the file already exists and is a valid JSON array: do nothing.\n4. If the file exists but is not valid JSON: overwrite it with: []\n5. Return no output.`,
|
|
99
|
+
{ label: 'ensure-ledger', phase: 'Discovery', model: 'haiku' }
|
|
100
|
+
)
|
|
101
|
+
log(`Ledger ensured at ${ledgerFilePath}`)
|
|
102
|
+
|
|
73
103
|
// ── generate-requirements ─────────────────────────────────────────────────────
|
|
74
104
|
phase('Requirements')
|
|
75
105
|
|
|
@@ -77,9 +107,25 @@ const tokenLedger = []
|
|
|
77
107
|
const errors = []
|
|
78
108
|
|
|
79
109
|
if (discoveryResult.needs_requirements) {
|
|
110
|
+
const reqKey = 'generate-requirements:phase1'
|
|
111
|
+
const reqStartedAt = new Date().toISOString()
|
|
112
|
+
await appendLedgerEntry(feature_dir, prefix, {
|
|
113
|
+
agent: reqKey,
|
|
114
|
+
phase: 'phase1',
|
|
115
|
+
model: 'haiku',
|
|
116
|
+
status: 'running',
|
|
117
|
+
phase_delta_tokens: 0,
|
|
118
|
+
started_at: reqStartedAt,
|
|
119
|
+
completed_at: null,
|
|
120
|
+
})
|
|
80
121
|
const beforeReq = budget.spent()
|
|
81
122
|
const reqResult = await agent(featurePath, { agentType: 'generate-requirements', label: 'generate-requirements', phase: 'Requirements' })
|
|
82
123
|
const reqTokens = budget.spent() - beforeReq
|
|
124
|
+
await updateLedgerEntry(feature_dir, prefix, reqKey, {
|
|
125
|
+
status: 'done',
|
|
126
|
+
completed_at: new Date().toISOString(),
|
|
127
|
+
phase_delta_tokens: reqTokens,
|
|
128
|
+
})
|
|
83
129
|
tokenLedger.push({ agent: 'generate-requirements', model: 'haiku', phase_delta_tokens: reqTokens })
|
|
84
130
|
log(`generate-requirements done — phase delta: ${reqTokens} tokens`)
|
|
85
131
|
} else {
|
|
@@ -90,9 +136,25 @@ if (discoveryResult.needs_requirements) {
|
|
|
90
136
|
phase('Tech-Spec')
|
|
91
137
|
|
|
92
138
|
if (discoveryResult.needs_tech_spec) {
|
|
139
|
+
const specKey = 'generate-tech-spec:phase1'
|
|
140
|
+
const specStartedAt = new Date().toISOString()
|
|
141
|
+
await appendLedgerEntry(feature_dir, prefix, {
|
|
142
|
+
agent: specKey,
|
|
143
|
+
phase: 'phase1',
|
|
144
|
+
model: 'haiku',
|
|
145
|
+
status: 'running',
|
|
146
|
+
phase_delta_tokens: 0,
|
|
147
|
+
started_at: specStartedAt,
|
|
148
|
+
completed_at: null,
|
|
149
|
+
})
|
|
93
150
|
const beforeSpec = budget.spent()
|
|
94
151
|
const specResult = await agent(featurePath, { agentType: 'generate-tech-spec', label: 'generate-tech-spec', phase: 'Tech-Spec' })
|
|
95
152
|
const specTokens = budget.spent() - beforeSpec
|
|
153
|
+
await updateLedgerEntry(feature_dir, prefix, specKey, {
|
|
154
|
+
status: 'done',
|
|
155
|
+
completed_at: new Date().toISOString(),
|
|
156
|
+
phase_delta_tokens: specTokens,
|
|
157
|
+
})
|
|
96
158
|
tokenLedger.push({ agent: 'generate-tech-spec', model: 'haiku', phase_delta_tokens: specTokens })
|
|
97
159
|
log(`generate-tech-spec done — phase delta: ${specTokens} tokens`)
|
|
98
160
|
} else {
|
|
@@ -108,9 +170,25 @@ const MAX_CYCLES = 3
|
|
|
108
170
|
|
|
109
171
|
for (let cycle = 1; cycle <= MAX_CYCLES; cycle++) {
|
|
110
172
|
log(`validate-feature-docs cycle ${cycle}`)
|
|
173
|
+
const valKey = `validate-feature-docs:phase1:cycle${cycle}`
|
|
174
|
+
const valStartedAt = new Date().toISOString()
|
|
175
|
+
await appendLedgerEntry(feature_dir, prefix, {
|
|
176
|
+
agent: valKey,
|
|
177
|
+
phase: 'phase1',
|
|
178
|
+
model: 'haiku',
|
|
179
|
+
status: 'running',
|
|
180
|
+
phase_delta_tokens: 0,
|
|
181
|
+
started_at: valStartedAt,
|
|
182
|
+
completed_at: null,
|
|
183
|
+
})
|
|
111
184
|
const beforeVal = budget.spent()
|
|
112
185
|
const valResult = await agent(featurePath, { agentType: 'validate-feature-docs', label: `validate-feature-docs (cycle ${cycle})`, phase: 'Validation' })
|
|
113
186
|
const valTokens = budget.spent() - beforeVal
|
|
187
|
+
await updateLedgerEntry(feature_dir, prefix, valKey, {
|
|
188
|
+
status: 'done',
|
|
189
|
+
completed_at: new Date().toISOString(),
|
|
190
|
+
phase_delta_tokens: valTokens,
|
|
191
|
+
})
|
|
114
192
|
tokenLedger.push({ agent: `validate-feature-docs (cycle ${cycle})`, model: 'haiku', phase_delta_tokens: valTokens })
|
|
115
193
|
|
|
116
194
|
// valResult is the agent's text output — check for gap indicators
|
|
@@ -7,15 +7,54 @@ export const meta = {
|
|
|
7
7
|
],
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
+
// ── Ledger helper functions ───────────────────────────────────────────────────
|
|
11
|
+
// Route all ledger I/O through agent() — fs is not available in the workflow runtime.
|
|
12
|
+
// Mirrors appendLedgerEntry / updateLedgerEntry from bin/cli.js (same contract).
|
|
13
|
+
|
|
14
|
+
async function appendLedgerEntry(featureDir, prefix, entry) {
|
|
15
|
+
const ledgerPath = `${featureDir}/${prefix}-token-ledger.json`
|
|
16
|
+
const entryJson = JSON.stringify(entry)
|
|
17
|
+
await agent(
|
|
18
|
+
`Append a JSON object to the ledger array at: ${ledgerPath}\n\n1. Read the file. If it does not exist or cannot be parsed as a JSON array, start with [].\n2. Push this object onto the array: ${entryJson}\n3. Write the full array back (JSON, 2-space indent). Return no output.`,
|
|
19
|
+
{ label: 'append-ledger', phase: 'Work Breakdown', model: 'haiku' }
|
|
20
|
+
)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async function updateLedgerEntry(featureDir, prefix, agentKey, updates) {
|
|
24
|
+
const ledgerPath = `${featureDir}/${prefix}-token-ledger.json`
|
|
25
|
+
const updatesJson = JSON.stringify(updates)
|
|
26
|
+
await agent(
|
|
27
|
+
`Update an entry in the ledger array at: ${ledgerPath}\n\n1. Read the file. If it does not exist or cannot be parsed as a JSON array, do nothing.\n2. Search from the end for the last entry where agent === "${agentKey}".\n3. If found, merge these fields into that entry: ${updatesJson}\n4. Write the full array back (JSON, 2-space indent). Return no output.\n5. If not found, do nothing.`,
|
|
28
|
+
{ label: 'update-ledger', phase: 'Work Breakdown', model: 'haiku' }
|
|
29
|
+
)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// ── Parse args ────────────────────────────────────────────────────────────────
|
|
33
|
+
|
|
10
34
|
// args: "<path-to-feature.md>"
|
|
11
35
|
const featurePath = (typeof args === 'string' ? args : '').trim().split(/\s+/)[0]
|
|
12
36
|
|
|
37
|
+
// Derive feature directory and prefix early (needed for ledger writes before metrics agent runs)
|
|
38
|
+
const featureDir = featurePath.replace(/\/[^/]+$/, '')
|
|
39
|
+
const prefixMatch = featureDir.match(/([A-Z]+-\d+)/)
|
|
40
|
+
const prefix = prefixMatch ? prefixMatch[1] : 'FTR-000'
|
|
41
|
+
|
|
13
42
|
// ── generate-work-breakdown ───────────────────────────────────────────────────
|
|
14
43
|
phase('Work Breakdown')
|
|
15
44
|
|
|
16
45
|
const tokenLedger = []
|
|
17
46
|
|
|
18
47
|
log(`Running generate-work-breakdown for ${featurePath}`)
|
|
48
|
+
const wbStartedAt = new Date().toISOString()
|
|
49
|
+
await appendLedgerEntry(featureDir, prefix, {
|
|
50
|
+
agent: 'generate-work-breakdown:phase2',
|
|
51
|
+
phase: 'phase2',
|
|
52
|
+
model: 'haiku',
|
|
53
|
+
status: 'running',
|
|
54
|
+
phase_delta_tokens: 0,
|
|
55
|
+
started_at: wbStartedAt,
|
|
56
|
+
completed_at: null,
|
|
57
|
+
})
|
|
19
58
|
const beforeWB = budget.spent()
|
|
20
59
|
await agent(featurePath, {
|
|
21
60
|
agentType: 'generate-work-breakdown',
|
|
@@ -23,6 +62,11 @@ await agent(featurePath, {
|
|
|
23
62
|
phase: 'Work Breakdown',
|
|
24
63
|
})
|
|
25
64
|
const wbTokens = budget.spent() - beforeWB
|
|
65
|
+
await updateLedgerEntry(featureDir, prefix, 'generate-work-breakdown:phase2', {
|
|
66
|
+
status: 'done',
|
|
67
|
+
completed_at: new Date().toISOString(),
|
|
68
|
+
phase_delta_tokens: wbTokens,
|
|
69
|
+
})
|
|
26
70
|
tokenLedger.push({ agent: 'generate-work-breakdown', model: 'haiku', phase_delta_tokens: wbTokens })
|
|
27
71
|
log(`generate-work-breakdown done — phase delta: ${wbTokens} tokens`)
|
|
28
72
|
|
|
@@ -48,7 +92,34 @@ const PARSE_SCHEMA = {
|
|
|
48
92
|
required: ['prefix', 'user_stories', 'total_tasks', 'work_breakdown_path'],
|
|
49
93
|
}
|
|
50
94
|
|
|
51
|
-
|
|
95
|
+
// ── Load token pricing ────────────────────────────────────────────────────────
|
|
96
|
+
// Pricing config is read inline (haiku agent) so it survives path-agnostic installs.
|
|
97
|
+
// Cost formula: tokens * (0.8 * input + 0.2 * output) / 1_000_000 * usd_to_eur
|
|
98
|
+
// (80/20 input/output split — no per-agent breakdown available in the ledger)
|
|
99
|
+
const pricingRaw = await agent(
|
|
100
|
+
`Read the file docs/token-pricing.json and return its raw JSON contents as a string, nothing else.`,
|
|
101
|
+
{ label: 'read-pricing', phase: 'Effort Estimate', model: 'haiku' }
|
|
102
|
+
)
|
|
103
|
+
let pricing = null
|
|
104
|
+
try { pricing = JSON.parse(typeof pricingRaw === 'string' ? pricingRaw.trim() : '{}') } catch (_) {}
|
|
105
|
+
|
|
106
|
+
function tokenCostEur(tokens, model) {
|
|
107
|
+
if (!pricing || !pricing.models) return null
|
|
108
|
+
const m = pricing.models[model] || pricing.models['sonnet']
|
|
109
|
+
const usdPer1M = 0.8 * m.input_per_1m_usd + 0.2 * m.output_per_1m_usd
|
|
110
|
+
return (tokens * usdPer1M / 1_000_000) * (pricing.usd_to_eur || 1)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function formatEur(val) {
|
|
114
|
+
if (val === null || val === undefined) return '—'
|
|
115
|
+
return '€' + val.toFixed(4)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// NOTE: pm-phase2 is a separate workflow and does NOT receive pm-phase1's token ledger,
|
|
119
|
+
// so the real Phase 1 actuals (generate-requirements/tech-spec/validate) are unknown here.
|
|
120
|
+
// They are written as "—" placeholders below and filled later by the orchestrator (the
|
|
121
|
+
// implement-feature skill Step 7, which holds all three phase ledgers). Do NOT reuse wbTokens
|
|
122
|
+
// as a stand-in for Phase 1 — that mislabels Phase 2's tokens as Phase 1's.
|
|
52
123
|
|
|
53
124
|
const metrics = await agent(
|
|
54
125
|
`You have four tasks:
|
|
@@ -113,46 +184,52 @@ Write {feature_dir}/{PREFIX}-Token-Estimate.md with this format:
|
|
|
113
184
|
|
|
114
185
|
## Phase 1 — Documentation (Actuals)
|
|
115
186
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
|
120
|
-
|
|
121
|
-
|
|
|
187
|
+
Phase 1 ran in a separate workflow (pm-phase1); its per-agent token actuals are filled in
|
|
188
|
+
by the orchestrator after implementation. Leave the Tokens Actual cells as "—" here.
|
|
189
|
+
|
|
190
|
+
| Agent | Task | Model | Tokens Est. | Est. cost € | Tokens Actual | Actual cost € |
|
|
191
|
+
|-------|------|-------|------------|------------|--------------|--------------|
|
|
192
|
+
| generate-requirements | Generate requirements from feature.md | haiku | — | — | — | — |
|
|
193
|
+
| generate-tech-spec | Generate tech spec from feature.md | haiku | — | — | — | — |
|
|
194
|
+
| validate-feature-docs | Validate requirements + tech spec | haiku | — | — | — | — |
|
|
195
|
+
| **Phase 1 total** | | | **—** | **—** | **—** | **—** |
|
|
122
196
|
|
|
123
197
|
## Phase 2 — Work Breakdown (Actuals)
|
|
124
198
|
|
|
125
|
-
| Agent | Task | Model | Tokens Est. | Tokens Actual |
|
|
126
|
-
|
|
127
|
-
| generate-work-breakdown | Generate work breakdown from docs | haiku | — | {wbTokens from above} |
|
|
128
|
-
| **Phase 2 total** | | | **—** | **{wbTokens from above}** |
|
|
199
|
+
| Agent | Task | Model | Tokens Est. | Est. cost € | Tokens Actual | Actual cost € |
|
|
200
|
+
|-------|------|-------|------------|------------|--------------|--------------|
|
|
201
|
+
| generate-work-breakdown | Generate work breakdown from docs | haiku | — | — | {wbTokens from above} | ${formatEur(tokenCostEur(wbTokens, 'haiku'))} |
|
|
202
|
+
| **Phase 2 total** | | | **—** | **—** | **{wbTokens from above}** | **${formatEur(tokenCostEur(wbTokens, 'haiku'))}** |
|
|
129
203
|
|
|
130
204
|
## Phase 3 — Implementation (Estimates)
|
|
131
205
|
|
|
132
206
|
Estimates based on {total_tasks} tasks ({domain_breakdown}), {user_stories} User Stories.
|
|
133
207
|
Baseline: ~15,000 tokens/BE task, ~8,000/TEST task, ~5,000/INFRA task.
|
|
134
208
|
|
|
135
|
-
| Agent | Task | Model | Tokens Est. | Tokens Actual |
|
|
136
|
-
|
|
137
|
-
| developer-backend | Implement BE/INFRA tasks | sonnet | {estimated: be_tasks * 15000 + infra_tasks * 5000} | — |
|
|
138
|
-
| developer-testing | Implement TEST tasks | sonnet | {estimated: test_tasks * 8000} | — |
|
|
139
|
-
| review-solution (×{user_stories}) | Architect review per US | sonnet | {estimated: user_stories * 8000} | — |
|
|
140
|
-
| remediation | Fix review issues | sonnet | ~10,000 | — |
|
|
141
|
-
| pr-and-registry | Push branch, create PR | sonnet | ~5,000 | — |
|
|
142
|
-
| write-actuals | Update Token/Effort Estimate | sonnet | ~3,000 | — |
|
|
143
|
-
| **Phase 3 total** | | | **{sum of phase 3 estimates}** | **—** |
|
|
209
|
+
| Agent | Task | Model | Tokens Est. | Est. cost € | Tokens Actual | Actual cost € |
|
|
210
|
+
|-------|------|-------|------------|------------|--------------|--------------|
|
|
211
|
+
| developer-backend | Implement BE/INFRA tasks | sonnet | {estimated: be_tasks * 15000 + infra_tasks * 5000} | {cost for that estimate, sonnet} | — | — |
|
|
212
|
+
| developer-testing | Implement TEST tasks | sonnet | {estimated: test_tasks * 8000} | {cost for that estimate, sonnet} | — | — |
|
|
213
|
+
| review-solution (×{user_stories}) | Architect review per US | sonnet | {estimated: user_stories * 8000} | {cost for that estimate, sonnet} | — | — |
|
|
214
|
+
| remediation | Fix review issues | sonnet | ~10,000 | {cost for 10000, sonnet} | — | — |
|
|
215
|
+
| pr-and-registry | Push branch, create PR | sonnet | ~5,000 | {cost for 5000, sonnet} | — | — |
|
|
216
|
+
| write-actuals | Update Token/Effort Estimate | sonnet | ~3,000 | {cost for 3000, sonnet} | — | — |
|
|
217
|
+
| **Phase 3 total** | | | **{sum of phase 3 token estimates}** | **{sum of phase 3 cost estimates}** | **—** | **—** |
|
|
218
|
+
|
|
219
|
+
For the "Est. cost €" column in Phase 3: use the formula tokens * (0.8 * 3.00 + 0.2 * 15.00) / 1_000_000 * ${pricing ? pricing.usd_to_eur : 0.92} for sonnet rows. Round to 4 decimal places, prefix with €.
|
|
144
220
|
|
|
145
221
|
## Grand Total
|
|
146
222
|
|
|
147
|
-
| Phase | Tokens Est. | Tokens Actual |
|
|
148
|
-
|
|
149
|
-
| Phase 1 — Documentation | — |
|
|
150
|
-
| Phase 2 — Work Breakdown | — | {wbTokens from above} |
|
|
151
|
-
| Phase 3 — Implementation | {sum of phase 3 estimates} | — |
|
|
152
|
-
| **Total** | **{sum of phase 3 estimates}** | **{
|
|
223
|
+
| Phase | Tokens Est. | Est. cost € | Tokens Actual | Actual cost € |
|
|
224
|
+
|-------|------------|------------|--------------|--------------|
|
|
225
|
+
| Phase 1 — Documentation | — | — | — (filled by orchestrator) | — |
|
|
226
|
+
| Phase 2 — Work Breakdown | — | — | {wbTokens from above} | ${formatEur(tokenCostEur(wbTokens, 'haiku'))} |
|
|
227
|
+
| Phase 3 — Implementation | {sum of phase 3 token estimates} | {sum of phase 3 cost estimates} | — | — |
|
|
228
|
+
| **Total** | **{sum of phase 3 token estimates}** | **{sum of phase 3 cost estimates}** | **{wbTokens} (partial)** | **${formatEur(tokenCostEur(wbTokens, 'haiku'))} (partial)** |
|
|
153
229
|
|
|
154
230
|
---
|
|
155
231
|
*Actuals will be appended by pm-phase3 after implementation completes.*
|
|
232
|
+
*Cost assumes 80% input / 20% output token split. Pricing: sonnet $3.00/$15.00 per 1M, haiku $0.80/$4.00 per 1M (USD). Rate: $1 = €${pricing ? pricing.usd_to_eur : '0.92'} (${pricing ? pricing.usd_to_eur_date : 'see docs/token-pricing.json'}).*
|
|
156
233
|
|
|
157
234
|
TASK 4 — Set return values.
|
|
158
235
|
Set effort_estimate_path to the full path of the written Effort-Estimate.md.
|