@structured-vibe-coding/core 0.17.1 → 0.18.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.
Files changed (31) hide show
  1. package/defaults/prices.yaml +9 -0
  2. package/defaults/search.yaml +18 -0
  3. package/dist/{agents-CLTJJVOD.js → agents-AKSUXQHN.js} +18 -7
  4. package/dist/{change-context-W4HDO2IK.js → change-context-REE7UKU7.js} +2 -4
  5. package/dist/chunk-44LQ5BKZ.js +2628 -0
  6. package/dist/{chunk-KYWETNFU.js → chunk-4BRO7BOJ.js} +2 -4
  7. package/dist/chunk-4PIRWYRA.js +1125 -0
  8. package/dist/{chunk-72QJHR53.js → chunk-LHSR7JG2.js} +569 -39
  9. package/dist/{chunk-EPFURATC.js → chunk-O5SP6X5R.js} +89 -0
  10. package/dist/{chunk-BGM6KS64.js → chunk-QGP43XCN.js} +2 -2
  11. package/dist/{chunk-A752UVPF.js → chunk-QHV5KZKX.js} +193 -28
  12. package/dist/{gather-S4A5YR7X.js → gather-ZNV2NAUN.js} +3 -4
  13. package/dist/{greenfield-QQJH3XL3.js → greenfield-I5S5OS2D.js} +5 -6
  14. package/dist/index.d.ts +1034 -40
  15. package/dist/index.js +2927 -988
  16. package/dist/{sources-PF45EUH2.js → sources-3TXUPU6V.js} +2 -3
  17. package/package.json +11 -3
  18. package/vendor/tree-sitter/NOTICE +29 -0
  19. package/vendor/tree-sitter/tree-sitter-javascript.wasm +0 -0
  20. package/vendor/tree-sitter/tree-sitter-python.wasm +0 -0
  21. package/vendor/tree-sitter/tree-sitter-rust.wasm +0 -0
  22. package/vendor/tree-sitter/tree-sitter-tsx.wasm +0 -0
  23. package/vendor/tree-sitter/tree-sitter-typescript.wasm +0 -0
  24. package/dist/chunk-67AUAGNV.js +0 -150
  25. package/dist/chunk-7MJYPJMR.js +0 -51
  26. package/dist/chunk-BVCTVKBD.js +0 -538
  27. package/dist/chunk-WHDG6OTG.js +0 -46
  28. package/dist/chunk-WPAGUBZ5.js +0 -139
  29. package/dist/config-X5VDBVR4.js +0 -16
  30. package/dist/fs-J6DIV6CX.js +0 -28
  31. package/dist/paths-64GA32IZ.js +0 -28
@@ -1,4 +1,5 @@
1
1
  import {
2
+ ChangeMetaSchema,
2
3
  activeStages,
3
4
  buildContext,
4
5
  changeDirName,
@@ -13,38 +14,118 @@ import {
13
14
  setActiveChange,
14
15
  shouldAutoSkip,
15
16
  uniqueChangeDirName
16
- } from "./chunk-A752UVPF.js";
17
- import {
18
- ChangeMetaSchema
19
- } from "./chunk-67AUAGNV.js";
17
+ } from "./chunk-QHV5KZKX.js";
20
18
  import {
21
19
  archiveDir,
22
20
  changeMetaPath,
23
21
  changePath,
24
22
  changesDir,
25
- memoryDir,
26
- templatesDir
27
- } from "./chunk-7MJYPJMR.js";
28
- import {
29
23
  copyDir,
24
+ displayArchivePath,
30
25
  ensureDir,
31
26
  listDirs,
27
+ memoryDir,
32
28
  pathExists,
33
29
  readText,
34
30
  readYaml,
35
31
  removePath,
32
+ templatesDir,
36
33
  writeText,
37
34
  writeYaml
38
- } from "./chunk-EPFURATC.js";
35
+ } from "./chunk-O5SP6X5R.js";
39
36
 
40
37
  // src/greenfield.ts
41
- import { join as join3 } from "pathe";
38
+ import { join as join4 } from "pathe";
42
39
 
43
40
  // src/artifacts.ts
