@dtlabs/ai-toolkit 0.7.0 → 0.9.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.
@@ -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
- Create the directory if it doesn't exist.
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
@@ -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,38 @@ 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
+ // started_at is generated via Bash inside the agent to get a real UTC timestamp with time
17
+ const entryWithoutTs = JSON.stringify({ ...entry, started_at: '__TS__', completed_at: null })
18
+ await agent(
19
+ `Append a JSON object to the ledger array at: ${ledgerPath}\n\n` +
20
+ `1. Run: date -u +"%Y-%m-%dT%H:%M:%SZ" and capture the output as NOW.\n` +
21
+ `2. Read the file. If it does not exist or cannot be parsed as a JSON array, start with [].\n` +
22
+ `3. Push this object onto the array, replacing "__TS__" in started_at with NOW: ${entryWithoutTs}\n` +
23
+ `4. Write the full array back (JSON, 2-space indent). Return no output.`,
24
+ { label: 'append-ledger', phase: 'Requirements', model: 'haiku' }
25
+ )
26
+ }
27
+
28
+ async function updateLedgerEntry(featureDir, prefix, agentKey, updates) {
29
+ const ledgerPath = `${featureDir}/${prefix}-token-ledger.json`
30
+ // completed_at is generated via Bash inside the agent to get a real UTC timestamp with time
31
+ const updatesWithoutTs = JSON.stringify({ ...updates, completed_at: '__TS__' })
32
+ await agent(
33
+ `Update an entry in the ledger array at: ${ledgerPath}\n\n` +
34
+ `1. Run: date -u +"%Y-%m-%dT%H:%M:%SZ" and capture the output as NOW.\n` +
35
+ `2. Read the file. If it does not exist or cannot be parsed as a JSON array, do nothing.\n` +
36
+ `3. Search from the end for the last entry where agent === "${agentKey}".\n` +
37
+ `4. If found, merge these fields into that entry, replacing "__TS__" in completed_at with NOW: ${updatesWithoutTs}\n` +
38
+ `5. Write the full array back (JSON, 2-space indent). Return no output.\n` +
39
+ `6. If not found, do nothing.`,
40
+ { label: 'update-ledger', phase: 'Requirements', model: 'haiku' }
41
+ )
42
+ }
43
+
12
44
  // ── Parse args ────────────────────────────────────────────────────────────────
13
45
  // args is the raw prompt string: "<path-to-feature.md> [--force]"
14
46
  const featurePath = args.split(/\s+/)[0]
@@ -70,6 +102,16 @@ const { prefix, feature_dir } = discoveryResult
70
102
  log(`Prefix: ${prefix} | Dir: ${feature_dir}`)
71
103
  log(`needs_requirements=${discoveryResult.needs_requirements} needs_tech_spec=${discoveryResult.needs_tech_spec}`)
72
104
 
105
+ // ── Ensure ledger file exists (US-02-T02) ─────────────────────────────────────
106
+ // If define-feature was not used, the ledger file will not exist yet.
107
+ // Touch it with an empty array so appendLedgerEntry can always assume a valid base.
108
+ const ledgerFilePath = `${feature_dir}/${prefix}-token-ledger.json`
109
+ await agent(
110
+ `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.`,
111
+ { label: 'ensure-ledger', phase: 'Discovery', model: 'haiku' }
112
+ )
113
+ log(`Ledger ensured at ${ledgerFilePath}`)
114
+
73
115
  // ── generate-requirements ─────────────────────────────────────────────────────
74
116
  phase('Requirements')
75
117
 
@@ -77,9 +119,24 @@ const tokenLedger = []
77
119
  const errors = []
78
120
 
