@structured-vibe-coding/core 0.17.2 → 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.
- package/defaults/prices.yaml +9 -0
- package/defaults/search.yaml +18 -0
- package/dist/{agents-RWYBLZQE.js → agents-AKSUXQHN.js} +18 -7
- package/dist/{change-context-W4HDO2IK.js → change-context-REE7UKU7.js} +2 -4
- package/dist/chunk-44LQ5BKZ.js +2628 -0
- package/dist/{chunk-KYWETNFU.js → chunk-4BRO7BOJ.js} +2 -4
- package/dist/chunk-4PIRWYRA.js +1125 -0
- package/dist/{chunk-7JLDL2OK.js → chunk-LHSR7JG2.js} +569 -39
- package/dist/{chunk-EPFURATC.js → chunk-O5SP6X5R.js} +89 -0
- package/dist/{chunk-74OGSVJZ.js → chunk-QGP43XCN.js} +2 -2
- package/dist/{chunk-A752UVPF.js → chunk-QHV5KZKX.js} +193 -28
- package/dist/{gather-BZDW4EN4.js → gather-ZNV2NAUN.js} +3 -4
- package/dist/{greenfield-Z425JOMP.js → greenfield-I5S5OS2D.js} +5 -6
- package/dist/index.d.ts +1034 -40
- package/dist/index.js +2927 -988
- package/dist/{sources-PF45EUH2.js → sources-3TXUPU6V.js} +2 -3
- package/package.json +11 -3
- package/vendor/tree-sitter/NOTICE +29 -0
- package/vendor/tree-sitter/tree-sitter-javascript.wasm +0 -0
- package/vendor/tree-sitter/tree-sitter-python.wasm +0 -0
- package/vendor/tree-sitter/tree-sitter-rust.wasm +0 -0
- package/vendor/tree-sitter/tree-sitter-tsx.wasm +0 -0
- package/vendor/tree-sitter/tree-sitter-typescript.wasm +0 -0
- package/dist/chunk-67AUAGNV.js +0 -150
- package/dist/chunk-7MJYPJMR.js +0 -51
- package/dist/chunk-P3N67UEZ.js +0 -538
- package/dist/chunk-R5WQN4HA.js +0 -139
- package/dist/chunk-WHDG6OTG.js +0 -46
- package/dist/config-X5VDBVR4.js +0 -16
- package/dist/fs-J6DIV6CX.js +0 -28
- 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-
|
|
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-
|
|
35
|
+
} from "./chunk-O5SP6X5R.js";
|
|
39
36
|
|
|
40
37
|
// src/greenfield.ts
|
|
41
|
-
import { join as
|
|
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
|
|
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
|
|
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 =
|
|
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 =
|
|
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
|
|
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-
|
|
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 =
|
|
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 =
|
|
726
|
+
const templatePath = join3(tdir, artifact.template.replace(/^templates\//, ""));
|
|
259
727
|
const candidates = [
|
|
260
|
-
|
|
728
|
+
join3(tdir, artifact.template),
|
|
261
729
|
templatePath,
|
|
262
|
-
|
|
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 =
|
|
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-
|
|
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-
|
|
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-
|
|
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 =
|
|
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(
|
|
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 =
|
|
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 =
|
|
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-
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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(
|
|
1257
|
+
written.push(join4(config.memory_path, to));
|
|
737
1258
|
}
|
|
738
|
-
const indexPath =
|
|
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,
|