41
+ var PLACEHOLDER_WORD = /^(?:TBD|TODO|N\/A|NA|none|tbd|todo|n\/a|\.{3}|…|-|—|–|_|\*)$/i;
42
+ function isPlaceholderCell(cell) {
43
+ const text = cell.trim();
44
+ if (!text) return true;
45
+ if (/^\d+$/.test(text)) return true;
46
+ if (PLACEHOLDER_WORD.test(text)) return true;
47
+ if (/^[-–—_.:]+$/.test(text)) return true;
48
+ return false;
49
+ }
50
+ function isTableRow(line) {
51
+ const trimmed = line.trim();
52
+ return trimmed.includes("|") && /^\|.*\|$/.test(trimmed);
53
+ }
54
+ function isSeparatorRow(line) {
55
+ if (!isTableRow(line)) return false;
56
+ const cells = line.trim().replace(/^\|/, "").replace(/\|$/, "").split("|").map((cell) => cell.trim());
57
+ return cells.length > 0 && cells.every((cell) => /^:?-+:?$/.test(cell));
58
+ }
59
+ function tableRowToText(line) {
60
+ const cells = line.trim().replace(/^\|/, "").replace(/\|$/, "").split("|").map((cell) => cell.trim());
61
+ return cells.filter((cell) => !isPlaceholderCell(cell)).join(" ");
62
+ }
63
+ function isInstructionalLine(line) {
64
+ const text = line.replace(/^\s*(?:[-*+]|\d+\.)\s+/, "").replace(/^\[[ xX]\]\s*/, "").replace(/\*\*/g, "").trim();
65
+ if (!text) return true;
66
+ if (PLACEHOLDER_WORD.test(text)) return true;
67
+ if (/^[-–—_.*\s]+$/.test(text)) return true;
68
+ if (/^\{\{[^}]+\}\}$/.test(text)) return true;
69
+ if (/^<[^>\n]+>$/.test(text)) return true;
70
+ if (/^<!--/.test(text)) return true;
71
+ if (/^[^:]{1,48}:\s*$/.test(text)) return true;
72
+ if (/^(?:Q|A):\s*$/i.test(text)) return true;
73
+ if (/^(?:Approve(?: with conditions)?|Reject|Promote to feature change|Abandon|Needs more research)$/i.test(
74
+ text
75
+ )) {
76
+ return true;
77
+ }
78
+ if (/^No blocking unknowns remain\b/i.test(text)) return true;
79
+ if (/\?\s*$/.test(text) && text.length < 140) return true;
80
+ if (/^(?:As a|I want|So that)\b/i.test(text) && /…|\.\.\./.test(text)) return true;
81
+ if (/^sdd (?:skip|backlog)\b/i.test(text)) return true;
82
+ if (/^Optional stage\b/i.test(text)) return true;
83
+ if (/^skip if nothing is ambiguous\b/i.test(text)) return true;
84
+ if (/^Copy the block below\b/i.test(text)) return true;
85
+ if (/^Keep Status\b/i.test(text)) return true;
86
+ if (/^Each feature should fit\b/i.test(text)) return true;
87
+ if (/^Greenfield —/i.test(text)) return true;
88
+ if (/^MVP only\b/i.test(text)) return true;
89
+ if (/^US-\d+:/i.test(text) && /…|\.\.\./.test(text)) return true;
90
+ if (/^F-\d+:\s*</i.test(text)) return true;
91
+ if (/^(?:Status|Priority|Workflow|Summary|Requirements|Notes):\s*$/i.test(text)) return true;
92
+ if (/^(?:Status|Priority|Workflow):\s*(?:planned|must|should|could|feature|hotfix|patch|spike)\s*$/i.test(
93
+ text
94
+ )) {
95
+ return true;
96
+ }
97
+ if (/^Summary:\s*One or two sentences\b/i.test(text)) return true;
98
+ if (/^Requirements:\s*R-\d+\s*$/i.test(text)) return true;
99
+ if (/^One or two sentences\b/i.test(text)) return true;
100
+ if (/^R-\d+$/i.test(text)) return true;
101
+ if (/^(?:planned|must|should|feature)$/i.test(text)) return true;
102
+ if (/^How to write a feature$/i.test(text)) return true;
103
+ if (/^paste local commands$/i.test(text)) return true;
104
+ return false;
105
+ }
44
106
  function isSubstantiveArtifactContent(content) {
45
- const stripped = content.replace(/<!--[\s\S]*?-->/g, "").replace(/^#{1,6}\s+.*$/gm, "").replace(/^>\s.*$/gm, "").replace(/^\s*[-*+]\s*$/gm, "").replace(/^\s*[-*+]\s*\.\.\.\s*$/gm, "").replace(/^\s*[-*+]\s*TBD\s*$/gim, "").replace(/^\s*[-*+]\s*TODO\s*$/gim, "").replace(/^\s*\|.*\|\s*$/gm, "").replace(/`+/g, "").trim();
107
+ const withoutComments = content.replace(/<!--[\s\S]*?-->/g, "");
108
+ const rawLines = withoutComments.split(/\r?\n/);
109
+ const lines = [];
110
+ for (let i = 0; i < rawLines.length; i++) {
111
+ const line = rawLines[i]?.replace(/`+/g, "") ?? "";
112
+ const trimmed = line.trim();
113
+ if (!trimmed) continue;
114
+ if (/^#{1,6}\s+/.test(trimmed)) continue;
115
+ if (/^>\s/.test(trimmed)) continue;
116
+ if (isTableRow(trimmed)) {
117
+ const next = rawLines[i + 1]?.replace(/`+/g, "").trim() ?? "";
118
+ if (isSeparatorRow(trimmed) || isSeparatorRow(next)) continue;
119
+ const kept = tableRowToText(trimmed);
120
+ if (kept) lines.push(kept);
121
+ continue;
122
+ }
123
+ if (isInstructionalLine(line)) continue;
124
+ lines.push(line);
125
+ }
126
+ const stripped = lines.join("\n").trim();
46
127
  if (stripped.length < 48) return false;
47
- const meaningful = stripped.replace(/[-–—_.\s]/g, "").replace(/\b(TBD|TODO|N\/A|none|n\/a)\b/gi, "");
128
+ const meaningful = stripped.replace(/[-–—_.\s|#*`]/g, "").replace(/\{\{[^}]*\}\}/g, "").replace(/\b(TBD|TODO|N\/A|none|n\/a)\b/gi, "");
48
129
  return meaningful.length >= 32;
49
130
  }
50
131
  function incompleteArtifactReason(path, content) {
@@ -68,22 +149,402 @@ function incompleteArtifactReason(path, content) {
68
149
  }
69
150
 
70
151
  // src/change.ts
71
- import { join as join2 } from "pathe";
152
+ import { join as join3 } from "pathe";
153
+
154
+ // src/metrics.ts
155
+ import { createReadStream } from "fs";
156
+ import { stat } from "fs/promises";
157
+ import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "path";
158
+ import { createInterface } from "readline";
159
+ import { z } from "zod";
160
+ var UtcTimestampSchema = z.string().datetime();
161
+ var PhaseMetricSchema = z.object({
162
+ phase: z.string().min(1),
163
+ startedAt: UtcTimestampSchema,
164
+ endedAt: UtcTimestampSchema.optional()
165
+ }).refine((phase) => !phase.endedAt || Date.parse(phase.endedAt) >= Date.parse(phase.startedAt));
166
+ var TokenUsageMetricSchema = z.object({
167
+ provider: z.enum(["claude-code", "codex", "grok"]),
168
+ model: z.string().min(1).nullable().optional(),
169
+ requestId: z.string().min(1),
170
+ phase: z.string().min(1),
171
+ startedAt: UtcTimestampSchema,
172
+ inputTokens: z.number().int().nonnegative().nullable(),
173
+ cacheReadTokens: z.number().int().nonnegative().nullable(),
174
+ cacheWriteTokens: z.number().int().nonnegative().nullable(),
175
+ outputTokens: z.number().int().nonnegative().nullable(),
176
+ measured: z.literal(true)
177
+ });
178
+ var ChangeMetricsSchema = z.object({
179
+ version: z.literal(1),
180
+ phases: z.array(PhaseMetricSchema),
181
+ requests: z.array(TokenUsageMetricSchema)
182
+ });
183
+ var MODEL_ID = /^[A-Za-z0-9._:/-]+$/;
184
+ function metricsPath(projectRoot, config, changeId) {
185
+ return `${changePath(projectRoot, config, changeId)}/metrics.json`;
186
+ }
187
+ function emptyChangeMetrics() {
188
+ return { version: 1, phases: [], requests: [] };
189
+ }
190
+ async function readChangeMetrics(projectRoot, config, changeId) {
191
+ const path = metricsPath(projectRoot, config, changeId);
192
+ if (!await pathExists(path)) return emptyChangeMetrics();
193
+ return ChangeMetricsSchema.parse(JSON.parse(await readText(path)));
194
+ }
195
+ async function writeChangeMetrics(projectRoot, config, changeId, metrics) {
196
+ const path = metricsPath(projectRoot, config, changeId);
197
+ const validated = ChangeMetricsSchema.parse(metrics);
198
+ await writeText(path, `${JSON.stringify(validated, null, 2)}
199
+ `);
200
+ }
201
+ async function startPhase(projectRoot, config, changeId, phase, startedAt = (/* @__PURE__ */ new Date()).toISOString()) {
202
+ if (!config.metrics.enabled) return;
203
+ const metrics = await readChangeMetrics(projectRoot, config, changeId);
204
+ const current = metrics.phases.at(-1);
205
+ if (current && !current.endedAt) {
206
+ throw new Error(`Cannot start phase "${phase}" while "${current.phase}" is still open.`);
207
+ }
208
+ metrics.phases.push({ phase, startedAt });
209
+ await writeChangeMetrics(projectRoot, config, changeId, metrics);
210
+ }
211
+ async function transitionPhase(projectRoot, config, changeId, fromPhase, toPhase, at = (/* @__PURE__ */ new Date()).toISOString(), fromStartedAt = at) {
212
+ if (!config.metrics.enabled || fromPhase === toPhase) return;
213
+ const metrics = await readChangeMetrics(projectRoot, config, changeId);
214
+ const current = metrics.phases.at(-1);
215
+ if (current && !current.endedAt) {
216
+ if (current.phase !== fromPhase) {
217
+ if (current.phase === toPhase) return;
218
+ throw new Error(
219
+ `Cannot leave phase "${fromPhase}" while metrics phase "${current.phase}" is open.`
220
+ );
221
+ }
222
+ current.endedAt = at;
223
+ } else {
224
+ metrics.phases.push({ phase: fromPhase, startedAt: fromStartedAt, endedAt: at });
225
+ }
226
+ if (toPhase) metrics.phases.push({ phase: toPhase, startedAt: at });
227
+ await writeChangeMetrics(projectRoot, config, changeId, metrics);
228
+ }
229
+ function asObject(value) {
230
+ return value && typeof value === "object" && !Array.isArray(value) ? value : null;
231
+ }
232
+ function tokenCount(value) {
233
+ return typeof value === "number" && Number.isSafeInteger(value) && value >= 0 ? value : null;
234
+ }
235
+ function isoTimestamp(value) {
236
+ if (typeof value !== "string" || !Number.isFinite(Date.parse(value))) return null;
237
+ return new Date(value).toISOString();
238
+ }
239
+ function stringId(...values) {
240
+ const value = values.find((item) => typeof item === "string" && item.trim().length > 0);
241
+ return typeof value === "string" ? value : null;
242
+ }
243
+ function parseClaudeUsage(record) {
244
+ const message = asObject(record.message);
245
+ const usage = asObject(message?.usage);
246
+ const requestId = stringId(record.requestId, message?.id);
247
+ const startedAt = isoTimestamp(record.timestamp ?? message?.timestamp);
248
+ if (!usage || !requestId || !startedAt) return null;
249
+ return {
250
+ provider: "claude-code",
251
+ requestId,
252
+ startedAt,
253
+ inputTokens: tokenCount(usage.input_tokens),
254
+ cacheReadTokens: tokenCount(usage.cache_read_input_tokens),
255
+ cacheWriteTokens: tokenCount(usage.cache_creation_input_tokens),
256
+ outputTokens: tokenCount(usage.output_tokens)
257
+ };
258
+ }
259
+ function parseCodexUsage(record, state) {
260
+ const payload = asObject(record.payload);
261
+ const payloadType = payload?.type;
262
+ const timestamp = isoTimestamp(record.timestamp);
263
+ if (record.type === "event_msg" && (payloadType === "task_started" || payloadType === "turn_started")) {
264
+ if (timestamp) state.turnStartedAt = timestamp;
265
+ state.messageId = void 0;
266
+ return null;
267
+ }
268
+ if (record.type === "response_item" && payloadType === "message" && payload?.role === "assistant") {
269
+ state.messageId = stringId(payload.id, record.id) ?? void 0;
270
+ return null;
271
+ }
272
+ if (record.type !== "event_msg" || payloadType !== "token_count") return null;
273
+ const info = asObject(payload?.info);
274
+ const usage = asObject(info?.last_token_usage);
275
+ const totalInput = tokenCount(usage?.input_tokens);
276
+ const cachedInput = tokenCount(usage?.cached_input_tokens);
277
+ const requestId = stringId(payload?.request_id, info?.request_id, state.messageId);
278
+ const startedAt = state.turnStartedAt;
279
+ if (!usage || !requestId || !startedAt) return null;
280
+ state.turnStartedAt = void 0;
281
+ state.messageId = void 0;
282
+ return {
283
+ provider: "codex",
284
+ requestId,
285
+ startedAt,
286
+ inputTokens: totalInput === null ? null : Math.max(0, totalInput - (cachedInput ?? 0)),
287
+ cacheReadTokens: cachedInput,
288
+ cacheWriteTokens: null,
289
+ outputTokens: tokenCount(usage.output_tokens)
290
+ };
291
+ }
292
+ function phaseAt(metrics, timestamp) {
293
+ const requestTime = Date.parse(timestamp);
294
+ return metrics.phases.find((phase) => {
295
+ const start = Date.parse(phase.startedAt);
296
+ const end = phase.endedAt ? Date.parse(phase.endedAt) : Number.POSITIVE_INFINITY;
297
+ return requestTime >= start && requestTime < end;
298
+ });
299
+ }
300
+ function toForwardSlashes(value) {
301
+ let out = "";
302
+ for (const ch of value) out += ch === "\\" ? "/" : ch;
303
+ return out;
304
+ }
305
+ function stripTrailingSlashes(value) {
306
+ let end = value.length;
307
+ while (end > 0 && value.charCodeAt(end - 1) === 47) end -= 1;
308
+ return value.slice(0, end);
309
+ }
310
+ function stripLeadingDotSlash(value) {
311
+ let out = value;
312
+ while (out.startsWith("./")) out = out.slice(2);
313
+ return out;
314
+ }
315
+ function prefixLooksAbsolute(prefix) {
316
+ if (prefix.startsWith("/") || prefix.startsWith("\\")) return true;
317
+ const first = prefix.charCodeAt(0);
318
+ const letter = first >= 65 && first <= 90 || first >= 97 && first <= 122;
319
+ return letter && prefix.charCodeAt(1) === 58;
320
+ }
321
+ function normalizeExcludePrefix(prefix) {
322
+ if (isAbsolute(prefix) || prefixLooksAbsolute(prefix)) {
323
+ throw new Error("metrics.exclude prefix is invalid.");
324
+ }
325
+ const slashed = stripLeadingDotSlash(toForwardSlashes(prefix));
326
+ if (prefixLooksAbsolute(slashed)) throw new Error("metrics.exclude prefix is invalid.");
327
+ const trimmed = stripTrailingSlashes(slashed);
328
+ if (!trimmed) throw new Error("metrics.exclude prefix is invalid.");
329
+ for (const segment of trimmed.split("/")) {
330
+ if (segment === "" || segment === "." || segment === "..") {
331
+ throw new Error("metrics.exclude prefix is invalid.");
332
+ }
333
+ }
334
+ return trimmed;
335
+ }
336
+ function assertSessionLogAllowed(projectRoot, prefixes, sessionLogPath) {
337
+ const normalized = prefixes.map(normalizeExcludePrefix);
338
+ const root = resolve(projectRoot);
339
+ const absLog = resolve(root, sessionLogPath);
340
+ const rel = relative(root, absLog);
341
+ if (rel.startsWith("..") || isAbsolute(rel)) return;
342
+ const relPosix = rel.split(sep).join("/");
343
+ for (const prefix of normalized) {
344
+ if (relPosix === prefix || relPosix.startsWith(`${prefix}/`)) {
345
+ throw new Error(`Refusing to import a session log under excluded prefix "${prefix}".`);
346
+ }
347
+ }
348
+ }
349
+ function recordParsed(metrics, requestIds, result, parsed) {
350
+ const phase = phaseAt(metrics, parsed.startedAt);
351
+ if (!phase) {
352
+ result.unmatched += 1;
353
+ return;
354
+ }
355
+ const key = `${parsed.provider}\0${parsed.requestId}`;
356
+ if (requestIds.has(key)) {
357
+ result.duplicates += 1;
358
+ return;
359
+ }
360
+ requestIds.add(key);
361
+ const request = {
362
+ provider: parsed.provider,
363
+ requestId: parsed.requestId,
364
+ phase: phase.phase,
365
+ startedAt: parsed.startedAt,
366
+ inputTokens: parsed.inputTokens,
367
+ cacheReadTokens: parsed.cacheReadTokens,
368
+ cacheWriteTokens: parsed.cacheWriteTokens,
369
+ outputTokens: parsed.outputTokens,
370
+ measured: true
371
+ };
372
+ if (parsed.model) request.model = parsed.model;
373
+ metrics.requests.push(request);
374
+ result.imported += 1;
375
+ }
376
+ async function readTurnStarts(directory) {
377
+ const starts = /* @__PURE__ */ new Map();
378
+ const path = join(directory, "events.jsonl");
379
+ if (!await pathExists(path)) return starts;
380
+ const lines = createInterface({
381
+ input: createReadStream(path),
382
+ crlfDelay: Number.POSITIVE_INFINITY
383
+ });
384
+ for await (const line of lines) {
385
+ if (!line.trim()) continue;
386
+ let record = null;
387
+ try {
388
+ record = asObject(JSON.parse(line));
389
+ } catch {
390
+ continue;
391
+ }
392
+ if (!record || record.type !== "turn_started") continue;
393
+ const turnNumber = tokenCount(record.turn_number);
394
+ const timestamp = isoTimestamp(record.ts);
395
+ if (turnNumber === null || turnNumber < 1 || !timestamp || starts.has(turnNumber)) continue;
396
+ starts.set(turnNumber, timestamp);
397
+ }
398
+ return starts;
399
+ }
400
+ function grokBucket(value) {
401
+ const bucket = asObject(value);
402
+ if (!bucket) return null;
403
+ const input = tokenCount(bucket.inputTokens);
404
+ const cacheRead = tokenCount(bucket.cachedReadTokens);
405
+ const cacheWrite = tokenCount(bucket.cacheCreationTokens);
406
+ const output = tokenCount(bucket.outputTokens);
407
+ if (input === null || cacheRead === null || cacheWrite === null || output === null) return null;
408
+ return { input, cacheRead, cacheWrite, output };
409
+ }
410
+ async function importGrokUsage(filePath, metrics, requestIds, providers, result) {
411
+ let parsed;
412
+ try {
413
+ parsed = JSON.parse(await readText(filePath));
414
+ } catch {
415
+ throw new Error("usage.json is not a Grok usage file.");
416
+ }
417
+ const document = asObject(parsed);
418
+ const sessionId = document?.sessionId;
419
+ const turns = document?.turns;
420
+ if (!document || typeof sessionId !== "string" || sessionId.length === 0 || !Array.isArray(turns)) {
421
+ throw new Error("usage.json is not a Grok usage file.");
422
+ }
423
+ providers.add("grok");
424
+ const session = asObject(document.session);
425
+ if (session?.usageIsIncomplete === true) result.incomplete = true;
426
+ const turnStarts = await readTurnStarts(dirname(filePath));
427
+ for (const turnValue of turns) {
428
+ const turn = asObject(turnValue);
429
+ if (!turn) {
430
+ result.ignored += 1;
431
+ continue;
432
+ }
433
+ if (turn.usageIsIncomplete === true) result.incomplete = true;
434
+ const turnNumber = tokenCount(turn.turnNumber);
435
+ const modelUsage = asObject(turn.modelUsage);
436
+ const entries = modelUsage ? Object.entries(modelUsage) : [];
437
+ if (turnNumber === null || turnNumber < 1) {
438
+ result.ignored += 1;
439
+ continue;
440
+ }
441
+ const startedAt = turnStarts.get(turnNumber) ?? isoTimestamp(turn.endedAt);
442
+ if (!startedAt) {
443
+ result.ignored += entries.length;
444
+ continue;
445
+ }
446
+ for (const [model, bucketValue] of entries) {
447
+ if (!MODEL_ID.test(model)) {
448
+ result.ignored += 1;
449
+ continue;
450
+ }
451
+ const bucket = grokBucket(bucketValue);
452
+ if (!bucket) {
453
+ result.ignored += 1;
454
+ continue;
455
+ }
456
+ recordParsed(metrics, requestIds, result, {
457
+ provider: "grok",
458
+ model,
459
+ requestId: `${sessionId}:${turnNumber}:${model}`,
460
+ startedAt,
461
+ inputTokens: Math.max(0, bucket.input - bucket.cacheRead - bucket.cacheWrite),
462
+ cacheReadTokens: bucket.cacheRead,
463
+ cacheWriteTokens: bucket.cacheWrite,
464
+ outputTokens: bucket.output
465
+ });
466
+ }
467
+ }
468
+ }
469
+ async function importJsonlUsage(sessionLogPath, metrics, requestIds, providers, result) {
470
+ const codexState = {};
471
+ const lines = createInterface({
472
+ input: createReadStream(sessionLogPath),
473
+ crlfDelay: Number.POSITIVE_INFINITY
474
+ });
475
+ for await (const line of lines) {
476
+ if (!line.trim()) continue;
477
+ let record = null;
478
+ try {
479
+ record = asObject(JSON.parse(line));
480
+ } catch {
481
+ result.ignored += 1;
482
+ continue;
483
+ }
484
+ if (!record) {
485
+ result.ignored += 1;
486
+ continue;
487
+ }
488
+ const message = asObject(record.message);
489
+ const claudeCandidate = record.type === "assistant" && Boolean(asObject(message?.usage));
490
+ const codexCandidate = record.type === "event_msg" && asObject(record.payload)?.type === "token_count";
491
+ const parsed = claudeCandidate ? parseClaudeUsage(record) : parseCodexUsage(record, codexState);
492
+ if (claudeCandidate) providers.add("claude-code");
493
+ else if (codexCandidate) providers.add("codex");
494
+ if (!parsed) {
495
+ if (claudeCandidate || codexCandidate) result.ignored += 1;
496
+ continue;
497
+ }
498
+ recordParsed(metrics, requestIds, result, parsed);
499
+ }
500
+ }
501
+ async function grokUsagePath(sessionLogPath) {
502
+ const info = await stat(sessionLogPath);
503
+ if (info.isDirectory()) {
504
+ const filePath = join(sessionLogPath, "usage.json");
505
+ if (!await pathExists(filePath)) throw new Error("Grok usage.json is missing.");
506
+ return filePath;
507
+ }
508
+ return basename(sessionLogPath) === "usage.json" ? sessionLogPath : null;
509
+ }
510
+ async function importSessionMetrics(projectRoot, config, changeId, sessionLogPath) {
511
+ if (!config.metrics.enabled) throw new Error("Metrics are disabled in .sdd/config.yaml.");
512
+ assertSessionLogAllowed(projectRoot, config.metrics.exclude, sessionLogPath);
513
+ const metrics = await readChangeMetrics(projectRoot, config, changeId);
514
+ const requestIds = new Set(
515
+ metrics.requests.map((request) => `${request.provider}\0${request.requestId}`)
516
+ );
517
+ const providers = /* @__PURE__ */ new Set();
518
+ const result = {
519
+ imported: 0,
520
+ duplicates: 0,
521
+ ignored: 0,
522
+ unmatched: 0,
523
+ providers: [],
524
+ incomplete: false
525
+ };
526
+ const grokPath = await grokUsagePath(sessionLogPath);
527
+ if (grokPath) await importGrokUsage(grokPath, metrics, requestIds, providers, result);
528
+ else await importJsonlUsage(sessionLogPath, metrics, requestIds, providers, result);
529
+ result.providers = [...providers].sort();
530
+ if (result.imported) await writeChangeMetrics(projectRoot, config, changeId, metrics);
531
+ return result;
532
+ }
72
533
 
73
534
  // src/stage-gates.ts
74
- import { join } from "pathe";
535
+ import { join as join2 } from "pathe";
75
536
  async function checkRequiredArtifacts(changePath2, stageId, artifacts) {
76
537
  const errors = [];
77
538
  for (const artifact of artifacts) {
78
539
  if (!artifact.required) continue;
79
540
  if (artifact.path.endsWith("/")) {
80
- const target2 = join(changePath2, artifact.path);
541
+ const target2 = join2(changePath2, artifact.path);
81
542
  if (!await pathExists(target2)) {
82
543
  errors.push(`Missing required directory: ${artifact.path} (stage ${stageId})`);
83
544
  }
84
545
  continue;
85
546
  }
86
- const target = join(changePath2, artifact.path);
547
+ const target = join2(changePath2, artifact.path);
87
548
  if (!await pathExists(target)) {
88
549
  errors.push(incompleteArtifactReason(artifact.path, null));
89
550
  continue;
@@ -146,7 +607,10 @@ async function canLeaveStage(ctx, config) {
146
607
  }
147
608
  const requiredCmds = stage.verify?.commands?.filter((c) => c.required) ?? [];
148
609
  if (requiredCmds.length) {
149
- const verifyOk = ctx.meta.verify_results?.[stage.id]?.ok === true;
610
+ const lastRun = ctx.meta.verify_results?.[stage.id];
611
+ const verifyOk = requiredCmds.every(
612
+ (c) => lastRun?.results?.some((r) => r.name === c.name && r.exitCode === 0)
613
+ );
150
614
  const humanOverride = gateState?.status === "waived" || gateState?.status === "approved";
151
615
  if (!verifyOk && !humanOverride) {
152
616
  errors.push(
@@ -216,6 +680,9 @@ async function createChange(input) {
216
680
  stage: workflow.stages[0].id,
217
681
  domain: input.domain,
218
682
  branch: input.branch,
683
+ epic: input.epic,
684
+ story: input.story,
685
+ depends: input.depends ?? [],
219
686
  flags: input.flags ?? {},
220
687
  overrides: {
221
688
  skip_stages: [],
@@ -232,10 +699,11 @@ async function createChange(input) {
232
699
  throw new Error(`Workflow ${workflowName} has no active stages`);
233
700
  }
234
701
  await writeYaml(changeMetaPath(projectRoot, config, id), meta);
702
+ await startPhase(projectRoot, config, id, startId, meta.created);
235
703
  await materializeStageArtifacts(projectRoot, config, id, workflow, meta, firstStage);
236
704
  await setActiveChange(projectRoot, config, id);
237
705
  try {
238
- const { refreshActiveAgentContext } = await import("./agents-CLTJJVOD.js");
706
+ const { refreshActiveAgentContext } = await import("./agents-AKSUXQHN.js");
239
707
  await refreshActiveAgentContext(projectRoot);
240
708
  } catch {
241
709
  }
@@ -246,7 +714,7 @@ async function materializeStageArtifacts(projectRoot, config, changeId, workflow
246
714
  const dir = changePath(projectRoot, config, changeId);
247
715
  const tdir = templatesDir(projectRoot);
248
716
  for (const artifact of stage.artifacts) {
249
- const target = join2(dir, artifact.path);
717
+ const target = join3(dir, artifact.path);
250
718
  if (await pathExists(target)) continue;
251
719
  if (artifact.path.endsWith("/")) {
252
720
  await ensureDir(target);
@@ -255,11 +723,11 @@ async function materializeStageArtifacts(projectRoot, config, changeId, workflow
255
723
  }
256
724
  let content = defaultArtifactContent(artifact.id, meta, stage, workflow);
257
725
  if (artifact.template) {
258
- const templatePath = join2(tdir, artifact.template.replace(/^templates\//, ""));
726
+ const templatePath = join3(tdir, artifact.template.replace(/^templates\//, ""));
259
727
  const candidates = [
260
- join2(tdir, artifact.template),
728
+ join3(tdir, artifact.template),
261
729
  templatePath,
262
- join2(tdir, `${artifact.id}.md`)
730
+ join3(tdir, `${artifact.id}.md`)
263
731
  ];
264
732
  for (const c of candidates) {
265
733
  if (await pathExists(c)) {
@@ -270,7 +738,7 @@ async function materializeStageArtifacts(projectRoot, config, changeId, workflow
270
738
  }
271
739
  }
272
740
  } else {
273
- const fallback = join2(tdir, `${artifact.id}.md`);
741
+ const fallback = join3(tdir, `${artifact.id}.md`);
274
742
  if (await pathExists(fallback)) {
275
743
  content = interpolate(await readText(fallback), meta, stage, workflow, {
276
744
  idea: String(meta.flags?.idea ?? meta.title ?? "")
@@ -289,7 +757,7 @@ function interpolate(template, meta, stage, workflow, extra) {
289
757
  function defaultArtifactContent(artifactId, meta, stage, workflow) {
290
758
  return `# ${artifactId}
291
759
 
292
- > Change: **${meta.title}** (\`${meta.id}\`)
760
+ > Change: **${meta.title}** (\`${meta.id}\`)
293
761
  > Workflow: \`${workflow.name}\` \xB7 Stage: \`${stage.id}\`
294
762
 
295
763
  ## Summary
@@ -311,6 +779,16 @@ async function advanceStage(projectRoot, config, changeId, opts) {
311
779
  const fallback = await fallbackIncompleteStageId(ctx);
312
780
  if (fallback && fallback !== ctx.meta.stage) {
313
781
  const from2 = ctx.meta.stage;
782
+ const at2 = nowIso();
783
+ await transitionPhase(
784
+ projectRoot,
785
+ config,
786
+ changeId,
787
+ from2,
788
+ fallback,
789
+ at2,
790
+ ctx.meta.updated ?? ctx.meta.created
791
+ );
314
792
  ctx.meta.stage = fallback;
315
793
  await saveChangeMeta(projectRoot, config, ctx.meta);
316
794
  const stage2 = getStage(ctx.workflow, ctx.meta, fallback);
@@ -323,7 +801,7 @@ async function advanceStage(projectRoot, config, changeId, opts) {
323
801
  stage2
324
802
  );
325
803
  try {
326
- const { refreshActiveAgentContext } = await import("./agents-CLTJJVOD.js");
804
+ const { refreshActiveAgentContext } = await import("./agents-AKSUXQHN.js");
327
805
  await refreshActiveAgentContext(projectRoot);
328
806
  } catch {
329
807
  }
@@ -358,6 +836,16 @@ async function advanceStage(projectRoot, config, changeId, opts) {
358
836
  warnings: [...warnings, "No further stages. Run `sdd complete` when ready."]
359
837
  };
360
838
  }
839
+ const at = nowIso();
840
+ await transitionPhase(
841
+ projectRoot,
842
+ config,
843
+ changeId,
844
+ from,
845
+ next,
846
+ at,
847
+ ctx.meta.updated ?? ctx.meta.created
848
+ );
361
849
  ctx.meta.stage = next;
362
850
  await saveChangeMeta(projectRoot, config, ctx.meta);
363
851
  const stage = getStage(ctx.workflow, ctx.meta, next);
@@ -370,7 +858,7 @@ async function advanceStage(projectRoot, config, changeId, opts) {
370
858
  stage
371
859
  );
372
860
  try {
373
- const { refreshActiveAgentContext } = await import("./agents-CLTJJVOD.js");
861
+ const { refreshActiveAgentContext } = await import("./agents-AKSUXQHN.js");
374
862
  await refreshActiveAgentContext(projectRoot);
375
863
  } catch {
376
864
  }
@@ -406,6 +894,16 @@ async function skipStage(projectRoot, config, changeId, stageId, reason) {
406
894
  if (ctx.meta.stage === stageId) {
407
895
  const next = nextStageId(ctx.workflow, ctx.meta);
408
896
  if (next) {
897
+ const at = nowIso();
898
+ await transitionPhase(
899
+ projectRoot,
900
+ config,
901
+ changeId,
902
+ stageId,
903
+ next,
904
+ at,
905
+ ctx.meta.updated ?? ctx.meta.created
906
+ );
409
907
  ctx.meta.stage = next;
410
908
  const nextStage = getStage(ctx.workflow, ctx.meta, next);
411
909
  if (nextStage) {
@@ -431,11 +929,24 @@ async function switchWorkflow(projectRoot, config, changeId, workflowName, reaso
431
929
  `Workflow "${workflowName}" is not in allowed_workflows: ${config.allowed_workflows.join(", ")}`
432
930
  );
433
931
  }
932
+ const previousStage = ctx.meta.stage;
434
933
  ctx.meta.workflow = workflowName;
435
934
  const hasStage = workflow.stages.some((s) => s.id === ctx.meta.stage);
436
935
  if (!hasStage) {
437
936
  ctx.meta.stage = workflow.stages[0].id;
438
937
  }
938
+ if (ctx.meta.stage !== previousStage) {
939
+ const at = nowIso();
940
+ await transitionPhase(
941
+ projectRoot,
942
+ config,
943
+ changeId,
944
+ previousStage,
945
+ ctx.meta.stage,
946
+ at,
947
+ ctx.meta.updated ?? ctx.meta.created
948
+ );
949
+ }
439
950
  if (reason) {
440
951
  ctx.meta.flags = {
441
952
  ...ctx.meta.flags,
@@ -465,13 +976,22 @@ async function completeChange(projectRoot, config, changeId) {
465
976
  if (!check.ok) {
466
977
  throw new Error(check.errors.join("\n"));
467
978
  }
979
+ await transitionPhase(
980
+ projectRoot,
981
+ config,
982
+ changeId,
983
+ ctx.meta.stage,
984
+ null,
985
+ nowIso(),
986
+ ctx.meta.updated ?? ctx.meta.created
987
+ );
468
988
  ctx.meta.status = "completed";
469
989
  ctx.meta.completed_at = nowIso();
470
990
  await saveChangeMeta(projectRoot, config, ctx.meta);
471
991
  let promoted;
472
992
  if (ctx.meta.workflow === "greenfield" || ctx.meta.flags?.greenfield === true) {
473
993
  try {
474
- const { promoteGreenfieldToMemory: promoteGreenfieldToMemory2 } = await import("./greenfield-QQJH3XL3.js");
994
+ const { promoteGreenfieldToMemory: promoteGreenfieldToMemory2 } = await import("./greenfield-I5S5OS2D.js");
475
995
  promoted = await promoteGreenfieldToMemory2(projectRoot, config, changeId);
476
996
  } catch {
477
997
  }
@@ -480,7 +1000,7 @@ async function completeChange(projectRoot, config, changeId) {
480
1000
  let archivedTo = null;
481
1001
  const shouldArchive = config.persistence.archive_on_complete && ctx.workflow.on_complete?.archive !== false;
482
1002
  if (shouldArchive) {
483
- const dest = join2(archiveDir(projectRoot, config), changeId);
1003
+ const dest = join3(archiveDir(projectRoot, config), changeId);
484
1004
  if (await pathExists(dest)) {
485
1005
  throw new Error(`Archive already exists for change: ${changeId}`);
486
1006
  }
@@ -490,13 +1010,14 @@ async function completeChange(projectRoot, config, changeId) {
490
1010
  archivedTo = dest;
491
1011
  }
492
1012
  if (archivedTo) {
493
- const meta = ChangeMetaSchema.parse(await readYaml(join2(archivedTo, "meta.yaml")));
1013
+ const meta = ChangeMetaSchema.parse(await readYaml(join3(archivedTo, "meta.yaml")));
494
1014
  return {
495
1015
  archivedTo,
496
1016
  promoted,
497
1017
  ctx: {
498
1018
  id: changeId,
499
1019
  path: archivedTo,
1020
+ displayPath: displayArchivePath(config, changeId),
500
1021
  meta,
501
1022
  workflow: ctx.workflow,
502
1023
  stages: ctx.stages,
@@ -528,7 +1049,7 @@ async function startGreenfield(input) {
528
1049
  workflowName: "greenfield",
529
1050
  flags: { greenfield: true, idea }
530
1051
  });
531
- const visionPath = join3(ctx.path, "vision.md");
1052
+ const visionPath = join4(ctx.path, "vision.md");
532
1053
  if (await pathExists(visionPath)) {
533
1054
  let body = await readText(visionPath);
534
1055
  if (body.includes("{{idea}}")) {
@@ -597,18 +1118,18 @@ function parseFeaturesBacklog(markdown, sourcePath) {
597
1118
  return features;
598
1119
  }
599
1120
  async function findFeaturesBacklogPath(projectRoot, config) {
600
- const mem = join3(memoryDir(projectRoot, config), "features.md");
1121
+ const mem = join4(memoryDir(projectRoot, config), "features.md");
601
1122
  if (await pathExists(mem)) return mem;
602
- const { getActiveChangeId: getActiveChangeId2 } = await import("./change-context-W4HDO2IK.js");
1123
+ const { getActiveChangeId: getActiveChangeId2 } = await import("./change-context-REE7UKU7.js");
603
1124
  const active = await getActiveChangeId2(projectRoot, config);
604
1125
  if (active) {
605
- const p = join3(changePath(projectRoot, config, active), "features.md");
1126
+ const p = join4(changePath(projectRoot, config, active), "features.md");
606
1127
  if (await pathExists(p)) return p;
607
1128
  }
608
1129
  const ids = (await listDirs(changesDir(projectRoot, config))).sort().reverse();
609
1130
  for (const id of ids) {
610
1131
  try {
611
- const p = join3(changePath(projectRoot, config, id), "features.md");
1132
+ const p = join4(changePath(projectRoot, config, id), "features.md");
612
1133
  if (await pathExists(p)) {
613
1134
  const body = await readText(p);
614
1135
  if (parseFeaturesBacklog(body, p).length) return p;
@@ -663,7 +1184,7 @@ async function startFeatureFromBacklog(input) {
663
1184
  const stage = ctx.workflow.stages.find((s) => s.id === ctx.meta.stage);
664
1185
  const art = stage?.artifacts.find((a) => a.required) ?? stage?.artifacts[0];
665
1186
  if (art) {
666
- const artPath = join3(ctx.path, art.path);
1187
+ const artPath = join4(ctx.path, art.path);
667
1188
  if (await pathExists(artPath)) {
668
1189
  let body = await readText(artPath);
669
1190
  if (!isSubstantiveArtifactContent(body)) {
@@ -719,11 +1240,11 @@ async function promoteGreenfieldToMemory(projectRoot, config, changeId) {
719
1240
  ];
720
1241
  const written = [];
721
1242
  for (const { from, to } of map) {
722
- const src = join3(dir, from);
1243
+ const src = join4(dir, from);
723
1244
  if (!await pathExists(src)) continue;
724
1245
  const body = await readText(src);
725
1246
  if (!isSubstantiveArtifactContent(body)) continue;
726
- const dest = join3(mem, to);
1247
+ const dest = join4(mem, to);
727
1248
  await writeText(
728
1249
  dest,
729
1250
  body.includes("Promoted from greenfield") ? body : `${body.trim()}
@@ -733,9 +1254,9 @@ async function promoteGreenfieldToMemory(projectRoot, config, changeId) {
733
1254
  _Promoted from greenfield change \`${changeId}\`._
734
1255
  `
735
1256
  );
736
- written.push(join3(config.memory_path, to));
1257
+ written.push(join4(config.memory_path, to));
737
1258
  }
738
- const indexPath = join3(mem, "index.md");
1259
+ const indexPath = join4(mem, "index.md");
739
1260
  if (await pathExists(indexPath)) {
740
1261
  let index = await readText(indexPath);
741
1262
  if (!index.includes("features.md")) {
@@ -751,6 +1272,15 @@ _Promoted from greenfield change \`${changeId}\`._
751
1272
  }
752
1273
 
753
1274
  export {
1275
+ PhaseMetricSchema,
1276
+ TokenUsageMetricSchema,
1277
+ ChangeMetricsSchema,
1278
+ emptyChangeMetrics,
1279
+ readChangeMetrics,
1280
+ startPhase,
1281
+ transitionPhase,
1282
+ assertSessionLogAllowed,
1283
+ importSessionMetrics,
754
1284
  isSubstantiveArtifactContent,
755
1285
  incompletePriorStageErrors,
756
1286
  canLeaveStage,