79
121
  if (discoveryResult.needs_requirements) {
122
+ const reqKey = 'generate-requirements:phase1'
123
+ await appendLedgerEntry(feature_dir, prefix, {
124
+ agent: reqKey,
125
+ phase: 'phase1',
126
+ model: 'haiku',
127
+ status: 'running',
128
+ phase_delta_tokens: 0,
129
+ started_at: '__TS__',
130
+ completed_at: null,
131
+ })
80
132
  const beforeReq = budget.spent()
81
133
  const reqResult = await agent(featurePath, { agentType: 'generate-requirements', label: 'generate-requirements', phase: 'Requirements' })
82
134
  const reqTokens = budget.spent() - beforeReq
135
+ await updateLedgerEntry(feature_dir, prefix, reqKey, {
136
+ status: 'done',
137
+ completed_at: '__TS__',
138
+ phase_delta_tokens: reqTokens,
139
+ })
83
140
  tokenLedger.push({ agent: 'generate-requirements', model: 'haiku', phase_delta_tokens: reqTokens })
84
141
  log(`generate-requirements done — phase delta: ${reqTokens} tokens`)
85
142
  } else {
@@ -90,9 +147,24 @@ if (discoveryResult.needs_requirements) {
90
147
  phase('Tech-Spec')
91
148
 
92
149
  if (discoveryResult.needs_tech_spec) {
150
+ const specKey = 'generate-tech-spec:phase1'
151
+ await appendLedgerEntry(feature_dir, prefix, {
152
+ agent: specKey,
153
+ phase: 'phase1',
154
+ model: 'haiku',
155
+ status: 'running',
156
+ phase_delta_tokens: 0,
157
+ started_at: '__TS__',
158
+ completed_at: null,
159
+ })
93
160
  const beforeSpec = budget.spent()
94
161
  const specResult = await agent(featurePath, { agentType: 'generate-tech-spec', label: 'generate-tech-spec', phase: 'Tech-Spec' })
95
162
  const specTokens = budget.spent() - beforeSpec
163
+ await updateLedgerEntry(feature_dir, prefix, specKey, {
164
+ status: 'done',
165
+ completed_at: '__TS__',
166
+ phase_delta_tokens: specTokens,
167
+ })
96
168
  tokenLedger.push({ agent: 'generate-tech-spec', model: 'haiku', phase_delta_tokens: specTokens })
97
169
  log(`generate-tech-spec done — phase delta: ${specTokens} tokens`)
98
170
  } else {
@@ -108,9 +180,24 @@ const MAX_CYCLES = 3
108
180
 
109
181
  for (let cycle = 1; cycle <= MAX_CYCLES; cycle++) {
110
182
  log(`validate-feature-docs cycle ${cycle}`)
183
+ const valKey = `validate-feature-docs:phase1:cycle${cycle}`
184
+ await appendLedgerEntry(feature_dir, prefix, {
185
+ agent: valKey,
186
+ phase: 'phase1',
187
+ model: 'haiku',
188
+ status: 'running',
189
+ phase_delta_tokens: 0,
190
+ started_at: '__TS__',
191
+ completed_at: null,
192
+ })
111
193
  const beforeVal = budget.spent()
112
194
  const valResult = await agent(featurePath, { agentType: 'validate-feature-docs', label: `validate-feature-docs (cycle ${cycle})`, phase: 'Validation' })
113
195
  const valTokens = budget.spent() - beforeVal
196
+ await updateLedgerEntry(feature_dir, prefix, valKey, {
197
+ status: 'done',
198
+ completed_at: '__TS__',
199
+ phase_delta_tokens: valTokens,
200
+ })
114
201
  tokenLedger.push({ agent: `validate-feature-docs (cycle ${cycle})`, model: 'haiku', phase_delta_tokens: valTokens })
115
202
 
116
203
  // valResult is the agent's text output — check for gap indicators
@@ -7,15 +7,65 @@ 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
+ // started_at is generated via Bash inside the agent to get a real UTC timestamp with time
17
+ const entryWithoutTs = JSON.stringify({ ...entry, started_at: '__TS__', completed_at: null })
18
+ await agent(
19
+ `Append a JSON object to the ledger array at: ${ledgerPath}\n\n` +
20
+ `1. Run: date -u +"%Y-%m-%dT%H:%M:%SZ" and capture the output as NOW.\n` +
21
+ `2. Read the file. If it does not exist or cannot be parsed as a JSON array, start with [].\n` +
22
+ `3. Push this object onto the array, replacing "__TS__" in started_at with NOW: ${entryWithoutTs}\n` +
23
+ `4. Write the full array back (JSON, 2-space indent). Return no output.`,
24
+ { label: 'append-ledger', phase: 'Work Breakdown', model: 'haiku' }
25
+ )
26
+ }
27
+
28
+ async function updateLedgerEntry(featureDir, prefix, agentKey, updates) {
29
+ const ledgerPath = `${featureDir}/${prefix}-token-ledger.json`
30
+ // completed_at is generated via Bash inside the agent to get a real UTC timestamp with time
31
+ const updatesWithoutTs = JSON.stringify({ ...updates, completed_at: '__TS__' })
32
+ await agent(
33
+ `Update an entry in the ledger array at: ${ledgerPath}\n\n` +
34
+ `1. Run: date -u +"%Y-%m-%dT%H:%M:%SZ" and capture the output as NOW.\n` +
35
+ `2. Read the file. If it does not exist or cannot be parsed as a JSON array, do nothing.\n` +
36
+ `3. Search from the end for the last entry where agent === "${agentKey}".\n` +
37
+ `4. If found, merge these fields into that entry, replacing "__TS__" in completed_at with NOW: ${updatesWithoutTs}\n` +
38
+ `5. Write the full array back (JSON, 2-space indent). Return no output.\n` +
39
+ `6. If not found, do nothing.`,
40
+ { label: 'update-ledger', phase: 'Work Breakdown', model: 'haiku' }
41
+ )
42
+ }
43
+
44
+ // ── Parse args ────────────────────────────────────────────────────────────────
45
+
10
46
  // args: "<path-to-feature.md>"
11
47
  const featurePath = (typeof args === 'string' ? args : '').trim().split(/\s+/)[0]
12
48
 
49
+ // Derive feature directory and prefix early (needed for ledger writes before metrics agent runs)
50
+ const featureDir = featurePath.replace(/\/[^/]+$/, '')
51
+ const prefixMatch = featureDir.match(/([A-Z]+-\d+)/)
52
+ const prefix = prefixMatch ? prefixMatch[1] : 'FTR-000'
53
+
13
54
  // ── generate-work-breakdown ───────────────────────────────────────────────────
14
55
  phase('Work Breakdown')
15
56
 
16
57
  const tokenLedger = []
17
58
 
18
59
  log(`Running generate-work-breakdown for ${featurePath}`)
60
+ await appendLedgerEntry(featureDir, prefix, {
61
+ agent: 'generate-work-breakdown:phase2',
62
+ phase: 'phase2',
63
+ model: 'haiku',
64
+ status: 'running',
65
+ phase_delta_tokens: 0,
66
+ started_at: '__TS__',
67
+ completed_at: null,
68
+ })
19
69
  const beforeWB = budget.spent()
20
70
  await agent(featurePath, {
21
71
  agentType: 'generate-work-breakdown',
@@ -23,6 +73,11 @@ await agent(featurePath, {
23
73
  phase: 'Work Breakdown',
24
74
  })
25
75
  const wbTokens = budget.spent() - beforeWB
76
+ await updateLedgerEntry(featureDir, prefix, 'generate-work-breakdown:phase2', {
77
+ status: 'done',
78
+ completed_at: '__TS__',
79
+ phase_delta_tokens: wbTokens,
80
+ })
26
81
  tokenLedger.push({ agent: 'generate-work-breakdown', model: 'haiku', phase_delta_tokens: wbTokens })
27
82
  log(`generate-work-breakdown done — phase delta: ${wbTokens} tokens`)
28
83
 
@@ -48,7 +103,34 @@ const PARSE_SCHEMA = {
48
103
  required: ['prefix', 'user_stories', 'total_tasks', 'work_breakdown_path'],
49
104
  }
50
105
 
51
- const phase1Tokens = wbTokens // tokens consumed by this phase so far
106
+ // ── Load token pricing ────────────────────────────────────────────────────────
107
+ // Pricing config is read inline (haiku agent) so it survives path-agnostic installs.
108
+ // Cost formula: tokens * (0.8 * input + 0.2 * output) / 1_000_000 * usd_to_eur
109
+ // (80/20 input/output split — no per-agent breakdown available in the ledger)
110
+ const pricingRaw = await agent(
111
+ `Read the file docs/token-pricing.json and return its raw JSON contents as a string, nothing else.`,
112
+ { label: 'read-pricing', phase: 'Effort Estimate', model: 'haiku' }
113
+ )
114
+ let pricing = null
115
+ try { pricing = JSON.parse(typeof pricingRaw === 'string' ? pricingRaw.trim() : '{}') } catch (_) {}
116
+
117
+ function tokenCostEur(tokens, model) {
118
+ if (!pricing || !pricing.models) return null
119
+ const m = pricing.models[model] || pricing.models['sonnet']
120
+ const usdPer1M = 0.8 * m.input_per_1m_usd + 0.2 * m.output_per_1m_usd
121
+ return (tokens * usdPer1M / 1_000_000) * (pricing.usd_to_eur || 1)
122
+ }
123
+
124
+ function formatEur(val) {
125
+ if (val === null || val === undefined) return '—'
126
+ return '€' + val.toFixed(4)
127
+ }
128
+
129
+ // NOTE: pm-phase2 is a separate workflow and does NOT receive pm-phase1's token ledger,
130
+ // so the real Phase 1 actuals (generate-requirements/tech-spec/validate) are unknown here.
131
+ // They are written as "—" placeholders below and filled later by the orchestrator (the
132
+ // implement-feature skill Step 7, which holds all three phase ledgers). Do NOT reuse wbTokens
133
+ // as a stand-in for Phase 1 — that mislabels Phase 2's tokens as Phase 1's.
52
134
 
53
135
  const metrics = await agent(
54
136
  `You have four tasks:
@@ -113,46 +195,52 @@ Write {feature_dir}/{PREFIX}-Token-Estimate.md with this format:
113
195
 
114
196
  ## Phase 1 — Documentation (Actuals)
115
197
 
116
- | Agent | Task | Model | Tokens Est. | Tokens Actual |
117
- |-------|------|-------|------------|--------------|
118
- | generate-requirements | Generate requirements from feature.md | haiku | — | {phase1_req_tokens} |
119
- | generate-tech-spec | Generate tech spec from feature.md | haiku | — | {phase1_spec_tokens} |
120
- | validate-feature-docs | Validate requirements + tech spec | haiku | — | {phase1_val_tokens} |
121
- | **Phase 1 total** | | | **—** | **${phase1Tokens}** |
198
+ Phase 1 ran in a separate workflow (pm-phase1); its per-agent token actuals are filled in
199
+ by the orchestrator after implementation. Leave the Tokens Actual cells as "—" here.
200
+
201
+ | Agent | Task | Model | Tokens Est. | Est. cost € | Tokens Actual | Actual cost € |
202
+ |-------|------|-------|------------|------------|--------------|--------------|
203
+ | generate-requirements | Generate requirements from feature.md | haiku | — | — | — | — |
204
+ | generate-tech-spec | Generate tech spec from feature.md | haiku | — | — | — | — |
205
+ | validate-feature-docs | Validate requirements + tech spec | haiku | — | — | — | — |
206
+ | **Phase 1 total** | | | **—** | **—** | **—** | **—** |
122
207
 
123
208
  ## Phase 2 — Work Breakdown (Actuals)
124
209
 
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}** |
210
+ | Agent | Task | Model | Tokens Est. | Est. cost € | Tokens Actual | Actual cost € |
211
+ |-------|------|-------|------------|------------|--------------|--------------|
212
+ | generate-work-breakdown | Generate work breakdown from docs | haiku | — | — | {wbTokens from above} | ${formatEur(tokenCostEur(wbTokens, 'haiku'))} |
213
+ | **Phase 2 total** | | | **—** | **—** | **{wbTokens from above}** | **${formatEur(tokenCostEur(wbTokens, 'haiku'))}** |
129
214
 
130
215
  ## Phase 3 — Implementation (Estimates)
131
216
 
132
217
  Estimates based on {total_tasks} tasks ({domain_breakdown}), {user_stories} User Stories.
133
218
  Baseline: ~15,000 tokens/BE task, ~8,000/TEST task, ~5,000/INFRA task.
134
219
 
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}** | **—** |
220
+ | Agent | Task | Model | Tokens Est. | Est. cost € | Tokens Actual | Actual cost € |
221
+ |-------|------|-------|------------|------------|--------------|--------------|
222
+ | developer-backend | Implement BE/INFRA tasks | sonnet | {estimated: be_tasks * 15000 + infra_tasks * 5000} | {cost for that estimate, sonnet} | — | — |
223
+ | developer-testing | Implement TEST tasks | sonnet | {estimated: test_tasks * 8000} | {cost for that estimate, sonnet} | — | — |
224
+ | review-solution (×{user_stories}) | Architect review per US | sonnet | {estimated: user_stories * 8000} | {cost for that estimate, sonnet} | — | — |
225
+ | remediation | Fix review issues | sonnet | ~10,000 | {cost for 10000, sonnet} | — | — |
226
+ | pr-and-registry | Push branch, create PR | sonnet | ~5,000 | {cost for 5000, sonnet} | — | — |
227
+ | write-actuals | Update Token/Effort Estimate | sonnet | ~3,000 | {cost for 3000, sonnet} | — | — |
228
+ | **Phase 3 total** | | | **{sum of phase 3 token estimates}** | **{sum of phase 3 cost estimates}** | **—** | **—** |
229
+
230
+ 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
231
 
145
232
  ## Grand Total
146
233
 
147
- | Phase | Tokens Est. | Tokens Actual |
148
- |-------|------------|--------------|
149
- | Phase 1 — Documentation | — | ${phase1Tokens} |
150
- | Phase 2 — Work Breakdown | — | {wbTokens from above} |
151
- | Phase 3 — Implementation | {sum of phase 3 estimates} | — |
152
- | **Total** | **{sum of phase 3 estimates}** | **{phase1Tokens + wbTokens} (partial)** |
234
+ | Phase | Tokens Est. | Est. cost € | Tokens Actual | Actual cost € |
235
+ |-------|------------|------------|--------------|--------------|
236
+ | Phase 1 — Documentation | — | — | — (filled by orchestrator) | — |
237
+ | Phase 2 — Work Breakdown | — | — | {wbTokens from above} | ${formatEur(tokenCostEur(wbTokens, 'haiku'))} |
238
+ | Phase 3 — Implementation | {sum of phase 3 token estimates} | {sum of phase 3 cost estimates} | — | — |
239
+ | **Total** | **{sum of phase 3 token estimates}** | **{sum of phase 3 cost estimates}** | **{wbTokens} (partial)** | **${formatEur(tokenCostEur(wbTokens, 'haiku'))} (partial)** |
153
240
 
154
241
  ---
155
242
  *Actuals will be appended by pm-phase3 after implementation completes.*
243
+ *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
244
 
157
245
  TASK 4 — Set return values.
158
246
  Set effort_estimate_path to the full path of the written Effort-Estimate.md.