@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
package/dist/index.js
CHANGED
|
@@ -1,22 +1,3 @@
|
|
|
1
|
-
import {
|
|
2
|
-
advanceStage,
|
|
3
|
-
approveGate,
|
|
4
|
-
canLeaveStage,
|
|
5
|
-
completeChange,
|
|
6
|
-
createChange,
|
|
7
|
-
fallbackIncompleteStageId,
|
|
8
|
-
findFeaturesBacklogPath,
|
|
9
|
-
incompletePriorStageErrors,
|
|
10
|
-
isSubstantiveArtifactContent,
|
|
11
|
-
listBacklogFeatures,
|
|
12
|
-
parseFeaturesBacklog,
|
|
13
|
-
promoteGreenfieldToMemory,
|
|
14
|
-
skipStage,
|
|
15
|
-
startFeatureFromBacklog,
|
|
16
|
-
startGreenfield,
|
|
17
|
-
switchWorkflow,
|
|
18
|
-
updateFeatureStatus
|
|
19
|
-
} from "./chunk-7JLDL2OK.js";
|
|
20
1
|
import {
|
|
21
2
|
SDD_CORE_VERSION,
|
|
22
3
|
callMcpSourceTool,
|
|
@@ -30,7 +11,7 @@ import {
|
|
|
30
11
|
mcpTimeoutMs,
|
|
31
12
|
toProcessEnv,
|
|
32
13
|
withMcpSource
|
|
33
|
-
} from "./chunk-
|
|
14
|
+
} from "./chunk-QGP43XCN.js";
|
|
34
15
|
import {
|
|
35
16
|
McpConfigSchema,
|
|
36
17
|
McpInvokeSchema,
|
|
@@ -45,40 +26,142 @@ import {
|
|
|
45
26
|
mcpConfigPath,
|
|
46
27
|
removeMcpSource,
|
|
47
28
|
saveMcpConfig
|
|
48
|
-
} from "./chunk-
|
|
29
|
+
} from "./chunk-4BRO7BOJ.js";
|
|
49
30
|
import {
|
|
50
31
|
AGENT_INTEGRATIONS,
|
|
51
32
|
AGENT_TARGET_OPTIONS,
|
|
52
33
|
ALL_AGENT_TARGETS,
|
|
34
|
+
AgentInstallPreflightError,
|
|
53
35
|
DEFAULT_INIT_INTEGRATION,
|
|
54
36
|
PROTOCOL_MD,
|
|
55
37
|
SDD_AGENT_ROLES,
|
|
56
38
|
agentHostPaths,
|
|
57
39
|
agentKickoffMessage,
|
|
40
|
+
assertAgentInstallable,
|
|
41
|
+
contentHash,
|
|
42
|
+
defaultMcpYamlPath,
|
|
43
|
+
defaultMemoryDir,
|
|
44
|
+
defaultPricesYamlPath,
|
|
45
|
+
defaultSearchYamlPath,
|
|
46
|
+
defaultTemplatesDir,
|
|
47
|
+
defaultWorkflowsDir,
|
|
48
|
+
defaultsRoot,
|
|
49
|
+
describeFsError,
|
|
50
|
+
describeKept,
|
|
51
|
+
generatedState,
|
|
58
52
|
getIntegration,
|
|
59
53
|
installAgentIntegration,
|
|
60
54
|
installAgentIntegrations,
|
|
61
55
|
integrationKeyFor,
|
|
56
|
+
loadGeneratedManifest,
|
|
62
57
|
loadInstalledAgent,
|
|
58
|
+
markGenerated,
|
|
63
59
|
optionForTarget,
|
|
64
60
|
parseAgentTargets,
|
|
65
61
|
parseIntegration,
|
|
62
|
+
planAgentIntegration,
|
|
63
|
+
reconcileGeneratedTree,
|
|
66
64
|
refreshActiveAgentContext,
|
|
65
|
+
refreshConfiguredGuidance,
|
|
67
66
|
removeOtherAgentHosts,
|
|
67
|
+
removeOtherAgentHostsDetailed,
|
|
68
|
+
renderAgentsMd,
|
|
69
|
+
renderMemoryIndex,
|
|
70
|
+
renderProtocolMd,
|
|
71
|
+
renderSddReadme,
|
|
68
72
|
renderThinAgent,
|
|
73
|
+
saveGeneratedManifest,
|
|
69
74
|
writeAgentHandoff
|
|
70
|
-
} from "./chunk-
|
|
75
|
+
} from "./chunk-4PIRWYRA.js";
|
|
71
76
|
import {
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
77
|
+
ChangeMetricsSchema,
|
|
78
|
+
PhaseMetricSchema,
|
|
79
|
+
TokenUsageMetricSchema,
|
|
80
|
+
advanceStage,
|
|
81
|
+
approveGate,
|
|
82
|
+
assertSessionLogAllowed,
|
|
83
|
+
canLeaveStage,
|
|
84
|
+
completeChange,
|
|
85
|
+
createChange,
|
|
86
|
+
emptyChangeMetrics,
|
|
87
|
+
fallbackIncompleteStageId,
|
|
88
|
+
findFeaturesBacklogPath,
|
|
89
|
+
importSessionMetrics,
|
|
90
|
+
incompletePriorStageErrors,
|
|
91
|
+
isSubstantiveArtifactContent,
|
|
92
|
+
listBacklogFeatures,
|
|
93
|
+
parseFeaturesBacklog,
|
|
94
|
+
promoteGreenfieldToMemory,
|
|
95
|
+
readChangeMetrics,
|
|
96
|
+
skipStage,
|
|
97
|
+
startFeatureFromBacklog,
|
|
98
|
+
startGreenfield,
|
|
99
|
+
startPhase,
|
|
100
|
+
switchWorkflow,
|
|
101
|
+
transitionPhase,
|
|
102
|
+
updateFeatureStatus
|
|
103
|
+
} from "./chunk-LHSR7JG2.js";
|
|
75
104
|
import {
|
|
105
|
+
EMBEDDINGS_UNAVAILABLE,
|
|
106
|
+
EMBEDDING_MODEL,
|
|
107
|
+
RRF_K,
|
|
108
|
+
SUMMARY_TOKEN_CAP,
|
|
109
|
+
SearchConfigError,
|
|
110
|
+
budgetForStage,
|
|
111
|
+
buildAgentPrompt,
|
|
112
|
+
buildRetrievedInner,
|
|
113
|
+
chunkCode,
|
|
114
|
+
chunkMarkdown,
|
|
115
|
+
classifyPath,
|
|
116
|
+
clearRegisteredSearchProviders,
|
|
117
|
+
countTokens,
|
|
118
|
+
createBuiltinProvider,
|
|
119
|
+
createFastembedEmbedder,
|
|
76
120
|
defaultConfig,
|
|
121
|
+
embedSearchQuery,
|
|
122
|
+
executeSearch,
|
|
123
|
+
extractiveSummary,
|
|
124
|
+
fallbackWarning,
|
|
125
|
+
fastembedImportCount,
|
|
126
|
+
formatStatus,
|
|
127
|
+
ftsTokens,
|
|
128
|
+
fuseByRank,
|
|
129
|
+
fuseHits,
|
|
130
|
+
getHeadSha,
|
|
131
|
+
isHardDenied,
|
|
132
|
+
isIgnoredPath,
|
|
77
133
|
isInitialized,
|
|
134
|
+
isPrunedDir,
|
|
135
|
+
isSecretPath,
|
|
136
|
+
keywordHits,
|
|
137
|
+
listScopeHits,
|
|
78
138
|
loadConfig,
|
|
79
|
-
|
|
80
|
-
|
|
139
|
+
loadExcluder,
|
|
140
|
+
loadSearchConfig,
|
|
141
|
+
mergeConfigDefaults,
|
|
142
|
+
mergeRankedHits,
|
|
143
|
+
packHits,
|
|
144
|
+
rankScope,
|
|
145
|
+
readPackedCache,
|
|
146
|
+
registerSearchProvider,
|
|
147
|
+
renderBlock,
|
|
148
|
+
resolveActiveDirectory,
|
|
149
|
+
resolveEmbedder,
|
|
150
|
+
resolveSearchProvider,
|
|
151
|
+
rrf,
|
|
152
|
+
saveConfig,
|
|
153
|
+
scopeForPath,
|
|
154
|
+
searchPreamble,
|
|
155
|
+
writePackedCache
|
|
156
|
+
} from "./chunk-44LQ5BKZ.js";
|
|
81
157
|
import {
|
|
158
|
+
ArtifactDefSchema,
|
|
159
|
+
ChangeMetaSchema,
|
|
160
|
+
ConfigSchema,
|
|
161
|
+
GateSchema,
|
|
162
|
+
StageSchema,
|
|
163
|
+
VerifyCommandSchema,
|
|
164
|
+
WorkflowSchema,
|
|
82
165
|
activeStages,
|
|
83
166
|
buildContext,
|
|
84
167
|
changeDirName,
|
|
@@ -103,16 +186,7 @@ import {
|
|
|
103
186
|
slugify,
|
|
104
187
|
todayIsoDate,
|
|
105
188
|
uniqueChangeDirName
|
|
106
|
-
} from "./chunk-
|
|
107
|
-
import {
|
|
108
|
-
ArtifactDefSchema,
|
|
109
|
-
ChangeMetaSchema,
|
|
110
|
-
ConfigSchema,
|
|
111
|
-
GateSchema,
|
|
112
|
-
StageSchema,
|
|
113
|
-
VerifyCommandSchema,
|
|
114
|
-
WorkflowSchema
|
|
115
|
-
} from "./chunk-67AUAGNV.js";
|
|
189
|
+
} from "./chunk-QHV5KZKX.js";
|
|
116
190
|
import {
|
|
117
191
|
SddError,
|
|
118
192
|
errorMessage,
|
|
@@ -125,105 +199,557 @@ import {
|
|
|
125
199
|
changeMetaPath,
|
|
126
200
|
changePath,
|
|
127
201
|
changesDir,
|
|
202
|
+
changesPathRel,
|
|
128
203
|
configPath,
|
|
129
|
-
domainsDir,
|
|
130
|
-
memoryDir,
|
|
131
|
-
sddRoot,
|
|
132
|
-
templatesDir,
|
|
133
|
-
workflowsDir
|
|
134
|
-
} from "./chunk-7MJYPJMR.js";
|
|
135
|
-
import {
|
|
136
204
|
copyDir,
|
|
137
205
|
copyDirSkipExisting,
|
|
206
|
+
displayArchivePath,
|
|
207
|
+
displayChangePath,
|
|
208
|
+
domainsDir,
|
|
138
209
|
ensureDir,
|
|
210
|
+
guidancePaths,
|
|
211
|
+
indexDbPath,
|
|
139
212
|
listDirs,
|
|
140
213
|
listFiles,
|
|
214
|
+
memoryDir,
|
|
215
|
+
memoryPathRel,
|
|
141
216
|
pathExists,
|
|
142
217
|
readText,
|
|
143
218
|
readYaml,
|
|
144
219
|
removePath,
|
|
220
|
+
sddRoot,
|
|
221
|
+
searchConfigPath,
|
|
222
|
+
templatesDir,
|
|
223
|
+
workflowsDir,
|
|
145
224
|
writeText,
|
|
146
225
|
writeTextIfMissing,
|
|
147
226
|
writeYaml
|
|
148
|
-
} from "./chunk-
|
|
149
|
-
|
|
150
|
-
// src/init.ts
|
|
151
|
-
import { join as join2 } from "pathe";
|
|
227
|
+
} from "./chunk-O5SP6X5R.js";
|
|
152
228
|
|
|
153
|
-
// src/
|
|
154
|
-
import {
|
|
155
|
-
import {
|
|
156
|
-
import {
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
229
|
+
// src/report.ts
|
|
230
|
+
import { join } from "pathe";
|
|
231
|
+
import { parse as parseYaml } from "yaml";
|
|
232
|
+
import { z } from "zod";
|
|
233
|
+
var RATE_SCALE = 1000000000n;
|
|
234
|
+
var USD_DENOM = 1000000n * RATE_SCALE;
|
|
235
|
+
var PriceCardSchema = z.object({
|
|
236
|
+
input_per_million_usd: z.number().finite().nonnegative(),
|
|
237
|
+
cache_read_per_million_usd: z.number().finite().nonnegative(),
|
|
238
|
+
cache_write_per_million_usd: z.number().finite().nonnegative(),
|
|
239
|
+
output_per_million_usd: z.number().finite().nonnegative()
|
|
240
|
+
});
|
|
241
|
+
var PricesSchema = z.object({
|
|
242
|
+
usd_to_inr: z.number().finite().nonnegative().default(84),
|
|
243
|
+
models: z.object({
|
|
244
|
+
default: PriceCardSchema
|
|
245
|
+
}).catchall(PriceCardSchema)
|
|
246
|
+
});
|
|
247
|
+
var TOKEN_HEADER = "source phase provider model requests input cache_read cache_write output usd inr";
|
|
248
|
+
function finiteMs(startIso, endMs) {
|
|
249
|
+
const start = Date.parse(startIso);
|
|
250
|
+
if (!Number.isFinite(start) || !Number.isFinite(endMs)) return 0;
|
|
251
|
+
return Math.max(0, endMs - start);
|
|
252
|
+
}
|
|
253
|
+
function formatDuration(ms) {
|
|
254
|
+
if (!Number.isFinite(ms) || ms <= 0) return "0h 0m 0s";
|
|
255
|
+
const totalSeconds = Math.floor(ms / 1e3);
|
|
256
|
+
const hours = Math.floor(totalSeconds / 3600);
|
|
257
|
+
const minutes = Math.floor(totalSeconds % 3600 / 60);
|
|
258
|
+
const seconds = totalSeconds % 60;
|
|
259
|
+
return `${hours}h ${minutes}m ${seconds}s`;
|
|
260
|
+
}
|
|
261
|
+
function phaseTiming(phases, now) {
|
|
262
|
+
const nowMs = now.getTime();
|
|
263
|
+
let activeMs = 0;
|
|
264
|
+
const rows = phases.map((phase) => {
|
|
265
|
+
const endedAt = phase.endedAt;
|
|
266
|
+
const open = endedAt === void 0;
|
|
267
|
+
const active = finiteMs(phase.startedAt, open ? nowMs : Date.parse(endedAt));
|
|
268
|
+
activeMs += active;
|
|
269
|
+
return {
|
|
270
|
+
phase: phase.phase,
|
|
271
|
+
startedAt: phase.startedAt,
|
|
272
|
+
endedAt: phase.endedAt,
|
|
273
|
+
activeMs: active,
|
|
274
|
+
open
|
|
275
|
+
};
|
|
276
|
+
});
|
|
277
|
+
let waitMs = 0;
|
|
278
|
+
for (let i = 0; i < phases.length - 1; i++) {
|
|
279
|
+
const phase = phases[i];
|
|
280
|
+
const next = phases[i + 1];
|
|
281
|
+
if (!phase?.endedAt || !next) continue;
|
|
282
|
+
const gap = Date.parse(next.startedAt) - Date.parse(phase.endedAt);
|
|
283
|
+
if (Number.isFinite(gap)) waitMs += Math.max(0, gap);
|
|
284
|
+
}
|
|
285
|
+
return { activeMs, waitMs, totalMs: activeMs + waitMs, phases: rows };
|
|
286
|
+
}
|
|
287
|
+
function stripLeadingZeros(digits) {
|
|
288
|
+
let i = 0;
|
|
289
|
+
while (i < digits.length - 1 && digits.charCodeAt(i) === 48) i += 1;
|
|
290
|
+
return digits.slice(i);
|
|
291
|
+
}
|
|
292
|
+
function rateUnits(rate) {
|
|
293
|
+
if (!Number.isFinite(rate) || rate < 0) throw new Error("prices.yaml is invalid");
|
|
294
|
+
let text;
|
|
295
|
+
try {
|
|
296
|
+
text = rate.toFixed(9);
|
|
297
|
+
} catch {
|
|
298
|
+
throw new Error("prices.yaml is invalid");
|
|
299
|
+
}
|
|
300
|
+
const negative = text.startsWith("-");
|
|
301
|
+
const unsigned = negative ? text.slice(1) : text;
|
|
302
|
+
const dot = unsigned.indexOf(".");
|
|
303
|
+
const whole = dot === -1 ? unsigned : unsigned.slice(0, dot);
|
|
304
|
+
const frac = dot === -1 ? "" : unsigned.slice(dot + 1, dot + 10);
|
|
305
|
+
const units = BigInt(stripLeadingZeros(`${whole}${frac.padEnd(9, "0")}`));
|
|
306
|
+
return negative ? -units : units;
|
|
307
|
+
}
|
|
308
|
+
function formatRatio(numerator, denominator, decimals) {
|
|
309
|
+
const negative = numerator < 0n;
|
|
310
|
+
const scale = 10n ** BigInt(decimals);
|
|
311
|
+
const num = (negative ? -numerator : numerator) * scale;
|
|
312
|
+
const quot = num / denominator;
|
|
313
|
+
const rem = num % denominator;
|
|
314
|
+
const rounded = rem * 2n >= denominator ? quot + 1n : quot;
|
|
315
|
+
const digits = rounded.toString().padStart(decimals + 1, "0");
|
|
316
|
+
const cut = digits.length - decimals;
|
|
317
|
+
return `${negative ? "-" : ""}${digits.slice(0, cut)}.${digits.slice(cut)}`;
|
|
318
|
+
}
|
|
319
|
+
function costFor(counts, card, usdToInr) {
|
|
320
|
+
const parts = [
|
|
321
|
+
[counts.input, card.input_per_million_usd],
|
|
322
|
+
[counts.cacheRead, card.cache_read_per_million_usd],
|
|
323
|
+
[counts.cacheWrite, card.cache_write_per_million_usd],
|
|
324
|
+
[counts.output, card.output_per_million_usd]
|
|
325
|
+
];
|
|
326
|
+
let numerator = 0n;
|
|
327
|
+
let any = false;
|
|
328
|
+
for (const [tokens, rate] of parts) {
|
|
329
|
+
if (tokens === null) continue;
|
|
330
|
+
any = true;
|
|
331
|
+
numerator += BigInt(tokens) * rateUnits(rate);
|
|
332
|
+
}
|
|
333
|
+
if (!any) return { usd: "n/a", inr: "n/a" };
|
|
334
|
+
const inrNumerator = numerator * rateUnits(usdToInr);
|
|
335
|
+
return {
|
|
336
|
+
usd: formatRatio(numerator, USD_DENOM, 4),
|
|
337
|
+
inr: formatRatio(inrNumerator, USD_DENOM * RATE_SCALE, 2)
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
async function loadPrices(projectRoot) {
|
|
341
|
+
const path = join(sddRoot(projectRoot), "prices.yaml");
|
|
342
|
+
if (!await pathExists(path)) return null;
|
|
343
|
+
try {
|
|
344
|
+
return PricesSchema.parse(parseYaml(await readText(path)));
|
|
345
|
+
} catch {
|
|
346
|
+
throw new Error("prices.yaml is invalid");
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
function cardFor(prices, provider, model) {
|
|
350
|
+
if (model && prices.models[model]) return prices.models[model];
|
|
351
|
+
return prices.models[provider] ?? prices.models.default;
|
|
352
|
+
}
|
|
353
|
+
function modelCell(model) {
|
|
354
|
+
return model ? model : "n/a";
|
|
355
|
+
}
|
|
356
|
+
function emptyTotals() {
|
|
357
|
+
return { requests: 0, input: null, cacheRead: null, cacheWrite: null, output: null };
|
|
358
|
+
}
|
|
359
|
+
function addCount(current, value) {
|
|
360
|
+
if (typeof value !== "number") return current;
|
|
361
|
+
return (current ?? 0) + value;
|
|
362
|
+
}
|
|
363
|
+
function addRequest(totals, request) {
|
|
364
|
+
totals.requests += 1;
|
|
365
|
+
totals.input = addCount(totals.input, request.inputTokens);
|
|
366
|
+
totals.cacheRead = addCount(totals.cacheRead, request.cacheReadTokens);
|
|
367
|
+
totals.cacheWrite = addCount(totals.cacheWrite, request.cacheWriteTokens);
|
|
368
|
+
totals.output = addCount(totals.output, request.outputTokens);
|
|
369
|
+
}
|
|
370
|
+
function cell(value) {
|
|
371
|
+
return value === null ? "n/a" : String(value);
|
|
372
|
+
}
|
|
373
|
+
function tokenLine(source, phase, provider, model, totals, prices, card) {
|
|
374
|
+
const cost = prices && card ? costFor(totals, card, prices.usd_to_inr) : { usd: "n/a", inr: "n/a" };
|
|
375
|
+
return [
|
|
376
|
+
source,
|
|
377
|
+
phase,
|
|
378
|
+
provider,
|
|
379
|
+
model,
|
|
380
|
+
String(totals.requests),
|
|
381
|
+
cell(totals.input),
|
|
382
|
+
cell(totals.cacheRead),
|
|
383
|
+
cell(totals.cacheWrite),
|
|
384
|
+
cell(totals.output),
|
|
385
|
+
cost.usd,
|
|
386
|
+
cost.inr
|
|
387
|
+
].join(" ");
|
|
388
|
+
}
|
|
389
|
+
function groupRequests(metrics) {
|
|
390
|
+
const groups = /* @__PURE__ */ new Map();
|
|
391
|
+
for (const request of metrics.requests) {
|
|
392
|
+
const model = request.model ?? null;
|
|
393
|
+
const key = `${request.phase}\0${request.provider}\0${model ?? ""}`;
|
|
394
|
+
let group = groups.get(key);
|
|
395
|
+
if (!group) {
|
|
396
|
+
group = { phase: request.phase, provider: request.provider, model, totals: emptyTotals() };
|
|
397
|
+
groups.set(key, group);
|
|
398
|
+
}
|
|
399
|
+
addRequest(group.totals, request);
|
|
400
|
+
}
|
|
401
|
+
const phaseOrder = [];
|
|
402
|
+
for (const phase of metrics.phases) {
|
|
403
|
+
if (!phaseOrder.includes(phase.phase)) phaseOrder.push(phase.phase);
|
|
404
|
+
}
|
|
405
|
+
for (const request of metrics.requests) {
|
|
406
|
+
if (!phaseOrder.includes(request.phase)) phaseOrder.push(request.phase);
|
|
407
|
+
}
|
|
408
|
+
return [...groups.values()].sort((left, right) => {
|
|
409
|
+
const phaseDelta = phaseOrder.indexOf(left.phase) - phaseOrder.indexOf(right.phase);
|
|
410
|
+
if (phaseDelta !== 0) return phaseDelta;
|
|
411
|
+
const providerDelta = left.provider.localeCompare(right.provider);
|
|
412
|
+
if (providerDelta !== 0) return providerDelta;
|
|
413
|
+
return (left.model ?? "").localeCompare(right.model ?? "");
|
|
414
|
+
});
|
|
415
|
+
}
|
|
416
|
+
function renderChangeReport(input) {
|
|
417
|
+
const timing = phaseTiming(input.metrics.phases, input.now);
|
|
418
|
+
const lines = [
|
|
419
|
+
`Change: ${input.changeId}`,
|
|
420
|
+
`Active: ${formatDuration(timing.activeMs)}`,
|
|
421
|
+
`Human wait: ${formatDuration(timing.waitMs)}`,
|
|
422
|
+
`Total: ${formatDuration(timing.totalMs)}`,
|
|
423
|
+
"Phases:"
|
|
424
|
+
];
|
|
425
|
+
if (timing.phases.length === 0) {
|
|
426
|
+
lines.push(" (none)");
|
|
427
|
+
} else {
|
|
428
|
+
for (const phase of timing.phases) {
|
|
429
|
+
const start = new Date(phase.startedAt).toLocaleString();
|
|
430
|
+
const end = phase.open ? "in progress" : new Date(phase.endedAt ?? "").toLocaleString();
|
|
431
|
+
lines.push(` ${phase.phase} ${start} \u2192 ${end} active ${formatDuration(phase.activeMs)}`);
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
lines.push("Tokens:");
|
|
435
|
+
lines.push(` ${TOKEN_HEADER}`);
|
|
436
|
+
if (input.metrics.requests.length === 0) {
|
|
437
|
+
if (input.handoffCodePoints === null) {
|
|
438
|
+
lines.push(" (none)");
|
|
439
|
+
lines.push("No estimate: .sdd/handoff.md is missing.");
|
|
440
|
+
} else {
|
|
441
|
+
const estimated = Math.floor(input.handoffCodePoints / 4);
|
|
442
|
+
lines.push(
|
|
443
|
+
` ${tokenLine(
|
|
444
|
+
"estimated (handoff code points / 4)",
|
|
445
|
+
"n/a",
|
|
446
|
+
"default",
|
|
447
|
+
"n/a",
|
|
448
|
+
{
|
|
449
|
+
requests: 1,
|
|
450
|
+
input: estimated,
|
|
451
|
+
cacheRead: null,
|
|
452
|
+
cacheWrite: null,
|
|
453
|
+
output: null
|
|
454
|
+
},
|
|
455
|
+
input.prices,
|
|
456
|
+
input.prices?.models.default ?? null
|
|
457
|
+
)}`
|
|
458
|
+
);
|
|
459
|
+
}
|
|
460
|
+
} else {
|
|
461
|
+
for (const group of groupRequests(input.metrics)) {
|
|
462
|
+
const card = input.prices ? cardFor(input.prices, group.provider, group.model) : null;
|
|
463
|
+
lines.push(
|
|
464
|
+
` ${tokenLine(
|
|
465
|
+
"measured",
|
|
466
|
+
group.phase,
|
|
467
|
+
group.provider,
|
|
468
|
+
modelCell(group.model),
|
|
469
|
+
group.totals,
|
|
470
|
+
input.prices,
|
|
471
|
+
card
|
|
472
|
+
)}`
|
|
473
|
+
);
|
|
474
|
+
}
|
|
475
|
+
const total = emptyTotals();
|
|
476
|
+
for (const request of input.metrics.requests) addRequest(total, request);
|
|
477
|
+
lines.push(` ${totalLine(total, input.metrics, input.prices)}`);
|
|
478
|
+
}
|
|
479
|
+
if (input.prices === null) lines.push("prices.yaml is missing");
|
|
480
|
+
return lines.join("\n");
|
|
481
|
+
}
|
|
482
|
+
function requestUsdNumerator(request, card) {
|
|
483
|
+
const parts = [
|
|
484
|
+
[request.inputTokens, card.input_per_million_usd],
|
|
485
|
+
[request.cacheReadTokens, card.cache_read_per_million_usd],
|
|
486
|
+
[request.cacheWriteTokens, card.cache_write_per_million_usd],
|
|
487
|
+
[request.outputTokens, card.output_per_million_usd]
|
|
166
488
|
];
|
|
167
|
-
|
|
168
|
-
|
|
489
|
+
let numerator = 0n;
|
|
490
|
+
for (const [tokens, rate] of parts) {
|
|
491
|
+
if (tokens === null) continue;
|
|
492
|
+
numerator += BigInt(tokens) * rateUnits(rate);
|
|
493
|
+
}
|
|
494
|
+
return numerator;
|
|
495
|
+
}
|
|
496
|
+
function requestHasNumericToken(request) {
|
|
497
|
+
return request.inputTokens !== null || request.cacheReadTokens !== null || request.cacheWriteTokens !== null || request.outputTokens !== null;
|
|
498
|
+
}
|
|
499
|
+
function totalLine(totals, metrics, prices) {
|
|
500
|
+
if (!prices) return tokenLine("measured", "total", "all", "all", totals, null, null);
|
|
501
|
+
let numerator = 0n;
|
|
502
|
+
let any = false;
|
|
503
|
+
for (const request of metrics.requests) {
|
|
504
|
+
if (!requestHasNumericToken(request)) continue;
|
|
505
|
+
any = true;
|
|
506
|
+
numerator += requestUsdNumerator(request, cardFor(prices, request.provider, request.model));
|
|
507
|
+
}
|
|
508
|
+
const cost = !any ? { usd: "n/a", inr: "n/a" } : {
|
|
509
|
+
usd: formatRatio(numerator, USD_DENOM, 4),
|
|
510
|
+
inr: formatRatio(numerator * rateUnits(prices.usd_to_inr), USD_DENOM * RATE_SCALE, 2)
|
|
511
|
+
};
|
|
512
|
+
return [
|
|
513
|
+
"measured",
|
|
514
|
+
"total",
|
|
515
|
+
"all",
|
|
516
|
+
"all",
|
|
517
|
+
String(totals.requests),
|
|
518
|
+
cell(totals.input),
|
|
519
|
+
cell(totals.cacheRead),
|
|
520
|
+
cell(totals.cacheWrite),
|
|
521
|
+
cell(totals.output),
|
|
522
|
+
cost.usd,
|
|
523
|
+
cost.inr
|
|
524
|
+
].join(" ");
|
|
525
|
+
}
|
|
526
|
+
function compareChangesByFirstPhase(left, right) {
|
|
527
|
+
if (left.invalid !== right.invalid) return left.invalid ? 1 : -1;
|
|
528
|
+
const leftStart = left.invalid ? null : left.firstStartedAt;
|
|
529
|
+
const rightStart = right.invalid ? null : right.firstStartedAt;
|
|
530
|
+
if (leftStart === null !== (rightStart === null)) return leftStart === null ? 1 : -1;
|
|
531
|
+
if (leftStart !== null && rightStart !== null && leftStart !== rightStart) {
|
|
532
|
+
return leftStart < rightStart ? -1 : 1;
|
|
533
|
+
}
|
|
534
|
+
return left.id.localeCompare(right.id);
|
|
535
|
+
}
|
|
536
|
+
async function readHandoffCodePoints(projectRoot) {
|
|
537
|
+
try {
|
|
538
|
+
const text = await readText(join(sddRoot(projectRoot), "handoff.md"));
|
|
539
|
+
return Array.from(text).length;
|
|
540
|
+
} catch (err) {
|
|
541
|
+
const code = typeof err === "object" && err !== null && "code" in err ? err.code : void 0;
|
|
542
|
+
if (code === "ENOENT") return null;
|
|
543
|
+
throw new Error("Could not read .sdd/handoff.md.");
|
|
169
544
|
}
|
|
170
|
-
return candidates[0];
|
|
171
545
|
}
|
|
172
|
-
function
|
|
173
|
-
|
|
546
|
+
async function readReportMetrics(path) {
|
|
547
|
+
if (!await pathExists(path)) return { status: "missing" };
|
|
548
|
+
try {
|
|
549
|
+
return { status: "ok", metrics: ChangeMetricsSchema.parse(JSON.parse(await readText(path))) };
|
|
550
|
+
} catch {
|
|
551
|
+
return { status: "invalid" };
|
|
552
|
+
}
|
|
174
553
|
}
|
|
175
|
-
function
|
|
176
|
-
|
|
554
|
+
async function reportChange(input) {
|
|
555
|
+
const found = await readReportMetrics(
|
|
556
|
+
join(changePath(input.projectRoot, input.config, input.changeId), "metrics.json")
|
|
557
|
+
);
|
|
558
|
+
if (found.status === "invalid") throw new Error("metrics.json is invalid");
|
|
559
|
+
const metrics = found.status === "ok" ? found.metrics : emptyChangeMetrics();
|
|
560
|
+
const handoffCodePoints = metrics.requests.length === 0 ? await readHandoffCodePoints(input.projectRoot) : null;
|
|
561
|
+
return renderChangeReport({
|
|
562
|
+
changeId: input.changeId,
|
|
563
|
+
metrics,
|
|
564
|
+
prices: input.prices,
|
|
565
|
+
handoffCodePoints,
|
|
566
|
+
now: input.now
|
|
567
|
+
});
|
|
177
568
|
}
|
|
178
|
-
function
|
|
179
|
-
|
|
569
|
+
async function reportAllChanges(projectRoot, config, prices, now) {
|
|
570
|
+
const ids = await listDirs(changesDir(projectRoot, config));
|
|
571
|
+
const blocks = [];
|
|
572
|
+
let handoff;
|
|
573
|
+
for (const id of ids) {
|
|
574
|
+
const found = await readReportMetrics(
|
|
575
|
+
join(changePath(projectRoot, config, id), "metrics.json")
|
|
576
|
+
);
|
|
577
|
+
if (found.status === "missing") continue;
|
|
578
|
+
if (found.status === "invalid") {
|
|
579
|
+
blocks.push({
|
|
580
|
+
id,
|
|
581
|
+
invalid: true,
|
|
582
|
+
firstStartedAt: null,
|
|
583
|
+
text: `Change: ${id}
|
|
584
|
+
metrics.json is invalid`
|
|
585
|
+
});
|
|
586
|
+
continue;
|
|
587
|
+
}
|
|
588
|
+
let handoffCodePoints = null;
|
|
589
|
+
if (found.metrics.requests.length === 0) {
|
|
590
|
+
if (handoff === void 0) handoff = await readHandoffCodePoints(projectRoot);
|
|
591
|
+
handoffCodePoints = handoff;
|
|
592
|
+
}
|
|
593
|
+
blocks.push({
|
|
594
|
+
id,
|
|
595
|
+
invalid: false,
|
|
596
|
+
firstStartedAt: found.metrics.phases[0]?.startedAt ?? null,
|
|
597
|
+
text: renderChangeReport({
|
|
598
|
+
changeId: id,
|
|
599
|
+
metrics: found.metrics,
|
|
600
|
+
prices,
|
|
601
|
+
handoffCodePoints,
|
|
602
|
+
now
|
|
603
|
+
})
|
|
604
|
+
});
|
|
605
|
+
}
|
|
606
|
+
if (!blocks.length) return "No change metrics found.";
|
|
607
|
+
blocks.sort(compareChangesByFirstPhase);
|
|
608
|
+
return blocks.map((block) => block.text).join("\n\n");
|
|
180
609
|
}
|
|
181
|
-
|
|
182
|
-
|
|
610
|
+
|
|
611
|
+
// src/sync.ts
|
|
612
|
+
import { join as join2 } from "pathe";
|
|
613
|
+
var VERIFY_MARKER = /<!-- sdd:verify stage="([^"]+)" head_sha="(?:none|[0-9a-f]{40}|[0-9a-f]{64})" result_count="(\d+)" -->/gi;
|
|
614
|
+
async function syncChangeHead(projectRoot, config, changeId, opts) {
|
|
615
|
+
const headSha = opts?.headSha !== void 0 ? opts.headSha : await getHeadSha(projectRoot);
|
|
616
|
+
if (!headSha) throw new Error("Cannot sync: Git HEAD is unavailable in this project.");
|
|
617
|
+
const ctx = await buildContext(projectRoot, config, changeId);
|
|
618
|
+
let updatedResults = 0;
|
|
619
|
+
for (const result of Object.values(ctx.meta.verify_results ?? {})) {
|
|
620
|
+
if (!result.results.length || result.head_sha === headSha) continue;
|
|
621
|
+
result.head_sha = headSha;
|
|
622
|
+
updatedResults += 1;
|
|
623
|
+
}
|
|
624
|
+
if (updatedResults) await saveChangeMeta(projectRoot, config, ctx.meta);
|
|
625
|
+
const reportPath = join2(ctx.path, "local-test-results.md");
|
|
626
|
+
let reportUpdated = false;
|
|
627
|
+
let reportPreserved = false;
|
|
628
|
+
if (await pathExists(reportPath)) {
|
|
629
|
+
const report = await readText(reportPath);
|
|
630
|
+
const markers = [...report.matchAll(VERIFY_MARKER)];
|
|
631
|
+
if (markers.length) {
|
|
632
|
+
const updated = report.replace(
|
|
633
|
+
VERIFY_MARKER,
|
|
634
|
+
(_marker, stage, resultCount) => `<!-- sdd:verify stage="${stage}" head_sha="${headSha}" result_count="${resultCount}" -->`
|
|
635
|
+
);
|
|
636
|
+
if (updated !== report) {
|
|
637
|
+
await writeText(reportPath, updated);
|
|
638
|
+
reportUpdated = true;
|
|
639
|
+
}
|
|
640
|
+
} else {
|
|
641
|
+
reportPreserved = true;
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
return { changeId, headSha, updatedResults, reportUpdated, reportPreserved };
|
|
183
645
|
}
|
|
184
646
|
|
|
185
647
|
// src/init.ts
|
|
648
|
+
import { spawn } from "child_process";
|
|
649
|
+
import { lstat } from "fs/promises";
|
|
650
|
+
import { join as join3 } from "pathe";
|
|
186
651
|
async function initProject(opts) {
|
|
187
652
|
const { projectRoot, force = false } = opts;
|
|
188
653
|
const created = [];
|
|
654
|
+
const kept = [];
|
|
655
|
+
const warnings = [];
|
|
189
656
|
const root = sddRoot(projectRoot);
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
);
|
|
657
|
+
const configFile = join3(root, "config.yaml");
|
|
658
|
+
if (await pathExists(configFile) && !force) {
|
|
659
|
+
await repairRequiredIgnores(projectRoot);
|
|
660
|
+
return { created, config: await loadConfig(projectRoot), kept, warnings };
|
|
661
|
+
}
|
|
662
|
+
try {
|
|
663
|
+
const st = await lstat(root);
|
|
664
|
+
if (!st.isDirectory() && !st.isSymbolicLink()) {
|
|
665
|
+
throw new SddError(`Cannot initialize: ${root} exists and is not a directory. Move it away.`);
|
|
666
|
+
}
|
|
667
|
+
} catch (err) {
|
|
668
|
+
if (err instanceof SddError) throw err;
|
|
669
|
+
}
|
|
670
|
+
const config = await pathExists(configFile) ? mergeConfigDefaults(await readYaml(configFile)) : defaultConfig();
|
|
671
|
+
if (opts.layoutRoot !== void 0) {
|
|
672
|
+
const layoutRoot = validateLayoutRoot(opts.layoutRoot);
|
|
673
|
+
config.memory_path = join3(layoutRoot, "memory");
|
|
674
|
+
config.changes_path = join3(layoutRoot, "changes");
|
|
675
|
+
config.domains_path = join3(layoutRoot, "domains");
|
|
676
|
+
config.archive_path = join3(layoutRoot, "archive");
|
|
677
|
+
}
|
|
678
|
+
const manifestPath = join3(root, ".generated.json");
|
|
679
|
+
const manifestExisted = await pathExists(manifestPath);
|
|
680
|
+
const manifest = await loadGeneratedManifest(manifestPath);
|
|
681
|
+
if (opts.agents) {
|
|
682
|
+
await assertAgentInstallable({ projectRoot, target: opts.agents, force });
|
|
194
683
|
}
|
|
195
|
-
const config = defaultConfig();
|
|
196
684
|
await ensureDir(root);
|
|
197
685
|
await saveConfig(projectRoot, config);
|
|
198
|
-
created.push(
|
|
686
|
+
created.push(join3(root, "config.yaml"));
|
|
199
687
|
const wfDest = workflowsDir(projectRoot);
|
|
200
|
-
await ensureDir(wfDest);
|
|
201
688
|
const wfSrc = defaultWorkflowsDir();
|
|
202
689
|
if (await pathExists(wfSrc)) {
|
|
203
|
-
await
|
|
690
|
+
const result = await reconcileGeneratedTree(
|
|
691
|
+
projectRoot,
|
|
692
|
+
wfSrc,
|
|
693
|
+
wfDest,
|
|
694
|
+
".sdd/workflows",
|
|
695
|
+
manifest,
|
|
696
|
+
force
|
|
697
|
+
);
|
|
698
|
+
kept.push(...result.kept);
|
|
204
699
|
created.push(wfDest);
|
|
700
|
+
} else {
|
|
701
|
+
await ensureDir(wfDest);
|
|
205
702
|
}
|
|
206
703
|
const tDest = templatesDir(projectRoot);
|
|
207
|
-
await ensureDir(tDest);
|
|
208
704
|
const tSrc = defaultTemplatesDir();
|
|
209
705
|
if (await pathExists(tSrc)) {
|
|
210
|
-
await
|
|
706
|
+
const result = await reconcileGeneratedTree(
|
|
707
|
+
projectRoot,
|
|
708
|
+
tSrc,
|
|
709
|
+
tDest,
|
|
710
|
+
".sdd/templates",
|
|
711
|
+
manifest,
|
|
712
|
+
force
|
|
713
|
+
);
|
|
714
|
+
kept.push(...result.kept);
|
|
211
715
|
created.push(tDest);
|
|
716
|
+
} else {
|
|
717
|
+
await ensureDir(tDest);
|
|
212
718
|
}
|
|
719
|
+
await saveGeneratedManifest(manifestPath, manifest);
|
|
720
|
+
if (!manifestExisted) created.push(manifestPath);
|
|
213
721
|
const mcpSrc = defaultMcpYamlPath();
|
|
214
722
|
const mcpDest = mcpConfigPath(projectRoot);
|
|
215
723
|
if (await pathExists(mcpSrc)) {
|
|
216
724
|
const wrote = await writeTextIfMissing(mcpDest, await readText(mcpSrc));
|
|
217
725
|
if (wrote) created.push(mcpDest);
|
|
218
726
|
}
|
|
727
|
+
const pricesSrc = defaultPricesYamlPath();
|
|
728
|
+
const pricesDest = join3(root, "prices.yaml");
|
|
729
|
+
if (await pathExists(pricesSrc)) {
|
|
730
|
+
const wrote = await writeTextIfMissing(pricesDest, await readText(pricesSrc));
|
|
731
|
+
if (wrote) created.push(pricesDest);
|
|
732
|
+
}
|
|
733
|
+
const searchSrc = defaultSearchYamlPath();
|
|
734
|
+
const searchDest = join3(root, "search.yaml");
|
|
735
|
+
if (await pathExists(searchSrc)) {
|
|
736
|
+
const wrote = await writeTextIfMissing(searchDest, await readText(searchSrc));
|
|
737
|
+
if (wrote) created.push(searchDest);
|
|
738
|
+
}
|
|
219
739
|
const mDest = memoryDir(projectRoot, config);
|
|
740
|
+
const existingMemoryFiles = await listRelativeFiles(mDest);
|
|
741
|
+
if (existingMemoryFiles.length && config.memory_path === "memory") {
|
|
742
|
+
warnings.push(
|
|
743
|
+
`Existing ${config.memory_path}/ content will be preserved. Use --root .sdd to keep SDD memory alongside the project without merging folders.`
|
|
744
|
+
);
|
|
745
|
+
}
|
|
220
746
|
await ensureDir(mDest);
|
|
221
747
|
const mSrc = defaultMemoryDir();
|
|
222
748
|
if (await pathExists(mSrc)) {
|
|
223
749
|
await copyDirSkipExisting(mSrc, mDest);
|
|
224
750
|
} else {
|
|
225
751
|
await writeTextIfMissing(
|
|
226
|
-
|
|
752
|
+
join3(mDest, "index.md"),
|
|
227
753
|
`# Documentation map
|
|
228
754
|
|
|
229
755
|
Parent page for **stable** project docs. Start here, then open only what you need.
|
|
@@ -235,15 +761,15 @@ Parent page for **stable** project docs. Start here, then open only what you nee
|
|
|
235
761
|
| Architecture | [architecture.md](architecture.md) |
|
|
236
762
|
| Conventions | [conventions.md](conventions.md) |
|
|
237
763
|
|
|
238
|
-
Active work: \`.sdd/active-context.md\` and
|
|
764
|
+
Active work: \`.sdd/active-context.md\` and the configured changes folder.
|
|
239
765
|
`
|
|
240
766
|
);
|
|
241
767
|
await writeTextIfMissing(
|
|
242
|
-
|
|
768
|
+
join3(mDest, "product.md"),
|
|
243
769
|
"# Product\n\n<!-- What are you building? -->\n"
|
|
244
770
|
);
|
|
245
771
|
await writeTextIfMissing(
|
|
246
|
-
|
|
772
|
+
join3(mDest, "constitution.md"),
|
|
247
773
|
`# Constitution
|
|
248
774
|
|
|
249
775
|
<!-- Non-negotiables agents must not violate. Broader style lives in conventions.md. -->
|
|
@@ -270,21 +796,21 @@ Active work: \`.sdd/active-context.md\` and \`changes/<id>/\`.
|
|
|
270
796
|
`
|
|
271
797
|
);
|
|
272
798
|
await writeTextIfMissing(
|
|
273
|
-
|
|
799
|
+
join3(mDest, "architecture.md"),
|
|
274
800
|
"# Architecture\n\n<!-- High-level system shape -->\n"
|
|
275
801
|
);
|
|
276
802
|
await writeTextIfMissing(
|
|
277
|
-
|
|
803
|
+
join3(mDest, "conventions.md"),
|
|
278
804
|
"# Conventions\n\n<!-- Coding and process conventions -->\n"
|
|
279
805
|
);
|
|
280
806
|
}
|
|
281
|
-
if (!await pathExists(
|
|
282
|
-
const fromDefault =
|
|
807
|
+
if (!await pathExists(join3(mDest, "constitution.md"))) {
|
|
808
|
+
const fromDefault = join3(mSrc, "constitution.md");
|
|
283
809
|
if (await pathExists(fromDefault)) {
|
|
284
810
|
await copyDirSkipExisting(mSrc, mDest);
|
|
285
811
|
} else {
|
|
286
812
|
await writeTextIfMissing(
|
|
287
|
-
|
|
813
|
+
join3(mDest, "constitution.md"),
|
|
288
814
|
`# Constitution
|
|
289
815
|
|
|
290
816
|
<!-- Non-negotiables agents must not violate. Broader style lives in conventions.md. -->
|
|
@@ -317,38 +843,36 @@ Active work: \`.sdd/active-context.md\` and \`changes/<id>/\`.
|
|
|
317
843
|
await ensureDir(domainsDir(projectRoot, config));
|
|
318
844
|
created.push(changesDir(projectRoot, config));
|
|
319
845
|
created.push(domainsDir(projectRoot, config));
|
|
320
|
-
await
|
|
321
|
-
await
|
|
322
|
-
const
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
sdd
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
`
|
|
351
|
-
);
|
|
846
|
+
await writeTextIfMissing(join3(changesDir(projectRoot, config), ".gitkeep"), "");
|
|
847
|
+
await writeTextIfMissing(join3(domainsDir(projectRoot, config), ".gitkeep"), "");
|
|
848
|
+
const sddIgnore = join3(root, ".gitignore");
|
|
849
|
+
const changesIgnore = join3(changesDir(projectRoot, config), ".gitignore");
|
|
850
|
+
if ((await ensureIgnoreEntries(sddIgnore, SDD_IGNORE_LINES)).created) {
|
|
851
|
+
created.push(sddIgnore);
|
|
852
|
+
}
|
|
853
|
+
if ((await ensureIgnoreEntries(changesIgnore, [".active"])).created) {
|
|
854
|
+
created.push(changesIgnore);
|
|
855
|
+
}
|
|
856
|
+
const perMachine = [
|
|
857
|
+
".sdd/active-context.md",
|
|
858
|
+
".sdd/handoff.md",
|
|
859
|
+
".sdd/index.db",
|
|
860
|
+
`${changesPathRel(config)}/.active`
|
|
861
|
+
];
|
|
862
|
+
const tracked = await (opts.indexLookup ?? gitTrackedPaths)(projectRoot, perMachine);
|
|
863
|
+
if (tracked) {
|
|
864
|
+
for (const rel of tracked) {
|
|
865
|
+
warnings.push(
|
|
866
|
+
`Already tracked (gitignore will not hide edits): ${rel}. sdd does not run git rm.`
|
|
867
|
+
);
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
const sddReadme = join3(root, "README.md");
|
|
871
|
+
const readmeState = await pathExists(sddReadme) ? generatedState(await readText(sddReadme)) : null;
|
|
872
|
+
if (readmeState === "edited" || readmeState === "not-generated") {
|
|
873
|
+
kept.push({ path: ".sdd/README.md", reason: readmeState });
|
|
874
|
+
} else if (readmeState === null || force) {
|
|
875
|
+
await writeText(sddReadme, markGenerated(renderSddReadme(config)));
|
|
352
876
|
created.push(sddReadme);
|
|
353
877
|
}
|
|
354
878
|
let agentsResult;
|
|
@@ -359,17 +883,92 @@ sdd complete
|
|
|
359
883
|
force
|
|
360
884
|
});
|
|
361
885
|
created.push(...ag.created);
|
|
362
|
-
|
|
886
|
+
kept.push(...ag.kept);
|
|
887
|
+
agentsResult = { created: ag.created, skipped: ag.skipped, kept: ag.kept, removed: ag.removed };
|
|
888
|
+
}
|
|
889
|
+
return { created, config, kept, warnings, agents: agentsResult };
|
|
890
|
+
}
|
|
891
|
+
var SDD_IGNORE_LINES = [
|
|
892
|
+
"handoff.md",
|
|
893
|
+
"active-context.md",
|
|
894
|
+
"index.db",
|
|
895
|
+
"index.db-journal",
|
|
896
|
+
"index.db-wal",
|
|
897
|
+
"index.db-shm",
|
|
898
|
+
"models/",
|
|
899
|
+
"role-runs/"
|
|
900
|
+
];
|
|
901
|
+
async function repairRequiredIgnores(projectRoot) {
|
|
902
|
+
const config = await loadConfig(projectRoot);
|
|
903
|
+
const root = sddRoot(projectRoot);
|
|
904
|
+
const sdd = await ensureIgnoreEntries(join3(root, ".gitignore"), SDD_IGNORE_LINES);
|
|
905
|
+
const changes = await ensureIgnoreEntries(join3(changesDir(projectRoot, config), ".gitignore"), [
|
|
906
|
+
".active"
|
|
907
|
+
]);
|
|
908
|
+
return sdd.added || changes.added;
|
|
909
|
+
}
|
|
910
|
+
async function ensureIgnoreEntries(filePath, required) {
|
|
911
|
+
const existed = await pathExists(filePath);
|
|
912
|
+
const original = existed ? await readText(filePath) : "";
|
|
913
|
+
const present = new Set(
|
|
914
|
+
original.split(/\r?\n/).map((line) => line.trim()).filter((line) => line.length > 0 && !line.startsWith("#"))
|
|
915
|
+
);
|
|
916
|
+
const missing = required.filter((line) => !present.has(line));
|
|
917
|
+
if (existed && missing.length === 0) return { created: false, added: false };
|
|
918
|
+
let next = original;
|
|
919
|
+
if (next.length > 0 && !next.endsWith("\n")) next += "\n";
|
|
920
|
+
next += missing.map((line) => `${line}
|
|
921
|
+
`).join("");
|
|
922
|
+
await writeText(filePath, next);
|
|
923
|
+
return { created: !existed, added: missing.length > 0 };
|
|
924
|
+
}
|
|
925
|
+
function gitTrackedPaths(projectRoot, relativePaths) {
|
|
926
|
+
return new Promise((resolve) => {
|
|
927
|
+
const child = spawn("git", ["ls-files", "--", ...relativePaths], {
|
|
928
|
+
cwd: projectRoot,
|
|
929
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
930
|
+
});
|
|
931
|
+
let stdout = "";
|
|
932
|
+
child.stdout?.on("data", (chunk) => {
|
|
933
|
+
stdout += chunk.toString();
|
|
934
|
+
});
|
|
935
|
+
child.on("error", () => resolve(null));
|
|
936
|
+
child.on("close", (code) => {
|
|
937
|
+
if (code !== 0) resolve(null);
|
|
938
|
+
else
|
|
939
|
+
resolve(
|
|
940
|
+
stdout.split(/\r?\n/).map((line) => line.trim()).filter(Boolean)
|
|
941
|
+
);
|
|
942
|
+
});
|
|
943
|
+
});
|
|
944
|
+
}
|
|
945
|
+
function validateLayoutRoot(value) {
|
|
946
|
+
const root = value.trim();
|
|
947
|
+
const segments = root.split(/[\\/]+/);
|
|
948
|
+
if (!root || root !== value || root.startsWith("/") || root.startsWith("\\") || /^[A-Za-z]:/.test(root) || segments.some((segment) => segment === ".." || segment === "")) {
|
|
949
|
+
throw new Error("--root must be a non-empty, project-relative path without '..' segments.");
|
|
363
950
|
}
|
|
364
|
-
return
|
|
951
|
+
return segments.join("/");
|
|
952
|
+
}
|
|
953
|
+
async function listRelativeFiles(directory, prefix = "") {
|
|
954
|
+
if (!await pathExists(directory)) return [];
|
|
955
|
+
const files = (await listFiles(directory)).map(
|
|
956
|
+
(name) => [prefix, name].filter(Boolean).join("/")
|
|
957
|
+
);
|
|
958
|
+
const nested = await Promise.all(
|
|
959
|
+
(await listDirs(directory)).map(
|
|
960
|
+
(name) => listRelativeFiles(join3(directory, name), [prefix, name].filter(Boolean).join("/"))
|
|
961
|
+
)
|
|
962
|
+
);
|
|
963
|
+
return [...files, ...nested.flat()];
|
|
365
964
|
}
|
|
366
965
|
|
|
367
966
|
// src/verify.ts
|
|
368
|
-
import { spawn } from "child_process";
|
|
369
|
-
import { join as
|
|
967
|
+
import { spawn as spawn2 } from "child_process";
|
|
968
|
+
import { join as join4 } from "pathe";
|
|
370
969
|
function runCommand(command, cwd) {
|
|
371
970
|
return new Promise((resolve) => {
|
|
372
|
-
const child =
|
|
971
|
+
const child = spawn2(command, {
|
|
373
972
|
cwd,
|
|
374
973
|
shell: true,
|
|
375
974
|
env: process.env
|
|
@@ -392,30 +991,51 @@ function runCommand(command, cwd) {
|
|
|
392
991
|
function isToolGeneratedResults(content) {
|
|
393
992
|
const trimmed = content.trim();
|
|
394
993
|
if (!trimmed) return true;
|
|
395
|
-
if (trimmed.includes("{{")
|
|
396
|
-
if (trimmed.startsWith("# Local verify results"))
|
|
994
|
+
if (trimmed.includes("{{")) return true;
|
|
995
|
+
if (trimmed.startsWith("# Local verify results")) {
|
|
996
|
+
return false;
|
|
997
|
+
}
|
|
998
|
+
if (trimmed.startsWith("<!-- sdd:verify-report:start -->")) {
|
|
999
|
+
return trimmed.includes("<!-- sdd:verify-report:end -->") && /<!-- sdd:verify stage="[^"]+" head_sha="(?:none|[0-9a-f]{40}|[0-9a-f]{64})" result_count="\d+" -->/i.test(
|
|
1000
|
+
trimmed
|
|
1001
|
+
);
|
|
1002
|
+
}
|
|
1003
|
+
if (trimmed.startsWith("# Local test results")) {
|
|
1004
|
+
const body = trimmed.replace(/^# Local test results\s*/, "");
|
|
1005
|
+
const filled = !body.includes("{{") && !body.includes("| 1 | PASS / FAIL |") && !/\bTODO\b/.test(body) && body.length > 80;
|
|
1006
|
+
return !filled;
|
|
1007
|
+
}
|
|
1008
|
+
if (/\bTODO\b/.test(trimmed)) return true;
|
|
397
1009
|
return false;
|
|
398
1010
|
}
|
|
399
|
-
|
|
1011
|
+
var STATUS_LABEL = {
|
|
1012
|
+
pass: "PASS",
|
|
1013
|
+
fail: "FAIL",
|
|
1014
|
+
not_run: "NOT RUN"
|
|
1015
|
+
};
|
|
1016
|
+
function buildSummaryLines(title, stageId, headSha, status, reason, results, checklist) {
|
|
400
1017
|
const lines = [
|
|
1018
|
+
`<!-- sdd:verify-report:start -->`,
|
|
1019
|
+
``,
|
|
401
1020
|
`# Local verify results`,
|
|
402
1021
|
``,
|
|
403
1022
|
`> Change: ${title}`,
|
|
404
1023
|
`> Stage: ${stageId}`,
|
|
1024
|
+
`<!-- sdd:verify stage="${stageId}" head_sha="${headSha ?? "none"}" result_count="${results.length}" -->`,
|
|
405
1025
|
`> At: ${nowIso()}`,
|
|
406
|
-
`> Overall: ${
|
|
1026
|
+
`> Overall: ${STATUS_LABEL[status]}${reason ? ` \u2014 ${reason}` : ""}`,
|
|
407
1027
|
``,
|
|
408
1028
|
`## Commands`,
|
|
409
1029
|
``
|
|
410
1030
|
];
|
|
411
1031
|
if (results.length === 0) {
|
|
412
|
-
lines.push(`_No commands
|
|
1032
|
+
lines.push(`_No commands ran. Complete the checklist manually._`);
|
|
413
1033
|
lines.push("");
|
|
414
1034
|
} else {
|
|
415
1035
|
for (const r of results) {
|
|
416
1036
|
lines.push(`### ${r.name}`);
|
|
417
1037
|
lines.push(`- Command: \`${r.command}\``);
|
|
418
|
-
lines.push(`- Exit: ${r.exitCode}`);
|
|
1038
|
+
lines.push(`- Exit: ${r.exitCode}${r.exitCode === 0 ? "" : " (FAILED)"}`);
|
|
419
1039
|
lines.push("");
|
|
420
1040
|
}
|
|
421
1041
|
}
|
|
@@ -429,6 +1049,8 @@ function buildSummaryLines(title, stageId, ok, results, checklist) {
|
|
|
429
1049
|
lines.push("");
|
|
430
1050
|
lines.push(`-`);
|
|
431
1051
|
lines.push("");
|
|
1052
|
+
lines.push(`<!-- sdd:verify-report:end -->`);
|
|
1053
|
+
lines.push("");
|
|
432
1054
|
return lines;
|
|
433
1055
|
}
|
|
434
1056
|
async function runLocalVerify(projectRoot, config, changeId, opts) {
|
|
@@ -437,8 +1059,73 @@ async function runLocalVerify(projectRoot, config, changeId, opts) {
|
|
|
437
1059
|
if (!stage) {
|
|
438
1060
|
throw new Error(`Unknown stage: ${ctx.meta.stage}`);
|
|
439
1061
|
}
|
|
1062
|
+
if (!stage.verify) {
|
|
1063
|
+
const verifyStage = ctx.workflow.stages.find((st) => st.verify);
|
|
1064
|
+
return {
|
|
1065
|
+
stageId: stage.id,
|
|
1066
|
+
status: "not_run",
|
|
1067
|
+
reason: verifyStage ? `stage "${stage.id}" has no verify step; run sdd verify on "${verifyStage.id}"` : `workflow "${ctx.workflow.name}" has no verify step`,
|
|
1068
|
+
results: [],
|
|
1069
|
+
checklist: [],
|
|
1070
|
+
evidencePath: null,
|
|
1071
|
+
resultsPath: null,
|
|
1072
|
+
ok: false
|
|
1073
|
+
};
|
|
1074
|
+
}
|
|
1075
|
+
const headSha = opts?.headSha !== void 0 ? opts.headSha : await getHeadSha(projectRoot);
|
|
1076
|
+
const anyRecordedSha = Object.entries(ctx.meta.verify_results ?? {}).find(
|
|
1077
|
+
([, recorded]) => recorded.head_sha
|
|
1078
|
+
);
|
|
1079
|
+
if (anyRecordedSha && !headSha) {
|
|
1080
|
+
throw new Error(
|
|
1081
|
+
`Cannot validate recorded SHA in ${join4(ctx.path, "meta.yaml")} at verify_results.${anyRecordedSha[0]}.head_sha because Git HEAD is unavailable.`
|
|
1082
|
+
);
|
|
1083
|
+
}
|
|
1084
|
+
for (const [resultStage, recorded] of Object.entries(ctx.meta.verify_results ?? {})) {
|
|
1085
|
+
if (recorded.head_sha && headSha && recorded.head_sha !== headSha) {
|
|
1086
|
+
throw new Error(
|
|
1087
|
+
`Stale SHA in ${join4(ctx.path, "meta.yaml")} at verify_results.${resultStage}.head_sha: recorded ${recorded.head_sha}, current HEAD ${headSha}. Run sdd sync ${changeId}.`
|
|
1088
|
+
);
|
|
1089
|
+
}
|
|
1090
|
+
}
|
|
1091
|
+
const summaryPath = join4(ctx.path, "local-test-results.md");
|
|
1092
|
+
if (await pathExists(summaryPath)) {
|
|
1093
|
+
const existing = await readText(summaryPath);
|
|
1094
|
+
const markerPattern = /<!-- sdd:verify stage="([^"]+)" head_sha="(none|[0-9a-f]{40}|[0-9a-f]{64})" result_count="(\d+)" -->/gi;
|
|
1095
|
+
for (const marker of existing.matchAll(markerPattern)) {
|
|
1096
|
+
const line = existing.slice(0, marker.index).split("\n").length;
|
|
1097
|
+
const reportStage = marker[1];
|
|
1098
|
+
const reportSha = marker[2].toLowerCase();
|
|
1099
|
+
const reportResult = ctx.meta.verify_results?.[reportStage];
|
|
1100
|
+
if (!reportResult) {
|
|
1101
|
+
throw new Error(
|
|
1102
|
+
`Missing verification metadata in ${join4(ctx.path, "meta.yaml")} for ${summaryPath}:${line} (${reportStage}).`
|
|
1103
|
+
);
|
|
1104
|
+
}
|
|
1105
|
+
if (reportSha !== "none" && !headSha) {
|
|
1106
|
+
throw new Error(
|
|
1107
|
+
`Cannot validate SHA in ${summaryPath}:${line} because Git HEAD is unavailable.`
|
|
1108
|
+
);
|
|
1109
|
+
}
|
|
1110
|
+
if (headSha && reportSha !== "none" && reportSha !== headSha) {
|
|
1111
|
+
throw new Error(
|
|
1112
|
+
`Stale SHA in ${summaryPath}:${line}: recorded ${reportSha}, current HEAD ${headSha}. Run sdd sync ${changeId}.`
|
|
1113
|
+
);
|
|
1114
|
+
}
|
|
1115
|
+
if (reportResult && Number(marker[3]) !== reportResult.results.length) {
|
|
1116
|
+
throw new Error(
|
|
1117
|
+
`Recorded result count mismatch in ${summaryPath}:${line}: report has ${marker[3]}, metadata has ${reportResult.results.length}.`
|
|
1118
|
+
);
|
|
1119
|
+
}
|
|
1120
|
+
if (reportResult?.head_sha && reportSha !== reportResult.head_sha) {
|
|
1121
|
+
throw new Error(
|
|
1122
|
+
`SHA mismatch between ${join4(ctx.path, "meta.yaml")} and ${summaryPath}:${line}.`
|
|
1123
|
+
);
|
|
1124
|
+
}
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
440
1127
|
const evidenceDirRaw = stage.verify?.evidence_dir?.trim();
|
|
441
|
-
const evidenceDir = evidenceDirRaw && evidenceDirRaw.length > 0 ?
|
|
1128
|
+
const evidenceDir = evidenceDirRaw && evidenceDirRaw.length > 0 ? join4(ctx.path, evidenceDirRaw) : null;
|
|
442
1129
|
if (evidenceDir) {
|
|
443
1130
|
await ensureDir(evidenceDir);
|
|
444
1131
|
}
|
|
@@ -455,7 +1142,7 @@ async function runLocalVerify(projectRoot, config, changeId, opts) {
|
|
|
455
1142
|
output: output.slice(0, 5e4)
|
|
456
1143
|
});
|
|
457
1144
|
if (evidenceDir) {
|
|
458
|
-
const logPath =
|
|
1145
|
+
const logPath = join4(evidenceDir, `${cmd.name.replace(/\s+/g, "-")}.log`);
|
|
459
1146
|
await writeText(logPath, `# ${cmd.name}
|
|
460
1147
|
$ ${cmd.run}
|
|
461
1148
|
exit: ${exitCode}
|
|
@@ -464,28 +1151,36 @@ ${output}`);
|
|
|
464
1151
|
}
|
|
465
1152
|
}
|
|
466
1153
|
}
|
|
467
|
-
|
|
468
|
-
let
|
|
469
|
-
if (
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
ok = results.every((r) => r.exitCode === 0);
|
|
1154
|
+
let status;
|
|
1155
|
+
let reason;
|
|
1156
|
+
if (!commands.length) {
|
|
1157
|
+
status = "not_run";
|
|
1158
|
+
reason = `no verify commands configured for stage "${stage.id}"`;
|
|
1159
|
+
} else if (!runCommands) {
|
|
1160
|
+
status = "not_run";
|
|
1161
|
+
reason = "commands not run (--no-run)";
|
|
1162
|
+
} else {
|
|
1163
|
+
const failed = results.filter((r) => r.exitCode !== 0).map((r) => r.name);
|
|
1164
|
+
status = failed.length ? "fail" : "pass";
|
|
1165
|
+
reason = failed.length ? `failed: ${failed.join(", ")}` : void 0;
|
|
480
1166
|
}
|
|
1167
|
+
const ok4 = status === "pass";
|
|
481
1168
|
const checklist = stage.gate?.checklist ?? [];
|
|
482
|
-
const
|
|
483
|
-
|
|
1169
|
+
const lines = buildSummaryLines(
|
|
1170
|
+
ctx.meta.title,
|
|
1171
|
+
stage.id,
|
|
1172
|
+
results.length ? headSha : null,
|
|
1173
|
+
status,
|
|
1174
|
+
reason,
|
|
1175
|
+
results,
|
|
1176
|
+
checklist
|
|
1177
|
+
);
|
|
484
1178
|
const summaryBody = lines.join("\n");
|
|
485
1179
|
if (await pathExists(summaryPath)) {
|
|
486
1180
|
const existing = await readText(summaryPath);
|
|
487
1181
|
if (isToolGeneratedResults(existing)) {
|
|
488
1182
|
await writeText(summaryPath, summaryBody);
|
|
1183
|
+
} else if (results.length === 0) {
|
|
489
1184
|
} else {
|
|
490
1185
|
const stamp = nowIso();
|
|
491
1186
|
const append = [
|
|
@@ -494,12 +1189,13 @@ ${output}`);
|
|
|
494
1189
|
``,
|
|
495
1190
|
`## Latest run (${stamp})`,
|
|
496
1191
|
``,
|
|
497
|
-
`> Overall: ${
|
|
1192
|
+
`> Overall: ${STATUS_LABEL[status]}${reason ? ` \u2014 ${reason}` : ""}`,
|
|
498
1193
|
`> Stage: ${stage.id}`,
|
|
1194
|
+
`<!-- sdd:verify stage="${stage.id}" head_sha="${results.length ? headSha ?? "none" : "none"}" result_count="${results.length}" -->`,
|
|
499
1195
|
``
|
|
500
1196
|
];
|
|
501
1197
|
if (results.length === 0) {
|
|
502
|
-
append.push(`_No commands
|
|
1198
|
+
append.push(`_No commands ran._`);
|
|
503
1199
|
append.push("");
|
|
504
1200
|
} else {
|
|
505
1201
|
for (const r of results) {
|
|
@@ -511,7 +1207,7 @@ ${output}`);
|
|
|
511
1207
|
}
|
|
512
1208
|
if (evidenceDir) {
|
|
513
1209
|
const stamp = nowIso();
|
|
514
|
-
await writeText(
|
|
1210
|
+
await writeText(join4(evidenceDir, `run-${stamp.replace(/[:.]/g, "-")}.md`), summaryBody);
|
|
515
1211
|
}
|
|
516
1212
|
} else {
|
|
517
1213
|
await writeText(summaryPath, summaryBody);
|
|
@@ -519,7 +1215,9 @@ ${output}`);
|
|
|
519
1215
|
ctx.meta.verify_results = {
|
|
520
1216
|
...ctx.meta.verify_results,
|
|
521
1217
|
[stage.id]: {
|
|
522
|
-
ok,
|
|
1218
|
+
ok: ok4,
|
|
1219
|
+
status,
|
|
1220
|
+
...results.length && headSha ? { head_sha: headSha } : {},
|
|
523
1221
|
at: nowIso(),
|
|
524
1222
|
results: results.map((r) => ({ name: r.name, exitCode: r.exitCode }))
|
|
525
1223
|
}
|
|
@@ -527,304 +1225,23 @@ ${output}`);
|
|
|
527
1225
|
await saveChangeMeta(projectRoot, config, ctx.meta);
|
|
528
1226
|
return {
|
|
529
1227
|
stageId: stage.id,
|
|
1228
|
+
status,
|
|
1229
|
+
reason,
|
|
530
1230
|
results,
|
|
531
1231
|
checklist,
|
|
532
1232
|
evidencePath: evidenceDir,
|
|
533
|
-
|
|
1233
|
+
resultsPath: summaryPath,
|
|
1234
|
+
ok: ok4
|
|
534
1235
|
};
|
|
535
1236
|
}
|
|
536
1237
|
|
|
537
1238
|
// src/code-context/index.ts
|
|
538
|
-
import {
|
|
539
|
-
|
|
540
|
-
// src/code-context/adapters/registry.ts
|
|
541
|
-
import { extname as extname2 } from "pathe";
|
|
542
|
-
|
|
543
|
-
// src/code-context/adapters/typescript.ts
|
|
544
|
-
import { extname } from "pathe";
|
|
545
|
-
import ts from "typescript";
|
|
546
|
-
var TS_JS_EXTENSIONS = [
|
|
547
|
-
".ts",
|
|
548
|
-
".tsx",
|
|
549
|
-
".js",
|
|
550
|
-
".jsx",
|
|
551
|
-
".mts",
|
|
552
|
-
".cts",
|
|
553
|
-
".mjs",
|
|
554
|
-
".cjs"
|
|
555
|
-
];
|
|
556
|
-
function scriptKindForPath(filePath) {
|
|
557
|
-
const ext = extname(filePath).toLowerCase();
|
|
558
|
-
switch (ext) {
|
|
559
|
-
case ".tsx":
|
|
560
|
-
return ts.ScriptKind.TSX;
|
|
561
|
-
case ".jsx":
|
|
562
|
-
return ts.ScriptKind.JSX;
|
|
563
|
-
case ".js":
|
|
564
|
-
case ".mjs":
|
|
565
|
-
case ".cjs":
|
|
566
|
-
return ts.ScriptKind.JS;
|
|
567
|
-
case ".cts":
|
|
568
|
-
case ".mts":
|
|
569
|
-
case ".ts":
|
|
570
|
-
default:
|
|
571
|
-
return ts.ScriptKind.TS;
|
|
572
|
-
}
|
|
573
|
-
}
|
|
574
|
-
function lineOf(sf, pos) {
|
|
575
|
-
return sf.getLineAndCharacterOfPosition(pos).line + 1;
|
|
576
|
-
}
|
|
577
|
-
function hasExportModifier(node) {
|
|
578
|
-
const mods = ts.canHaveModifiers(node) ? ts.getModifiers(node) : void 0;
|
|
579
|
-
return Boolean(mods?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword));
|
|
580
|
-
}
|
|
581
|
-
function isTopLevel(node, sf) {
|
|
582
|
-
let p = node.parent;
|
|
583
|
-
while (p && p !== sf) {
|
|
584
|
-
if (ts.isFunctionDeclaration(p) || ts.isFunctionExpression(p) || ts.isArrowFunction(p) || ts.isMethodDeclaration(p) || ts.isConstructorDeclaration(p) || ts.isGetAccessorDeclaration(p) || ts.isSetAccessorDeclaration(p)) {
|
|
585
|
-
return false;
|
|
586
|
-
}
|
|
587
|
-
if (ts.isClassDeclaration(p) || ts.isClassExpression(p)) {
|
|
588
|
-
return false;
|
|
589
|
-
}
|
|
590
|
-
p = p.parent;
|
|
591
|
-
}
|
|
592
|
-
return true;
|
|
593
|
-
}
|
|
594
|
-
function extractFromSource(filePath, source, repoRelativePath) {
|
|
595
|
-
const sf = ts.createSourceFile(
|
|
596
|
-
filePath,
|
|
597
|
-
source,
|
|
598
|
-
ts.ScriptTarget.Latest,
|
|
599
|
-
/*setParentNodes*/
|
|
600
|
-
true,
|
|
601
|
-
scriptKindForPath(filePath)
|
|
602
|
-
);
|
|
603
|
-
const symbols = [];
|
|
604
|
-
const imports = [];
|
|
605
|
-
const exports = [];
|
|
606
|
-
const references = [];
|
|
607
|
-
const pushSymbol = (name, node, kind, exported) => {
|
|
608
|
-
if (!name) return;
|
|
609
|
-
symbols.push({
|
|
610
|
-
name,
|
|
611
|
-
kind,
|
|
612
|
-
filePath: repoRelativePath,
|
|
613
|
-
startLine: lineOf(sf, node.getStart(sf, false)),
|
|
614
|
-
endLine: lineOf(sf, node.getEnd()),
|
|
615
|
-
exported
|
|
616
|
-
});
|
|
617
|
-
if (exported) {
|
|
618
|
-
exports.push({ name, kind });
|
|
619
|
-
}
|
|
620
|
-
};
|
|
621
|
-
const visit = (node) => {
|
|
622
|
-
if (ts.isImportDeclaration(node)) {
|
|
623
|
-
const spec = node.moduleSpecifier;
|
|
624
|
-
if (ts.isStringLiteral(spec)) {
|
|
625
|
-
const names = [];
|
|
626
|
-
const clause = node.importClause;
|
|
627
|
-
if (clause?.name) names.push(clause.name.text);
|
|
628
|
-
if (clause?.namedBindings) {
|
|
629
|
-
if (ts.isNamespaceImport(clause.namedBindings)) {
|
|
630
|
-
names.push(clause.namedBindings.name.text);
|
|
631
|
-
} else if (ts.isNamedImports(clause.namedBindings)) {
|
|
632
|
-
for (const el of clause.namedBindings.elements) {
|
|
633
|
-
names.push(el.name.text);
|
|
634
|
-
}
|
|
635
|
-
}
|
|
636
|
-
}
|
|
637
|
-
imports.push({ from: spec.text, names: names.length ? names : void 0 });
|
|
638
|
-
}
|
|
639
|
-
}
|
|
640
|
-
if (ts.isExportDeclaration(node) && node.moduleSpecifier && ts.isStringLiteral(node.moduleSpecifier)) {
|
|
641
|
-
imports.push({ from: node.moduleSpecifier.text, names: ["*"] });
|
|
642
|
-
}
|
|
643
|
-
if (ts.isFunctionDeclaration(node) && node.name && isTopLevel(node, sf)) {
|
|
644
|
-
pushSymbol(node.name.text, node, "function", hasExportModifier(node));
|
|
645
|
-
}
|
|
646
|
-
if (ts.isClassDeclaration(node) && node.name && isTopLevel(node, sf)) {
|
|
647
|
-
pushSymbol(node.name.text, node, "class", hasExportModifier(node));
|
|
648
|
-
}
|
|
649
|
-
if (ts.isInterfaceDeclaration(node) && isTopLevel(node, sf)) {
|
|
650
|
-
pushSymbol(node.name.text, node, "interface", hasExportModifier(node));
|
|
651
|
-
}
|
|
652
|
-
if (ts.isTypeAliasDeclaration(node) && isTopLevel(node, sf)) {
|
|
653
|
-
pushSymbol(node.name.text, node, "type", hasExportModifier(node));
|
|
654
|
-
}
|
|
655
|
-
if (ts.isEnumDeclaration(node) && isTopLevel(node, sf)) {
|
|
656
|
-
pushSymbol(node.name.text, node, "type", hasExportModifier(node));
|
|
657
|
-
}
|
|
658
|
-
if (ts.isVariableStatement(node) && isTopLevel(node, sf)) {
|
|
659
|
-
const exported = hasExportModifier(node);
|
|
660
|
-
for (const decl of node.declarationList.declarations) {
|
|
661
|
-
if (!ts.isIdentifier(decl.name)) continue;
|
|
662
|
-
const name = decl.name.text;
|
|
663
|
-
const init = decl.initializer;
|
|
664
|
-
const isFn = init && (ts.isArrowFunction(init) || ts.isFunctionExpression(init));
|
|
665
|
-
if (exported || isFn) {
|
|
666
|
-
pushSymbol(name, decl, isFn ? "function" : "const", exported);
|
|
667
|
-
}
|
|
668
|
-
}
|
|
669
|
-
}
|
|
670
|
-
if (ts.isExportAssignment(node) && !node.isExportEquals) {
|
|
671
|
-
exports.push({ name: "default", kind: "export" });
|
|
672
|
-
}
|
|
673
|
-
if (ts.isIdentifier(node) && node.parent && !ts.isImportSpecifier(node.parent)) {
|
|
674
|
-
const parent = node.parent;
|
|
675
|
-
if (ts.isCallExpression(parent) || ts.isPropertyAccessExpression(parent) || ts.isTypeReferenceNode(parent)) {
|
|
676
|
-
references.push({ name: node.text, line: lineOf(sf, node.getStart(sf)) });
|
|
677
|
-
}
|
|
678
|
-
}
|
|
679
|
-
ts.forEachChild(node, visit);
|
|
680
|
-
};
|
|
681
|
-
visit(sf);
|
|
682
|
-
return {
|
|
683
|
-
ok: true,
|
|
684
|
-
language: "typescript",
|
|
685
|
-
symbols,
|
|
686
|
-
imports,
|
|
687
|
-
exports,
|
|
688
|
-
references: references.slice(0, 200)
|
|
689
|
-
// bound noise
|
|
690
|
-
};
|
|
691
|
-
}
|
|
692
|
-
var typescriptAdapter = {
|
|
693
|
-
id: "typescript",
|
|
694
|
-
extensions: [...TS_JS_EXTENSIONS],
|
|
695
|
-
extract(input) {
|
|
696
|
-
try {
|
|
697
|
-
return extractFromSource(input.filePath, input.source, input.filePath);
|
|
698
|
-
} catch (err) {
|
|
699
|
-
return {
|
|
700
|
-
ok: false,
|
|
701
|
-
language: "typescript",
|
|
702
|
-
symbols: [],
|
|
703
|
-
imports: [],
|
|
704
|
-
exports: [],
|
|
705
|
-
errors: [err instanceof Error ? err.message : String(err)]
|
|
706
|
-
};
|
|
707
|
-
}
|
|
708
|
-
}
|
|
709
|
-
};
|
|
710
|
-
function isTypescriptAdapterAvailable() {
|
|
711
|
-
try {
|
|
712
|
-
return typeof ts.createSourceFile === "function";
|
|
713
|
-
} catch {
|
|
714
|
-
return false;
|
|
715
|
-
}
|
|
716
|
-
}
|
|
717
|
-
|
|
718
|
-
// src/code-context/adapters/registry.ts
|
|
719
|
-
var AdapterRegistry = class {
|
|
720
|
-
byExt = /* @__PURE__ */ new Map();
|
|
721
|
-
constructor(adapters) {
|
|
722
|
-
const list = adapters ?? [typescriptAdapter];
|
|
723
|
-
for (const a of list) {
|
|
724
|
-
for (const ext of a.extensions) {
|
|
725
|
-
this.byExt.set(ext.toLowerCase(), a);
|
|
726
|
-
}
|
|
727
|
-
}
|
|
728
|
-
}
|
|
729
|
-
getByPath(filePath) {
|
|
730
|
-
const ext = extname2(filePath).toLowerCase();
|
|
731
|
-
return this.byExt.get(ext);
|
|
732
|
-
}
|
|
733
|
-
hasExtension(ext) {
|
|
734
|
-
return this.byExt.has(ext.toLowerCase());
|
|
735
|
-
}
|
|
736
|
-
};
|
|
1239
|
+
import { stat } from "fs/promises";
|
|
1240
|
+
import { extname as extname3, isAbsolute as isAbsolute2, join as join6, relative as relative2 } from "pathe";
|
|
737
1241
|
|
|
738
1242
|
// src/code-context/focus.ts
|
|
739
1243
|
import { promises as fs } from "fs";
|
|
740
|
-
import { isAbsolute, join as
|
|
741
|
-
|
|
742
|
-
// src/code-context/ignore.ts
|
|
743
|
-
import { basename as basename2, extname as extname3, normalize as normalize2 } from "pathe";
|
|
744
|
-
|
|
745
|
-
// src/code-context/secrets.ts
|
|
746
|
-
import { basename, normalize } from "pathe";
|
|
747
|
-
var SECRET_BASENAME_PATTERNS = [
|
|
748
|
-
/^\.env$/i,
|
|
749
|
-
/^\.env\..+/i,
|
|
750
|
-
/\.pem$/i,
|
|
751
|
-
/\.key$/i,
|
|
752
|
-
/id_rsa/i,
|
|
753
|
-
/credentials/i,
|
|
754
|
-
/secret/i
|
|
755
|
-
];
|
|
756
|
-
function isSecretPath(filePath) {
|
|
757
|
-
const norm = normalize(filePath).replace(/\\/g, "/");
|
|
758
|
-
const parts = norm.split("/").filter(Boolean);
|
|
759
|
-
if (parts.includes(".git")) return true;
|
|
760
|
-
if (norm.includes("/.git/") || norm.startsWith(".git/") || norm === ".git") {
|
|
761
|
-
return true;
|
|
762
|
-
}
|
|
763
|
-
const base = basename(norm);
|
|
764
|
-
return SECRET_BASENAME_PATTERNS.some((re) => re.test(base));
|
|
765
|
-
}
|
|
766
|
-
|
|
767
|
-
// src/code-context/ignore.ts
|
|
768
|
-
var IGNORED_DIR_SEGMENTS = /* @__PURE__ */ new Set([
|
|
769
|
-
"node_modules",
|
|
770
|
-
"dist",
|
|
771
|
-
"coverage",
|
|
772
|
-
".turbo",
|
|
773
|
-
".next",
|
|
774
|
-
"build",
|
|
775
|
-
".cache"
|
|
776
|
-
]);
|
|
777
|
-
var BINARY_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
778
|
-
".png",
|
|
779
|
-
".jpg",
|
|
780
|
-
".jpeg",
|
|
781
|
-
".gif",
|
|
782
|
-
".webp",
|
|
783
|
-
".ico",
|
|
784
|
-
".svg",
|
|
785
|
-
".woff",
|
|
786
|
-
".woff2",
|
|
787
|
-
".ttf",
|
|
788
|
-
".eot",
|
|
789
|
-
".pdf",
|
|
790
|
-
".zip",
|
|
791
|
-
".tar",
|
|
792
|
-
".gz",
|
|
793
|
-
".tgz",
|
|
794
|
-
".7z",
|
|
795
|
-
".rar",
|
|
796
|
-
".exe",
|
|
797
|
-
".dll",
|
|
798
|
-
".so",
|
|
799
|
-
".dylib",
|
|
800
|
-
".bin",
|
|
801
|
-
".wasm",
|
|
802
|
-
".mp3",
|
|
803
|
-
".mp4",
|
|
804
|
-
".mov",
|
|
805
|
-
".lock"
|
|
806
|
-
]);
|
|
807
|
-
function isIgnoredPath(filePath) {
|
|
808
|
-
const norm = normalize2(filePath).replace(/\\/g, "/");
|
|
809
|
-
const parts = norm.split("/").filter(Boolean);
|
|
810
|
-
for (const p of parts) {
|
|
811
|
-
if (IGNORED_DIR_SEGMENTS.has(p)) return true;
|
|
812
|
-
}
|
|
813
|
-
const ext = extname3(basename2(norm)).toLowerCase();
|
|
814
|
-
if (BINARY_EXTENSIONS.has(ext)) return true;
|
|
815
|
-
const base = basename2(norm);
|
|
816
|
-
if (base === "package-lock.json" || base === "pnpm-lock.yaml" || base === "yarn.lock" || base === "bun.lockb") {
|
|
817
|
-
return true;
|
|
818
|
-
}
|
|
819
|
-
return false;
|
|
820
|
-
}
|
|
821
|
-
function classifyPath(filePath) {
|
|
822
|
-
if (isSecretPath(filePath)) return "secret";
|
|
823
|
-
if (isIgnoredPath(filePath)) return "ignored";
|
|
824
|
-
return "allowed";
|
|
825
|
-
}
|
|
826
|
-
|
|
827
|
-
// src/code-context/focus.ts
|
|
1244
|
+
import { isAbsolute, join as join5, normalize, relative } from "pathe";
|
|
828
1245
|
var KEYWORD_STOPWORDS = new Set(
|
|
829
1246
|
[
|
|
830
1247
|
"a",
|
|
@@ -915,12 +1332,12 @@ function extractPathLikeTokens(text) {
|
|
|
915
1332
|
return found;
|
|
916
1333
|
}
|
|
917
1334
|
function toRepoRelative(projectRoot, p) {
|
|
918
|
-
const abs = isAbsolute(p) ? p :
|
|
1335
|
+
const abs = isAbsolute(p) ? p : join5(projectRoot, p);
|
|
919
1336
|
const rel = relative(projectRoot, abs);
|
|
920
|
-
return
|
|
1337
|
+
return normalize(rel).replace(/\\/g, "/");
|
|
921
1338
|
}
|
|
922
1339
|
function toAbsolute(projectRoot, p) {
|
|
923
|
-
return isAbsolute(p) ? p :
|
|
1340
|
+
return isAbsolute(p) ? p : join5(projectRoot, p);
|
|
924
1341
|
}
|
|
925
1342
|
async function walkFiles(absDir, projectRoot, maxFiles, out, gaps) {
|
|
926
1343
|
if (out.length >= maxFiles) return;
|
|
@@ -933,7 +1350,7 @@ async function walkFiles(absDir, projectRoot, maxFiles, out, gaps) {
|
|
|
933
1350
|
}
|
|
934
1351
|
for (const entry of entries) {
|
|
935
1352
|
if (out.length >= maxFiles) return;
|
|
936
|
-
const abs =
|
|
1353
|
+
const abs = join5(absDir, entry.name);
|
|
937
1354
|
const rel = toRepoRelative(projectRoot, abs);
|
|
938
1355
|
const kind = classifyPath(rel);
|
|
939
1356
|
if (kind === "secret" || kind === "ignored") continue;
|
|
@@ -1027,7 +1444,7 @@ async function resolveChangeMeta(projectRoot, changeId, gaps) {
|
|
|
1027
1444
|
return null;
|
|
1028
1445
|
}
|
|
1029
1446
|
const cpath = changePath(projectRoot, config, id);
|
|
1030
|
-
const metaPath =
|
|
1447
|
+
const metaPath = join5(cpath, "meta.yaml");
|
|
1031
1448
|
if (!await pathExists(metaPath)) {
|
|
1032
1449
|
if (!await pathExists(cpath)) {
|
|
1033
1450
|
gaps.push({
|
|
@@ -1060,7 +1477,7 @@ async function searchByKeywords(projectRoot, keywords, symbols, maxFiles) {
|
|
|
1060
1477
|
const terms = [...keywords, ...symbols.map((s) => s.toLowerCase())];
|
|
1061
1478
|
if (!terms.length) return out;
|
|
1062
1479
|
for (const root of SOURCE_ROOTS) {
|
|
1063
|
-
const abs =
|
|
1480
|
+
const abs = join5(projectRoot, root);
|
|
1064
1481
|
if (!await pathExists(abs)) continue;
|
|
1065
1482
|
const files = [];
|
|
1066
1483
|
await walkFiles(abs, projectRoot, maxFiles * 4, files, []);
|
|
@@ -1074,7 +1491,7 @@ async function searchByKeywords(projectRoot, keywords, symbols, maxFiles) {
|
|
|
1074
1491
|
}
|
|
1075
1492
|
if (symbols.length) {
|
|
1076
1493
|
try {
|
|
1077
|
-
const text = await readText(
|
|
1494
|
+
const text = await readText(join5(projectRoot, rel));
|
|
1078
1495
|
if (symbols.some((s) => text.includes(s))) {
|
|
1079
1496
|
out.push(rel);
|
|
1080
1497
|
}
|
|
@@ -1110,7 +1527,7 @@ async function resolveFocus(request, options) {
|
|
|
1110
1527
|
}
|
|
1111
1528
|
notes.push(`Change signals from: ${change.id}`);
|
|
1112
1529
|
for (const artifact of ["tasks.md", "feature.md", "stories.md", "lld.md"]) {
|
|
1113
|
-
const body = await readBounded(
|
|
1530
|
+
const body = await readBounded(join5(change.path, artifact), 8e3);
|
|
1114
1531
|
if (!body) continue;
|
|
1115
1532
|
if (!request.paths?.length) {
|
|
1116
1533
|
for (const p of extractPathLikeTokens(body)) {
|
|
@@ -1183,17 +1600,6 @@ async function resolveFocus(request, options) {
|
|
|
1183
1600
|
}
|
|
1184
1601
|
|
|
1185
1602
|
// src/code-context/formatter.ts
|
|
1186
|
-
function fenceLang(filePath) {
|
|
1187
|
-
const lower = filePath.toLowerCase();
|
|
1188
|
-
if (lower.endsWith(".tsx") || lower.endsWith(".jsx")) return "tsx";
|
|
1189
|
-
if (lower.endsWith(".ts") || lower.endsWith(".mts") || lower.endsWith(".cts")) {
|
|
1190
|
-
return "ts";
|
|
1191
|
-
}
|
|
1192
|
-
if (lower.endsWith(".js") || lower.endsWith(".mjs") || lower.endsWith(".cjs")) {
|
|
1193
|
-
return "js";
|
|
1194
|
-
}
|
|
1195
|
-
return "";
|
|
1196
|
-
}
|
|
1197
1603
|
function formatMarkdown(input) {
|
|
1198
1604
|
const lines = [];
|
|
1199
1605
|
lines.push("# Code context");
|
|
@@ -1233,50 +1639,15 @@ function formatMarkdown(input) {
|
|
|
1233
1639
|
lines.push("");
|
|
1234
1640
|
lines.push("## Slices");
|
|
1235
1641
|
lines.push("");
|
|
1236
|
-
|
|
1237
|
-
const
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
for (const slice of input.slices) {
|
|
1241
|
-
if (lines.length >= maxLines) {
|
|
1242
|
-
truncated = true;
|
|
1243
|
-
break;
|
|
1244
|
-
}
|
|
1245
|
-
const header = `### \`${slice.filePath}\`${slice.symbolName ? ` \xB7 \`${slice.symbolName}\`` : ""} (lines ${slice.startLine}\u2013${slice.endLine}) \xB7 ${slice.reason}`;
|
|
1246
|
-
lines.push(header);
|
|
1247
|
-
lines.push("```" + fenceLang(slice.filePath));
|
|
1248
|
-
lines.push(slice.body);
|
|
1249
|
-
lines.push("```");
|
|
1250
|
-
lines.push("");
|
|
1251
|
-
approxTokens += Math.ceil((header.length + slice.body.length) / 4);
|
|
1252
|
-
if (approxTokens > maxTokens) {
|
|
1253
|
-
truncated = true;
|
|
1254
|
-
break;
|
|
1255
|
-
}
|
|
1256
|
-
}
|
|
1257
|
-
if (!input.slices.length) {
|
|
1258
|
-
lines.push("_No slices emitted._");
|
|
1259
|
-
lines.push("");
|
|
1260
|
-
}
|
|
1261
|
-
lines.push("## Structural notes");
|
|
1262
|
-
const topFiles = [...new Set(input.slices.map((s) => s.filePath))].slice(0, 15);
|
|
1263
|
-
if (topFiles.length) {
|
|
1264
|
-
lines.push(`- Ranked files (sample): ${topFiles.join(", ")}`);
|
|
1265
|
-
} else {
|
|
1266
|
-
lines.push("- No structural neighborhood (empty analysis set).");
|
|
1267
|
-
}
|
|
1268
|
-
lines.push("");
|
|
1269
|
-
let markdown = lines.join("\n");
|
|
1270
|
-
if (Math.ceil(markdown.length / 4) > maxTokens) {
|
|
1271
|
-
truncated = true;
|
|
1272
|
-
markdown = markdown.slice(0, maxTokens * 4) + "\n\n\u2026(truncated)\n";
|
|
1273
|
-
}
|
|
1274
|
-
return { markdown, truncated };
|
|
1642
|
+
const packed = input.packedText ?? "";
|
|
1643
|
+
const frame = `${lines.join("\n")}
|
|
1644
|
+
`;
|
|
1645
|
+
return { markdown: packed ? frame + packed : frame, truncated: input.summary.truncated };
|
|
1275
1646
|
}
|
|
1276
|
-
function formatJsonSummary(summary, gaps, slices,
|
|
1647
|
+
function formatJsonSummary(summary, gaps, slices, ok4) {
|
|
1277
1648
|
return JSON.stringify(
|
|
1278
1649
|
{
|
|
1279
|
-
ok,
|
|
1650
|
+
ok: ok4,
|
|
1280
1651
|
summary,
|
|
1281
1652
|
gaps,
|
|
1282
1653
|
slices: slices.map((s) => ({
|
|
@@ -1293,288 +1664,6 @@ function formatJsonSummary(summary, gaps, slices, ok) {
|
|
|
1293
1664
|
);
|
|
1294
1665
|
}
|
|
1295
1666
|
|
|
1296
|
-
// src/code-context/graph.ts
|
|
1297
|
-
import { dirname as dirname2, join as join5, normalize as normalize4 } from "pathe";
|
|
1298
|
-
var GraphLite = class {
|
|
1299
|
-
files = /* @__PURE__ */ new Map();
|
|
1300
|
-
edges = [];
|
|
1301
|
-
add(path, source, extract) {
|
|
1302
|
-
const norm = normalize4(path).replace(/\\/g, "/");
|
|
1303
|
-
this.files.set(norm, { path: norm, extract, source });
|
|
1304
|
-
for (const imp of extract.imports) {
|
|
1305
|
-
this.edges.push({
|
|
1306
|
-
from: norm,
|
|
1307
|
-
to: imp.from,
|
|
1308
|
-
names: imp.names
|
|
1309
|
-
});
|
|
1310
|
-
}
|
|
1311
|
-
}
|
|
1312
|
-
allSymbols() {
|
|
1313
|
-
const out = [];
|
|
1314
|
-
for (const node of this.files.values()) {
|
|
1315
|
-
out.push(...node.extract.symbols);
|
|
1316
|
-
}
|
|
1317
|
-
return out;
|
|
1318
|
-
}
|
|
1319
|
-
getSource(path) {
|
|
1320
|
-
return this.files.get(normalize4(path).replace(/\\/g, "/"))?.source;
|
|
1321
|
-
}
|
|
1322
|
-
/**
|
|
1323
|
-
* Resolve relative import specifier to a known graph file path, if present.
|
|
1324
|
-
*/
|
|
1325
|
-
resolveImportTarget(fromFile, specifier) {
|
|
1326
|
-
if (!specifier.startsWith(".")) return void 0;
|
|
1327
|
-
const base = dirname2(fromFile);
|
|
1328
|
-
const joined = normalize4(join5(base, specifier)).replace(/\\/g, "/");
|
|
1329
|
-
const candidates = [
|
|
1330
|
-
joined,
|
|
1331
|
-
`${joined}.ts`,
|
|
1332
|
-
`${joined}.tsx`,
|
|
1333
|
-
`${joined}.js`,
|
|
1334
|
-
`${joined}.jsx`,
|
|
1335
|
-
`${joined}.mts`,
|
|
1336
|
-
`${joined}.cts`,
|
|
1337
|
-
`${joined}/index.ts`,
|
|
1338
|
-
`${joined}/index.js`
|
|
1339
|
-
];
|
|
1340
|
-
for (const c of candidates) {
|
|
1341
|
-
if (this.files.has(c)) return c;
|
|
1342
|
-
}
|
|
1343
|
-
for (const [p] of this.files) {
|
|
1344
|
-
if (p === joined || p.startsWith(joined + ".")) return p;
|
|
1345
|
-
}
|
|
1346
|
-
return void 0;
|
|
1347
|
-
}
|
|
1348
|
-
/** Import hop distance from any seed file (0 = seed, 1 = direct import, …). */
|
|
1349
|
-
hopDistance(filePath, seedPaths, maxHops) {
|
|
1350
|
-
const norm = normalize4(filePath).replace(/\\/g, "/");
|
|
1351
|
-
const seeds = new Set(seedPaths.map((s) => normalize4(s).replace(/\\/g, "/")));
|
|
1352
|
-
if (seeds.has(norm)) return 0;
|
|
1353
|
-
let frontier = new Set(seeds);
|
|
1354
|
-
const visited = new Set(seeds);
|
|
1355
|
-
for (let hop = 1; hop <= maxHops; hop++) {
|
|
1356
|
-
const next = /* @__PURE__ */ new Set();
|
|
1357
|
-
for (const f of frontier) {
|
|
1358
|
-
for (const e of this.edges) {
|
|
1359
|
-
if (e.from !== f) continue;
|
|
1360
|
-
const target = this.resolveImportTarget(e.from, e.to);
|
|
1361
|
-
if (!target || visited.has(target)) continue;
|
|
1362
|
-
visited.add(target);
|
|
1363
|
-
next.add(target);
|
|
1364
|
-
if (target === norm) return hop;
|
|
1365
|
-
}
|
|
1366
|
-
}
|
|
1367
|
-
frontier = next;
|
|
1368
|
-
if (!frontier.size) break;
|
|
1369
|
-
}
|
|
1370
|
-
return null;
|
|
1371
|
-
}
|
|
1372
|
-
importNeighbors(filePath) {
|
|
1373
|
-
const norm = normalize4(filePath).replace(/\\/g, "/");
|
|
1374
|
-
const out = [];
|
|
1375
|
-
for (const e of this.edges) {
|
|
1376
|
-
if (e.from !== norm) continue;
|
|
1377
|
-
const t = this.resolveImportTarget(e.from, e.to);
|
|
1378
|
-
if (t) out.push(t);
|
|
1379
|
-
}
|
|
1380
|
-
return out;
|
|
1381
|
-
}
|
|
1382
|
-
};
|
|
1383
|
-
|
|
1384
|
-
// src/code-context/ranker.ts
|
|
1385
|
-
import { basename as basename3 } from "pathe";
|
|
1386
|
-
function isTestPath(filePath) {
|
|
1387
|
-
const p = filePath.replace(/\\/g, "/");
|
|
1388
|
-
return p.includes("__tests__/") || p.includes("/__tests__/") || /\.test\.[cm]?[jt]sx?$/.test(p) || /\.spec\.[cm]?[jt]sx?$/.test(p);
|
|
1389
|
-
}
|
|
1390
|
-
function pathSegments(filePath) {
|
|
1391
|
-
return filePath.replace(/\\/g, "/").toLowerCase().split("/").filter(Boolean).flatMap((s) => s.replace(/\.[^.]+$/, "").split(/[-_.]/));
|
|
1392
|
-
}
|
|
1393
|
-
function scoreSymbol(symbol, plan, graph, caps) {
|
|
1394
|
-
let score = 0;
|
|
1395
|
-
const reasons = [];
|
|
1396
|
-
const seeds = new Set(plan.seedPaths);
|
|
1397
|
-
const seedSymbols = plan.seedSymbols;
|
|
1398
|
-
if (seedSymbols.length) {
|
|
1399
|
-
const exact = seedSymbols.find((s) => s === symbol.name);
|
|
1400
|
-
if (exact) {
|
|
1401
|
-
score += 100;
|
|
1402
|
-
reasons.push("exact-symbol");
|
|
1403
|
-
} else {
|
|
1404
|
-
const ci = seedSymbols.find((s) => s.toLowerCase() === symbol.name.toLowerCase());
|
|
1405
|
-
if (ci) {
|
|
1406
|
-
score += 90;
|
|
1407
|
-
reasons.push("symbol-ci");
|
|
1408
|
-
}
|
|
1409
|
-
}
|
|
1410
|
-
}
|
|
1411
|
-
if (seeds.has(symbol.filePath)) {
|
|
1412
|
-
score += 40;
|
|
1413
|
-
reasons.push("seed-path");
|
|
1414
|
-
}
|
|
1415
|
-
const hop = graph.hopDistance(symbol.filePath, plan.seedPaths, caps.maxImportHops);
|
|
1416
|
-
if (hop === 0) {
|
|
1417
|
-
score += 30;
|
|
1418
|
-
reasons.push("hop-0");
|
|
1419
|
-
} else if (hop === 1) {
|
|
1420
|
-
score += 18;
|
|
1421
|
-
reasons.push("hop-1");
|
|
1422
|
-
}
|
|
1423
|
-
const nameLower = symbol.name.toLowerCase();
|
|
1424
|
-
let kwHits = 0;
|
|
1425
|
-
for (const k of plan.keywords) {
|
|
1426
|
-
if (nameLower.includes(k)) {
|
|
1427
|
-
kwHits++;
|
|
1428
|
-
if (kwHits >= 3) break;
|
|
1429
|
-
}
|
|
1430
|
-
}
|
|
1431
|
-
if (kwHits) {
|
|
1432
|
-
score += 15 * kwHits;
|
|
1433
|
-
reasons.push(`keyword-name\xD7${kwHits}`);
|
|
1434
|
-
}
|
|
1435
|
-
const segs = pathSegments(symbol.filePath);
|
|
1436
|
-
let pathHits = 0;
|
|
1437
|
-
for (const k of plan.keywords) {
|
|
1438
|
-
if (segs.some((s) => s.includes(k) || k.includes(s))) {
|
|
1439
|
-
pathHits++;
|
|
1440
|
-
if (pathHits >= 3) break;
|
|
1441
|
-
}
|
|
1442
|
-
}
|
|
1443
|
-
if (pathHits) {
|
|
1444
|
-
score += 8 * pathHits;
|
|
1445
|
-
reasons.push(`keyword-path\xD7${pathHits}`);
|
|
1446
|
-
}
|
|
1447
|
-
if (symbol.exported) {
|
|
1448
|
-
score += 5;
|
|
1449
|
-
reasons.push("exported");
|
|
1450
|
-
}
|
|
1451
|
-
const base = basename3(symbol.filePath).toLowerCase();
|
|
1452
|
-
let titleHits = 0;
|
|
1453
|
-
for (const k of plan.keywords.slice(0, 10)) {
|
|
1454
|
-
if (base.includes(k)) {
|
|
1455
|
-
titleHits++;
|
|
1456
|
-
if (titleHits >= 2) break;
|
|
1457
|
-
}
|
|
1458
|
-
}
|
|
1459
|
-
if (titleHits) {
|
|
1460
|
-
score += 6 * titleHits;
|
|
1461
|
-
reasons.push(`title-path\xD7${titleHits}`);
|
|
1462
|
-
}
|
|
1463
|
-
const focusIsTest = plan.seedPaths.some(isTestPath) || plan.keywords.some((k) => k === "test" || k === "tests" || k === "vitest");
|
|
1464
|
-
if (isTestPath(symbol.filePath) && !focusIsTest) {
|
|
1465
|
-
score -= 12;
|
|
1466
|
-
reasons.push("test-penalty");
|
|
1467
|
-
}
|
|
1468
|
-
const source = graph.getSource(symbol.filePath);
|
|
1469
|
-
if (source) {
|
|
1470
|
-
const lines = source.split(/\r?\n/).length;
|
|
1471
|
-
if (lines > 400) {
|
|
1472
|
-
const over = Math.floor((lines - 400) / 200);
|
|
1473
|
-
const pen = Math.min(10, over);
|
|
1474
|
-
if (pen > 0) {
|
|
1475
|
-
score -= pen;
|
|
1476
|
-
reasons.push("huge-file");
|
|
1477
|
-
}
|
|
1478
|
-
}
|
|
1479
|
-
}
|
|
1480
|
-
if (symbol.kind === "function" || symbol.kind === "class" || symbol.kind === "interface") {
|
|
1481
|
-
score += 2;
|
|
1482
|
-
}
|
|
1483
|
-
return { symbol, score, reasons };
|
|
1484
|
-
}
|
|
1485
|
-
function rankSymbols(graph, plan, caps) {
|
|
1486
|
-
const items = graph.allSymbols().map((s) => scoreSymbol(s, plan, graph, caps));
|
|
1487
|
-
items.sort((a, b) => {
|
|
1488
|
-
if (b.score !== a.score) return b.score - a.score;
|
|
1489
|
-
const fa = a.symbol.filePath.localeCompare(b.symbol.filePath);
|
|
1490
|
-
if (fa !== 0) return fa;
|
|
1491
|
-
return a.symbol.startLine - b.symbol.startLine;
|
|
1492
|
-
});
|
|
1493
|
-
return items.slice(0, caps.maxSymbols);
|
|
1494
|
-
}
|
|
1495
|
-
|
|
1496
|
-
// src/code-context/slicer.ts
|
|
1497
|
-
function sliceBody(source, startLine, endLine, maxLines) {
|
|
1498
|
-
const lines = source.split(/\r?\n/);
|
|
1499
|
-
const start = Math.max(1, startLine);
|
|
1500
|
-
let end = Math.min(lines.length, endLine);
|
|
1501
|
-
let truncated = false;
|
|
1502
|
-
if (end - start + 1 > maxLines) {
|
|
1503
|
-
end = start + maxLines - 1;
|
|
1504
|
-
truncated = true;
|
|
1505
|
-
}
|
|
1506
|
-
const body = lines.slice(start - 1, end).join("\n");
|
|
1507
|
-
return { body, end, truncated };
|
|
1508
|
-
}
|
|
1509
|
-
function emitSlices(ranked, graph, caps, options) {
|
|
1510
|
-
const slices = [];
|
|
1511
|
-
const gaps = [];
|
|
1512
|
-
let truncated = false;
|
|
1513
|
-
const seen = /* @__PURE__ */ new Set();
|
|
1514
|
-
for (const item of ranked) {
|
|
1515
|
-
if (slices.length >= caps.maxSlices) {
|
|
1516
|
-
truncated = true;
|
|
1517
|
-
break;
|
|
1518
|
-
}
|
|
1519
|
-
const sym = item.symbol;
|
|
1520
|
-
if (isSecretPath(sym.filePath)) continue;
|
|
1521
|
-
const source = graph.getSource(sym.filePath);
|
|
1522
|
-
if (!source) continue;
|
|
1523
|
-
const {
|
|
1524
|
-
body,
|
|
1525
|
-
end,
|
|
1526
|
-
truncated: lineTrunc
|
|
1527
|
-
} = sliceBody(source, sym.startLine, sym.endLine, caps.maxLinesPerSlice);
|
|
1528
|
-
if (lineTrunc) truncated = true;
|
|
1529
|
-
const id = `${sym.filePath}:${sym.startLine}-${end}:${sym.name}`;
|
|
1530
|
-
if (seen.has(id)) continue;
|
|
1531
|
-
seen.add(id);
|
|
1532
|
-
const reasonParts = item.reasons.length ? item.reasons.slice(0, 4).join(", ") : "ranked";
|
|
1533
|
-
slices.push({
|
|
1534
|
-
id,
|
|
1535
|
-
filePath: sym.filePath,
|
|
1536
|
-
startLine: sym.startLine,
|
|
1537
|
-
endLine: end,
|
|
1538
|
-
symbolName: sym.name,
|
|
1539
|
-
reason: reasonParts,
|
|
1540
|
-
body
|
|
1541
|
-
});
|
|
1542
|
-
if (options?.includeNeighbors && slices.length < caps.maxSlices) {
|
|
1543
|
-
const neighbors = graph.importNeighbors(sym.filePath);
|
|
1544
|
-
for (const n of neighbors.slice(0, 1)) {
|
|
1545
|
-
if (isSecretPath(n)) continue;
|
|
1546
|
-
const nSource = graph.getSource(n);
|
|
1547
|
-
const nNode = graph.files.get(n);
|
|
1548
|
-
if (!nSource || !nNode) continue;
|
|
1549
|
-
const nSym = nNode.extract.symbols.find((s) => s.exported) ?? nNode.extract.symbols[0];
|
|
1550
|
-
if (!nSym) continue;
|
|
1551
|
-
const nb = sliceBody(nSource, nSym.startLine, nSym.endLine, caps.maxLinesPerSlice);
|
|
1552
|
-
const nid = `${nSym.filePath}:${nSym.startLine}-${nb.end}:${nSym.name}:neighbor`;
|
|
1553
|
-
if (seen.has(nid)) continue;
|
|
1554
|
-
seen.add(nid);
|
|
1555
|
-
slices.push({
|
|
1556
|
-
id: nid,
|
|
1557
|
-
filePath: nSym.filePath,
|
|
1558
|
-
startLine: nSym.startLine,
|
|
1559
|
-
endLine: nb.end,
|
|
1560
|
-
symbolName: nSym.name,
|
|
1561
|
-
reason: `neighbor-of ${sym.name}`,
|
|
1562
|
-
body: nb.body
|
|
1563
|
-
});
|
|
1564
|
-
if (nb.truncated) truncated = true;
|
|
1565
|
-
break;
|
|
1566
|
-
}
|
|
1567
|
-
}
|
|
1568
|
-
}
|
|
1569
|
-
if (truncated) {
|
|
1570
|
-
gaps.push({
|
|
1571
|
-
code: "cap_truncated",
|
|
1572
|
-
message: "Output truncated at configured caps (maxSlices/maxLinesPerSlice)."
|
|
1573
|
-
});
|
|
1574
|
-
}
|
|
1575
|
-
return { slices, gaps, truncated };
|
|
1576
|
-
}
|
|
1577
|
-
|
|
1578
1667
|
// src/code-context/types.ts
|
|
1579
1668
|
var DEFAULT_CODE_CONTEXT_CAPS = {
|
|
1580
1669
|
maxFiles: 40,
|
|
@@ -1589,13 +1678,204 @@ function mergeCaps(partial) {
|
|
|
1589
1678
|
return { ...DEFAULT_CODE_CONTEXT_CAPS, ...partial };
|
|
1590
1679
|
}
|
|
1591
1680
|
|
|
1592
|
-
// src/code-context/adapters/
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1681
|
+
// src/code-context/adapters/registry.ts
|
|
1682
|
+
import { extname as extname2 } from "pathe";
|
|
1683
|
+
|
|
1684
|
+
// src/code-context/adapters/typescript.ts
|
|
1685
|
+
import { extname } from "pathe";
|
|
1686
|
+
import ts from "typescript";
|
|
1687
|
+
var TS_JS_EXTENSIONS = [
|
|
1688
|
+
".ts",
|
|
1689
|
+
".tsx",
|
|
1690
|
+
".js",
|
|
1691
|
+
".jsx",
|
|
1692
|
+
".mts",
|
|
1693
|
+
".cts",
|
|
1694
|
+
".mjs",
|
|
1695
|
+
".cjs"
|
|
1696
|
+
];
|
|
1697
|
+
function scriptKindForPath(filePath) {
|
|
1698
|
+
const ext = extname(filePath).toLowerCase();
|
|
1699
|
+
switch (ext) {
|
|
1700
|
+
case ".tsx":
|
|
1701
|
+
return ts.ScriptKind.TSX;
|
|
1702
|
+
case ".jsx":
|
|
1703
|
+
return ts.ScriptKind.JSX;
|
|
1704
|
+
case ".js":
|
|
1705
|
+
case ".mjs":
|
|
1706
|
+
case ".cjs":
|
|
1707
|
+
return ts.ScriptKind.JS;
|
|
1708
|
+
case ".cts":
|
|
1709
|
+
case ".mts":
|
|
1710
|
+
case ".ts":
|
|
1711
|
+
default:
|
|
1712
|
+
return ts.ScriptKind.TS;
|
|
1713
|
+
}
|
|
1714
|
+
}
|
|
1715
|
+
function lineOf(sf, pos) {
|
|
1716
|
+
return sf.getLineAndCharacterOfPosition(pos).line + 1;
|
|
1717
|
+
}
|
|
1718
|
+
function hasExportModifier(node) {
|
|
1719
|
+
const mods = ts.canHaveModifiers(node) ? ts.getModifiers(node) : void 0;
|
|
1720
|
+
return Boolean(mods?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword));
|
|
1721
|
+
}
|
|
1722
|
+
function isTopLevel(node, sf) {
|
|
1723
|
+
let p = node.parent;
|
|
1724
|
+
while (p && p !== sf) {
|
|
1725
|
+
if (ts.isFunctionDeclaration(p) || ts.isFunctionExpression(p) || ts.isArrowFunction(p) || ts.isMethodDeclaration(p) || ts.isConstructorDeclaration(p) || ts.isGetAccessorDeclaration(p) || ts.isSetAccessorDeclaration(p)) {
|
|
1726
|
+
return false;
|
|
1727
|
+
}
|
|
1728
|
+
if (ts.isClassDeclaration(p) || ts.isClassExpression(p)) {
|
|
1729
|
+
return false;
|
|
1730
|
+
}
|
|
1731
|
+
p = p.parent;
|
|
1732
|
+
}
|
|
1733
|
+
return true;
|
|
1734
|
+
}
|
|
1735
|
+
function extractFromSource(filePath, source, repoRelativePath) {
|
|
1736
|
+
const sf = ts.createSourceFile(
|
|
1737
|
+
filePath,
|
|
1738
|
+
source,
|
|
1739
|
+
ts.ScriptTarget.Latest,
|
|
1740
|
+
/*setParentNodes*/
|
|
1741
|
+
true,
|
|
1742
|
+
scriptKindForPath(filePath)
|
|
1743
|
+
);
|
|
1744
|
+
const symbols = [];
|
|
1745
|
+
const imports = [];
|
|
1746
|
+
const exports = [];
|
|
1747
|
+
const references = [];
|
|
1748
|
+
const pushSymbol = (name, node, kind, exported) => {
|
|
1749
|
+
if (!name) return;
|
|
1750
|
+
symbols.push({
|
|
1751
|
+
name,
|
|
1752
|
+
kind,
|
|
1753
|
+
filePath: repoRelativePath,
|
|
1754
|
+
startLine: lineOf(sf, node.getStart(sf, false)),
|
|
1755
|
+
endLine: lineOf(sf, node.getEnd()),
|
|
1756
|
+
exported
|
|
1757
|
+
});
|
|
1758
|
+
if (exported) {
|
|
1759
|
+
exports.push({ name, kind });
|
|
1760
|
+
}
|
|
1761
|
+
};
|
|
1762
|
+
const visit = (node) => {
|
|
1763
|
+
if (ts.isImportDeclaration(node)) {
|
|
1764
|
+
const spec = node.moduleSpecifier;
|
|
1765
|
+
if (ts.isStringLiteral(spec)) {
|
|
1766
|
+
const names = [];
|
|
1767
|
+
const clause = node.importClause;
|
|
1768
|
+
if (clause?.name) names.push(clause.name.text);
|
|
1769
|
+
if (clause?.namedBindings) {
|
|
1770
|
+
if (ts.isNamespaceImport(clause.namedBindings)) {
|
|
1771
|
+
names.push(clause.namedBindings.name.text);
|
|
1772
|
+
} else if (ts.isNamedImports(clause.namedBindings)) {
|
|
1773
|
+
for (const el of clause.namedBindings.elements) {
|
|
1774
|
+
names.push(el.name.text);
|
|
1775
|
+
}
|
|
1776
|
+
}
|
|
1777
|
+
}
|
|
1778
|
+
imports.push({ from: spec.text, names: names.length ? names : void 0 });
|
|
1779
|
+
}
|
|
1780
|
+
}
|
|
1781
|
+
if (ts.isExportDeclaration(node) && node.moduleSpecifier && ts.isStringLiteral(node.moduleSpecifier)) {
|
|
1782
|
+
imports.push({ from: node.moduleSpecifier.text, names: ["*"] });
|
|
1783
|
+
}
|
|
1784
|
+
if (ts.isFunctionDeclaration(node) && node.name && isTopLevel(node, sf)) {
|
|
1785
|
+
pushSymbol(node.name.text, node, "function", hasExportModifier(node));
|
|
1786
|
+
}
|
|
1787
|
+
if (ts.isClassDeclaration(node) && node.name && isTopLevel(node, sf)) {
|
|
1788
|
+
pushSymbol(node.name.text, node, "class", hasExportModifier(node));
|
|
1789
|
+
}
|
|
1790
|
+
if (ts.isInterfaceDeclaration(node) && isTopLevel(node, sf)) {
|
|
1791
|
+
pushSymbol(node.name.text, node, "interface", hasExportModifier(node));
|
|
1792
|
+
}
|
|
1793
|
+
if (ts.isTypeAliasDeclaration(node) && isTopLevel(node, sf)) {
|
|
1794
|
+
pushSymbol(node.name.text, node, "type", hasExportModifier(node));
|
|
1795
|
+
}
|
|
1796
|
+
if (ts.isEnumDeclaration(node) && isTopLevel(node, sf)) {
|
|
1797
|
+
pushSymbol(node.name.text, node, "type", hasExportModifier(node));
|
|
1798
|
+
}
|
|
1799
|
+
if (ts.isVariableStatement(node) && isTopLevel(node, sf)) {
|
|
1800
|
+
const exported = hasExportModifier(node);
|
|
1801
|
+
for (const decl of node.declarationList.declarations) {
|
|
1802
|
+
if (!ts.isIdentifier(decl.name)) continue;
|
|
1803
|
+
const name = decl.name.text;
|
|
1804
|
+
const init = decl.initializer;
|
|
1805
|
+
const isFn = init && (ts.isArrowFunction(init) || ts.isFunctionExpression(init));
|
|
1806
|
+
if (exported || isFn) {
|
|
1807
|
+
pushSymbol(name, decl, isFn ? "function" : "const", exported);
|
|
1808
|
+
}
|
|
1809
|
+
}
|
|
1810
|
+
}
|
|
1811
|
+
if (ts.isExportAssignment(node) && !node.isExportEquals) {
|
|
1812
|
+
exports.push({ name: "default", kind: "export" });
|
|
1813
|
+
}
|
|
1814
|
+
if (ts.isIdentifier(node) && node.parent && !ts.isImportSpecifier(node.parent)) {
|
|
1815
|
+
const parent = node.parent;
|
|
1816
|
+
if (ts.isCallExpression(parent) || ts.isPropertyAccessExpression(parent) || ts.isTypeReferenceNode(parent)) {
|
|
1817
|
+
references.push({ name: node.text, line: lineOf(sf, node.getStart(sf)) });
|
|
1818
|
+
}
|
|
1819
|
+
}
|
|
1820
|
+
ts.forEachChild(node, visit);
|
|
1821
|
+
};
|
|
1822
|
+
visit(sf);
|
|
1823
|
+
return {
|
|
1824
|
+
ok: true,
|
|
1825
|
+
language: "typescript",
|
|
1826
|
+
symbols,
|
|
1827
|
+
imports,
|
|
1828
|
+
exports,
|
|
1829
|
+
references: references.slice(0, 200)
|
|
1830
|
+
// bound noise
|
|
1831
|
+
};
|
|
1832
|
+
}
|
|
1833
|
+
var typescriptAdapter = {
|
|
1834
|
+
id: "typescript",
|
|
1835
|
+
extensions: [...TS_JS_EXTENSIONS],
|
|
1836
|
+
extract(input) {
|
|
1837
|
+
try {
|
|
1838
|
+
return extractFromSource(input.filePath, input.source, input.filePath);
|
|
1839
|
+
} catch (err) {
|
|
1840
|
+
return {
|
|
1841
|
+
ok: false,
|
|
1842
|
+
language: "typescript",
|
|
1843
|
+
symbols: [],
|
|
1844
|
+
imports: [],
|
|
1845
|
+
exports: [],
|
|
1846
|
+
errors: [err instanceof Error ? err.message : String(err)]
|
|
1847
|
+
};
|
|
1848
|
+
}
|
|
1849
|
+
}
|
|
1850
|
+
};
|
|
1851
|
+
|
|
1852
|
+
// src/code-context/adapters/registry.ts
|
|
1853
|
+
var AdapterRegistry = class {
|
|
1854
|
+
byExt = /* @__PURE__ */ new Map();
|
|
1855
|
+
constructor(adapters) {
|
|
1856
|
+
const list = adapters ?? [typescriptAdapter];
|
|
1857
|
+
for (const a of list) {
|
|
1858
|
+
for (const ext of a.extensions) {
|
|
1859
|
+
this.byExt.set(ext.toLowerCase(), a);
|
|
1860
|
+
}
|
|
1861
|
+
}
|
|
1862
|
+
}
|
|
1863
|
+
getByPath(filePath) {
|
|
1864
|
+
const ext = extname2(filePath).toLowerCase();
|
|
1865
|
+
return this.byExt.get(ext);
|
|
1866
|
+
}
|
|
1867
|
+
hasExtension(ext) {
|
|
1868
|
+
return this.byExt.has(ext.toLowerCase());
|
|
1869
|
+
}
|
|
1870
|
+
};
|
|
1871
|
+
|
|
1872
|
+
// src/code-context/adapters/fallback.ts
|
|
1873
|
+
var fallbackAdapter = {
|
|
1874
|
+
id: "fallback",
|
|
1875
|
+
extensions: [],
|
|
1876
|
+
extract(input) {
|
|
1877
|
+
const lines = input.source.split(/\r?\n/);
|
|
1878
|
+
const headEnd = Math.min(lines.length, 20);
|
|
1599
1879
|
return {
|
|
1600
1880
|
ok: true,
|
|
1601
1881
|
language: "text",
|
|
@@ -1657,75 +1937,125 @@ function hardFailure(gaps, message, summary) {
|
|
|
1657
1937
|
gaps
|
|
1658
1938
|
};
|
|
1659
1939
|
}
|
|
1940
|
+
function toRepoPath(projectRoot, input) {
|
|
1941
|
+
const abs = isAbsolute2(input) ? input : join6(projectRoot, input);
|
|
1942
|
+
let rel = relative2(projectRoot, abs).replaceAll("\\", "/");
|
|
1943
|
+
while (rel.startsWith("./")) rel = rel.slice(2);
|
|
1944
|
+
return rel;
|
|
1945
|
+
}
|
|
1946
|
+
function pathKept(rel, fileArgs, dirArgs) {
|
|
1947
|
+
if (fileArgs.includes(rel)) return true;
|
|
1948
|
+
return dirArgs.some((dir) => rel.startsWith(`${dir}/`));
|
|
1949
|
+
}
|
|
1950
|
+
function symbolKept(symbol, names) {
|
|
1951
|
+
return names.some((name) => symbol === name || symbol.endsWith(`.${name}`));
|
|
1952
|
+
}
|
|
1660
1953
|
async function generateCodeContext(request, deps) {
|
|
1661
|
-
const caps = mergeCaps(request.caps);
|
|
1662
1954
|
const projectRoot = request.projectRoot;
|
|
1663
1955
|
const gaps = [];
|
|
1664
|
-
if (!deps?.adapters?.length && !isTypescriptAdapterAvailable()) {
|
|
1665
|
-
return hardFailure(
|
|
1666
|
-
[
|
|
1667
|
-
{
|
|
1668
|
-
code: "adapter_unavailable",
|
|
1669
|
-
message: "TypeScript language adapter could not be loaded; ensure `typescript` is installed with @structured-vibe-coding/core."
|
|
1670
|
-
}
|
|
1671
|
-
],
|
|
1672
|
-
"Language adapter unavailable"
|
|
1673
|
-
);
|
|
1674
|
-
}
|
|
1675
|
-
const registry = new AdapterRegistry(deps?.adapters);
|
|
1676
1956
|
const { plan, gaps: focusGaps } = await resolveFocus(request);
|
|
1677
1957
|
gaps.push(...focusGaps);
|
|
1678
|
-
const
|
|
1679
|
-
if (!
|
|
1680
|
-
if (!plan.keywords.length) {
|
|
1681
|
-
gaps.push({
|
|
1682
|
-
code: "no_focus",
|
|
1683
|
-
message: "No focus resolved. Pass --path, --symbol, and/or --query, or run with an active change."
|
|
1684
|
-
});
|
|
1685
|
-
return hardFailure(gaps, "No focus resolved", {
|
|
1686
|
-
focusSymbols: plan.seedSymbols,
|
|
1687
|
-
keywords: plan.keywords
|
|
1688
|
-
});
|
|
1689
|
-
}
|
|
1690
|
-
}
|
|
1691
|
-
if (!plan.candidatePaths.length && !plan.seedPaths.length) {
|
|
1692
|
-
if (!hasFocus) {
|
|
1693
|
-
gaps.push({
|
|
1694
|
-
code: "no_focus",
|
|
1695
|
-
message: "No focus resolved. Pass --path, --symbol, and/or --query, or run with an active change."
|
|
1696
|
-
});
|
|
1697
|
-
return hardFailure(gaps, "No focus resolved", {
|
|
1698
|
-
focusSymbols: plan.seedSymbols,
|
|
1699
|
-
keywords: plan.keywords
|
|
1700
|
-
});
|
|
1701
|
-
}
|
|
1958
|
+
const hasExplicit = Boolean(request.paths?.length) || Boolean(request.query?.trim()) || Boolean(request.symbols?.length) || Boolean(request.changeId) || plan.seedPaths.length > 0 || plan.candidatePaths.length > 0 || plan.keywords.length > 0;
|
|
1959
|
+
if (!hasExplicit) {
|
|
1702
1960
|
gaps.push({
|
|
1703
1961
|
code: "no_focus",
|
|
1704
|
-
message: "
|
|
1962
|
+
message: "No focus resolved. Pass --path, --symbol, and/or --query, or run with an active change."
|
|
1705
1963
|
});
|
|
1706
|
-
return hardFailure(gaps, "No
|
|
1964
|
+
return hardFailure(gaps, "No focus resolved", {
|
|
1707
1965
|
focusSymbols: plan.seedSymbols,
|
|
1708
|
-
keywords: plan.keywords
|
|
1709
|
-
focusPaths: plan.seedPaths
|
|
1966
|
+
keywords: plan.keywords
|
|
1710
1967
|
});
|
|
1711
1968
|
}
|
|
1712
|
-
let
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1969
|
+
let searchConfig;
|
|
1970
|
+
try {
|
|
1971
|
+
searchConfig = await loadSearchConfig(projectRoot);
|
|
1972
|
+
} catch {
|
|
1973
|
+
return hardFailure(
|
|
1974
|
+
[{ code: "parse_error", message: "search.yaml is invalid" }],
|
|
1975
|
+
"search.yaml is invalid"
|
|
1976
|
+
);
|
|
1977
|
+
}
|
|
1978
|
+
const config = await loadConfig(projectRoot);
|
|
1979
|
+
let changeTitle = plan.changeId;
|
|
1980
|
+
let stage;
|
|
1981
|
+
if (request.changeId) {
|
|
1982
|
+
const mp = join6(changePath(projectRoot, config, request.changeId), "meta.yaml");
|
|
1983
|
+
if (await pathExists(mp)) {
|
|
1984
|
+
const meta = await readYaml(mp);
|
|
1985
|
+
changeTitle = meta.title ?? request.changeId;
|
|
1986
|
+
stage = meta.stage;
|
|
1987
|
+
}
|
|
1988
|
+
}
|
|
1989
|
+
const runtime = {
|
|
1990
|
+
embedder: deps?.embedder,
|
|
1991
|
+
embedderFactory: deps?.embedderFactory
|
|
1992
|
+
};
|
|
1993
|
+
await createBuiltinProvider(projectRoot, config, runtime).index();
|
|
1994
|
+
const budget = request.caps?.maxTokensApprox ?? budgetForStage(searchConfig, stage);
|
|
1995
|
+
const k = request.caps?.maxSlices ?? searchConfig.k;
|
|
1996
|
+
let query = request.query?.trim() ? request.query : "";
|
|
1997
|
+
if (!query && request.symbols?.length) query = request.symbols.join(" ");
|
|
1998
|
+
if (!query && changeTitle && request.changeId) query = changeTitle;
|
|
1999
|
+
const activeChange = await resolveActiveDirectory(projectRoot, config);
|
|
2000
|
+
const cacheRequest = {
|
|
2001
|
+
projectRoot,
|
|
2002
|
+
query: query.trim() ? query : "",
|
|
2003
|
+
channel: "context",
|
|
2004
|
+
scope: "code",
|
|
2005
|
+
k,
|
|
2006
|
+
budget,
|
|
2007
|
+
embeddings: searchConfig.embeddings,
|
|
2008
|
+
activeChange,
|
|
2009
|
+
filterPaths: request.paths ?? [],
|
|
2010
|
+
filterSymbols: request.symbols ?? [],
|
|
2011
|
+
scopes: ["code"]
|
|
2012
|
+
};
|
|
2013
|
+
try {
|
|
2014
|
+
const cached = readPackedCache(cacheRequest);
|
|
2015
|
+
if (cached) return finishContext(cached.hits, cached.text, cached.warning);
|
|
2016
|
+
} catch {
|
|
2017
|
+
}
|
|
2018
|
+
let hits;
|
|
2019
|
+
let mode = "bm25";
|
|
2020
|
+
let hybridScopes = "";
|
|
2021
|
+
if (query.trim()) {
|
|
2022
|
+
const embedder = await resolveEmbedder(projectRoot, searchConfig.embeddings, runtime);
|
|
2023
|
+
const embedded = await embedSearchQuery(embedder, searchConfig.embeddings, query);
|
|
2024
|
+
const ranked = await rankScope(projectRoot, query, "code", k, {
|
|
2025
|
+
embeddings: searchConfig.embeddings,
|
|
2026
|
+
queryVec: embedded.queryVec,
|
|
2027
|
+
failure: embedded.failure
|
|
1721
2028
|
});
|
|
2029
|
+
hits = ranked.hits;
|
|
2030
|
+
if (ranked.hybrid) hybridScopes = "code";
|
|
2031
|
+
if (ranked.hits.some((hit) => hit.rankedBy === "rrf")) mode = "rrf";
|
|
2032
|
+
if (ranked.warning) {
|
|
2033
|
+
gaps.push({ code: "embeddings_unavailable", message: EMBEDDINGS_UNAVAILABLE });
|
|
2034
|
+
}
|
|
2035
|
+
} else {
|
|
2036
|
+
hits = await listScopeHits(projectRoot, "code");
|
|
1722
2037
|
}
|
|
1723
|
-
const
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
2038
|
+
const fileArgs = [];
|
|
2039
|
+
const dirArgs = [];
|
|
2040
|
+
for (const input of request.paths ?? []) {
|
|
2041
|
+
const rel = toRepoPath(projectRoot, input);
|
|
2042
|
+
let info = null;
|
|
2043
|
+
try {
|
|
2044
|
+
info = await stat(join6(projectRoot, rel));
|
|
2045
|
+
} catch {
|
|
2046
|
+
info = null;
|
|
2047
|
+
}
|
|
2048
|
+
if (!info) {
|
|
2049
|
+
gaps.push({ code: "path_missing", message: `Path not found: ${rel}`, path: rel });
|
|
2050
|
+
continue;
|
|
2051
|
+
}
|
|
2052
|
+
if (info.isDirectory()) {
|
|
2053
|
+
dirArgs.push(rel);
|
|
2054
|
+
continue;
|
|
2055
|
+
}
|
|
2056
|
+
fileArgs.push(rel);
|
|
2057
|
+
const ext = extname3(rel).toLowerCase();
|
|
2058
|
+
if (isSecretPath(rel)) {
|
|
1729
2059
|
gaps.push({
|
|
1730
2060
|
code: "path_denied_secret",
|
|
1731
2061
|
message: `Skipped potential secret path: ${rel}`,
|
|
@@ -1733,169 +2063,150 @@ async function generateCodeContext(request, deps) {
|
|
|
1733
2063
|
});
|
|
1734
2064
|
continue;
|
|
1735
2065
|
}
|
|
1736
|
-
if (
|
|
1737
|
-
gaps.push({
|
|
1738
|
-
code: "path_ignored",
|
|
1739
|
-
message: `Skipped ignored path: ${rel}`,
|
|
1740
|
-
path: rel
|
|
1741
|
-
});
|
|
2066
|
+
if (isIgnoredPath(rel)) {
|
|
2067
|
+
gaps.push({ code: "path_ignored", message: `Skipped ignored path: ${rel}`, path: rel });
|
|
1742
2068
|
continue;
|
|
1743
2069
|
}
|
|
1744
|
-
|
|
1745
|
-
if (!await pathExists(abs)) {
|
|
2070
|
+
if (ext === ".go") {
|
|
1746
2071
|
gaps.push({
|
|
1747
|
-
code: "
|
|
1748
|
-
message: `
|
|
2072
|
+
code: "unsupported_language",
|
|
2073
|
+
message: `No structure adapter for ${ext}; skipped.`,
|
|
1749
2074
|
path: rel
|
|
1750
2075
|
});
|
|
1751
|
-
continue;
|
|
1752
2076
|
}
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
}
|
|
1774
|
-
let extract;
|
|
1775
|
-
try {
|
|
1776
|
-
extract = await adapter.extract({
|
|
1777
|
-
filePath: normalize5(rel).replace(/\\/g, "/"),
|
|
1778
|
-
source,
|
|
1779
|
-
projectRoot
|
|
1780
|
-
});
|
|
1781
|
-
} catch (err) {
|
|
1782
|
-
gaps.push({
|
|
1783
|
-
code: "parse_error",
|
|
1784
|
-
message: `Parse failed for ${rel} (continuing with other files).`,
|
|
1785
|
-
path: rel,
|
|
1786
|
-
detail: err instanceof Error ? err.message : String(err)
|
|
1787
|
-
});
|
|
1788
|
-
continue;
|
|
1789
|
-
}
|
|
1790
|
-
if (!extract.ok) {
|
|
2077
|
+
}
|
|
2078
|
+
if (request.paths?.length) {
|
|
2079
|
+
hits = hits.filter((hit) => pathKept(hit.path, fileArgs, dirArgs));
|
|
2080
|
+
}
|
|
2081
|
+
const names = request.symbols ?? [];
|
|
2082
|
+
if (names.length) {
|
|
2083
|
+
hits = hits.filter((hit) => symbolKept(hit.symbol, names));
|
|
2084
|
+
hits.sort((a, b) => {
|
|
2085
|
+
const aExact = names.some((name) => a.symbol === name) ? 0 : 1;
|
|
2086
|
+
const bExact = names.some((name) => b.symbol === name) ? 0 : 1;
|
|
2087
|
+
return aExact - bExact;
|
|
2088
|
+
});
|
|
2089
|
+
}
|
|
2090
|
+
if (!query.trim()) hits = hits.slice(0, k);
|
|
2091
|
+
const indexed = new Set((await listScopeHits(projectRoot, "code")).map((hit) => hit.path));
|
|
2092
|
+
for (const rel of fileArgs) {
|
|
2093
|
+
const ext = extname3(rel).toLowerCase();
|
|
2094
|
+
if (ext === ".go") continue;
|
|
2095
|
+
if (isSecretPath(rel) || isIgnoredPath(rel)) continue;
|
|
2096
|
+
if (!indexed.has(rel) && ext) {
|
|
1791
2097
|
gaps.push({
|
|
1792
2098
|
code: "parse_error",
|
|
1793
2099
|
message: `Parse failed for ${rel} (continuing with other files).`,
|
|
1794
|
-
path: rel,
|
|
1795
|
-
detail: extract.errors?.join("; ")
|
|
1796
|
-
});
|
|
1797
|
-
if (!extract.symbols.length) continue;
|
|
1798
|
-
}
|
|
1799
|
-
extract.symbols = extract.symbols.map((s) => ({
|
|
1800
|
-
...s,
|
|
1801
|
-
filePath: normalize5(rel).replace(/\\/g, "/")
|
|
1802
|
-
}));
|
|
1803
|
-
if (!extract.symbols.length && extract.ok) {
|
|
1804
|
-
gaps.push({
|
|
1805
|
-
code: "empty_extract",
|
|
1806
|
-
message: `No symbols extracted from ${rel}.`,
|
|
1807
2100
|
path: rel
|
|
1808
2101
|
});
|
|
1809
2102
|
}
|
|
1810
|
-
graph.add(normalize5(rel).replace(/\\/g, "/"), source, extract);
|
|
1811
|
-
filesAnalyzed++;
|
|
1812
|
-
symbolsExtracted += extract.symbols.length;
|
|
1813
2103
|
}
|
|
1814
|
-
const
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
}
|
|
1818
|
-
|
|
1819
|
-
truncated =
|
|
2104
|
+
const packed = packHits({ preamble: "", hits, budget });
|
|
2105
|
+
try {
|
|
2106
|
+
writePackedCache(cacheRequest, packed, hits, mode, hybridScopes);
|
|
2107
|
+
} catch {
|
|
2108
|
+
}
|
|
2109
|
+
const truncated = hits.length > packed.hits.length || packed.hits.some((hit) => hit.form === "signature");
|
|
2110
|
+
if (truncated) {
|
|
2111
|
+
gaps.push({
|
|
2112
|
+
code: "cap_truncated",
|
|
2113
|
+
message: "Packed under the token budget. Lower hits are signatures."
|
|
2114
|
+
});
|
|
2115
|
+
}
|
|
2116
|
+
const slices = packed.hits.map((hit) => ({
|
|
2117
|
+
id: `${hit.path}:${hit.startLine}-${hit.endLine}:${hit.symbol}`,
|
|
2118
|
+
filePath: hit.path,
|
|
2119
|
+
startLine: hit.startLine,
|
|
2120
|
+
endLine: hit.endLine,
|
|
2121
|
+
symbolName: hit.symbol,
|
|
2122
|
+
reason: hit.form === "full" ? "keyword full" : "keyword signature",
|
|
2123
|
+
body: hit.form === "full" ? hit.body : hit.signature
|
|
2124
|
+
}));
|
|
1820
2125
|
const summary = {
|
|
1821
2126
|
focusPaths: plan.seedPaths,
|
|
1822
2127
|
focusSymbols: plan.seedSymbols,
|
|
1823
2128
|
keywords: plan.keywords,
|
|
1824
|
-
filesAnalyzed,
|
|
1825
|
-
symbolsExtracted,
|
|
1826
|
-
slicesEmitted:
|
|
2129
|
+
filesAnalyzed: new Set(hits.map((hit) => hit.path)).size,
|
|
2130
|
+
symbolsExtracted: hits.length,
|
|
2131
|
+
slicesEmitted: slices.length,
|
|
1827
2132
|
truncated
|
|
1828
2133
|
};
|
|
1829
|
-
let changeTitle = plan.changeId;
|
|
1830
|
-
if (plan.changeId) {
|
|
1831
|
-
try {
|
|
1832
|
-
const { readYaml: readYaml2 } = await import("./fs-J6DIV6CX.js");
|
|
1833
|
-
const metaPath = join6(
|
|
1834
|
-
projectRoot,
|
|
1835
|
-
// best-effort: changes/<id>/meta.yaml via relative walk
|
|
1836
|
-
// Prefer using change path from resolve — re-read lightly
|
|
1837
|
-
"changes",
|
|
1838
|
-
plan.changeId,
|
|
1839
|
-
"meta.yaml"
|
|
1840
|
-
);
|
|
1841
|
-
const { loadConfig: loadConfig2 } = await import("./config-X5VDBVR4.js");
|
|
1842
|
-
const { changePath: changePath2 } = await import("./paths-64GA32IZ.js");
|
|
1843
|
-
const config = await loadConfig2(projectRoot);
|
|
1844
|
-
const mp = join6(changePath2(projectRoot, config, plan.changeId), "meta.yaml");
|
|
1845
|
-
if (await pathExists(mp)) {
|
|
1846
|
-
const meta = await readYaml2(mp);
|
|
1847
|
-
changeTitle = meta.title ?? plan.changeId;
|
|
1848
|
-
} else if (await pathExists(metaPath)) {
|
|
1849
|
-
const meta = await readYaml2(metaPath);
|
|
1850
|
-
changeTitle = meta.title ?? plan.changeId;
|
|
1851
|
-
}
|
|
1852
|
-
} catch {
|
|
1853
|
-
}
|
|
1854
|
-
}
|
|
1855
2134
|
const formatted = formatMarkdown({
|
|
1856
2135
|
plan,
|
|
1857
2136
|
summary,
|
|
1858
|
-
slices
|
|
2137
|
+
slices,
|
|
1859
2138
|
gaps,
|
|
1860
2139
|
changeTitle,
|
|
1861
|
-
|
|
1862
|
-
maxTokensApprox: caps.maxTokensApprox
|
|
1863
|
-
});
|
|
1864
|
-
if (formatted.truncated && !summary.truncated) {
|
|
1865
|
-
summary.truncated = true;
|
|
1866
|
-
if (!gaps.some((g) => g.code === "cap_truncated")) {
|
|
1867
|
-
gaps.push({
|
|
1868
|
-
code: "cap_truncated",
|
|
1869
|
-
message: "Output truncated at configured caps (maxOutputLines/maxTokensApprox)."
|
|
1870
|
-
});
|
|
1871
|
-
}
|
|
1872
|
-
}
|
|
1873
|
-
const finalFormat = formatMarkdown({
|
|
1874
|
-
plan,
|
|
1875
|
-
summary,
|
|
1876
|
-
slices: sliceResult.slices,
|
|
1877
|
-
gaps,
|
|
1878
|
-
changeTitle,
|
|
1879
|
-
maxOutputLines: caps.maxOutputLines,
|
|
1880
|
-
maxTokensApprox: caps.maxTokensApprox
|
|
2140
|
+
packedText: packed.text
|
|
1881
2141
|
});
|
|
1882
2142
|
let artifactPath;
|
|
1883
2143
|
if (request.writeArtifactTo) {
|
|
1884
|
-
await writeText(request.writeArtifactTo,
|
|
2144
|
+
await writeText(request.writeArtifactTo, formatted.markdown);
|
|
1885
2145
|
artifactPath = request.writeArtifactTo;
|
|
1886
2146
|
}
|
|
1887
2147
|
return {
|
|
1888
2148
|
ok: true,
|
|
1889
|
-
markdown:
|
|
2149
|
+
markdown: formatted.markdown,
|
|
1890
2150
|
summary,
|
|
1891
|
-
slices
|
|
2151
|
+
slices,
|
|
1892
2152
|
gaps,
|
|
1893
2153
|
artifactPath
|
|
1894
2154
|
};
|
|
2155
|
+
async function finishContext(packedHits, packedText, warning) {
|
|
2156
|
+
if (warning) {
|
|
2157
|
+
gaps.push({ code: "embeddings_unavailable", message: EMBEDDINGS_UNAVAILABLE });
|
|
2158
|
+
}
|
|
2159
|
+
const truncated2 = packedHits.some((hit) => hit.form === "signature");
|
|
2160
|
+
if (truncated2) {
|
|
2161
|
+
gaps.push({
|
|
2162
|
+
code: "cap_truncated",
|
|
2163
|
+
message: "Packed under the token budget. Lower hits are signatures."
|
|
2164
|
+
});
|
|
2165
|
+
}
|
|
2166
|
+
const slices2 = packedHits.map((hit) => ({
|
|
2167
|
+
id: `${hit.path}:${hit.startLine}-${hit.endLine}:${hit.symbol}`,
|
|
2168
|
+
filePath: hit.path,
|
|
2169
|
+
startLine: hit.startLine,
|
|
2170
|
+
endLine: hit.endLine,
|
|
2171
|
+
symbolName: hit.symbol,
|
|
2172
|
+
reason: hit.form === "full" ? "keyword full" : "keyword signature",
|
|
2173
|
+
body: hit.form === "full" ? hit.body : hit.signature
|
|
2174
|
+
}));
|
|
2175
|
+
const summary2 = {
|
|
2176
|
+
focusPaths: plan.seedPaths,
|
|
2177
|
+
focusSymbols: plan.seedSymbols,
|
|
2178
|
+
keywords: plan.keywords,
|
|
2179
|
+
filesAnalyzed: new Set(packedHits.map((hit) => hit.path)).size,
|
|
2180
|
+
symbolsExtracted: packedHits.length,
|
|
2181
|
+
slicesEmitted: slices2.length,
|
|
2182
|
+
truncated: truncated2
|
|
2183
|
+
};
|
|
2184
|
+
const formatted2 = formatMarkdown({
|
|
2185
|
+
plan,
|
|
2186
|
+
summary: summary2,
|
|
2187
|
+
slices: slices2,
|
|
2188
|
+
gaps,
|
|
2189
|
+
changeTitle,
|
|
2190
|
+
packedText
|
|
2191
|
+
});
|
|
2192
|
+
let artifactPath2;
|
|
2193
|
+
if (request.writeArtifactTo) {
|
|
2194
|
+
await writeText(request.writeArtifactTo, formatted2.markdown);
|
|
2195
|
+
artifactPath2 = request.writeArtifactTo;
|
|
2196
|
+
}
|
|
2197
|
+
return {
|
|
2198
|
+
ok: true,
|
|
2199
|
+
markdown: formatted2.markdown,
|
|
2200
|
+
summary: summary2,
|
|
2201
|
+
slices: slices2,
|
|
2202
|
+
gaps,
|
|
2203
|
+
artifactPath: artifactPath2
|
|
2204
|
+
};
|
|
2205
|
+
}
|
|
1895
2206
|
}
|
|
1896
2207
|
|
|
1897
2208
|
// src/refine.ts
|
|
1898
|
-
import { join as join7 } from "pathe";
|
|
2209
|
+
import { join as join7, relative as relative3 } from "pathe";
|
|
1899
2210
|
function stageArtifactRefs(ctx, stageId) {
|
|
1900
2211
|
const stage = getStage(ctx.workflow, ctx.meta, stageId);
|
|
1901
2212
|
if (!stage) return [];
|
|
@@ -1906,10 +2217,10 @@ function stageArtifactRefs(ctx, stageId) {
|
|
|
1906
2217
|
absPath: join7(ctx.path, a.path)
|
|
1907
2218
|
}));
|
|
1908
2219
|
}
|
|
1909
|
-
function listMarkdown(refs) {
|
|
2220
|
+
function listMarkdown(refs, displayDir) {
|
|
1910
2221
|
if (!refs.length) return "_None._";
|
|
1911
2222
|
return refs.map(
|
|
1912
|
-
(r) => `- \`${r.path}\` (stage \`${r.stageId}\`${r.required ? ", required" : ""}) \u2192 \`${r.
|
|
2223
|
+
(r) => `- \`${r.path}\` (stage \`${r.stageId}\`${r.required ? ", required" : ""}) \u2192 \`${displayDir}/${r.path}\``
|
|
1913
2224
|
).join("\n");
|
|
1914
2225
|
}
|
|
1915
2226
|
async function buildRefinePlan(opts) {
|
|
@@ -1949,8 +2260,13 @@ async function buildRefinePlan(opts) {
|
|
|
1949
2260
|
const briefPath = join7(ctx.path, "refine-brief.md");
|
|
1950
2261
|
const reportPath = join7(ctx.path, "quality-report.md");
|
|
1951
2262
|
const openItemsPath = join7(ctx.path, "open-items.md");
|
|
2263
|
+
const paths = guidancePaths(opts.config);
|
|
2264
|
+
const briefRel = `${ctx.displayPath}/refine-brief.md`;
|
|
2265
|
+
const reportRel = `${ctx.displayPath}/quality-report.md`;
|
|
2266
|
+
const openItemsRel = `${ctx.displayPath}/open-items.md`;
|
|
1952
2267
|
const briefMarkdown = buildRefineBriefMarkdown({
|
|
1953
2268
|
ctx,
|
|
2269
|
+
config: opts.config,
|
|
1954
2270
|
focusStageId,
|
|
1955
2271
|
focusStageTitle: focusStage.title ?? focusStageId,
|
|
1956
2272
|
mode,
|
|
@@ -1958,19 +2274,19 @@ async function buildRefinePlan(opts) {
|
|
|
1958
2274
|
focusArtifacts,
|
|
1959
2275
|
priorArtifacts,
|
|
1960
2276
|
laterArtifacts,
|
|
1961
|
-
constitutionPath,
|
|
2277
|
+
constitutionPath: paths.constitution,
|
|
1962
2278
|
constitutionExists,
|
|
1963
|
-
reportPath,
|
|
1964
|
-
openItemsPath
|
|
2279
|
+
reportPath: reportRel,
|
|
2280
|
+
openItemsPath: openItemsRel
|
|
1965
2281
|
});
|
|
1966
2282
|
const kickoff = [
|
|
1967
2283
|
`SDD: ${mode} stage \`${focusStageId}\`.`,
|
|
1968
2284
|
`Title: ${ctx.meta.title}`,
|
|
1969
2285
|
`Change: ${ctx.id}`,
|
|
1970
|
-
`Read first: ${
|
|
2286
|
+
`Read first: ${briefRel}`,
|
|
1971
2287
|
`Also: .sdd/protocol.md, .sdd/active-context.md`,
|
|
1972
|
-
constitutionExists ? `Constitution (READ-ONLY): ${
|
|
1973
|
-
mode === "analyze" ? `Mode ANALYZE: write findings to ${
|
|
2288
|
+
constitutionExists ? `Constitution (READ-ONLY): ${paths.constitution}` : `No constitution.md found (optional).`,
|
|
2289
|
+
mode === "analyze" ? `Mode ANALYZE: write findings to ${reportRel} only \u2014 do not edit pack artifacts.` : `Mode REFINE: improve focus-stage artifacts; impact-scan prior pack files (fix mechanical inconsistencies; highlight judgment calls). Never edit constitution. Do not run sdd next.`,
|
|
1974
2290
|
focusOnly ? `focusOnly=true: edit only focus-stage files (still read prior + constitution).` : `Prior artifacts may be auto-fixed for clear contradictions/term drift; scope changes \u2192 highlight + open-items (human accept).`
|
|
1975
2291
|
].join(" ");
|
|
1976
2292
|
return {
|
|
@@ -1997,6 +2313,7 @@ async function buildRefinePlan(opts) {
|
|
|
1997
2313
|
function buildRefineBriefMarkdown(opts) {
|
|
1998
2314
|
const {
|
|
1999
2315
|
ctx,
|
|
2316
|
+
config,
|
|
2000
2317
|
focusStageId,
|
|
2001
2318
|
focusStageTitle,
|
|
2002
2319
|
mode,
|
|
@@ -2009,6 +2326,7 @@ function buildRefineBriefMarkdown(opts) {
|
|
|
2009
2326
|
reportPath,
|
|
2010
2327
|
openItemsPath
|
|
2011
2328
|
} = opts;
|
|
2329
|
+
const paths = guidancePaths(config);
|
|
2012
2330
|
return `# Refine brief (auto-generated)
|
|
2013
2331
|
|
|
2014
2332
|
> Change: **${ctx.meta.title}** \xB7 \`${ctx.id}\`
|
|
@@ -2021,9 +2339,10 @@ function buildRefineBriefMarkdown(opts) {
|
|
|
2021
2339
|
You are the **SDD refine** agent (one agent, mode \`${mode}\`).
|
|
2022
2340
|
|
|
2023
2341
|
1. Work by **stage**, not hard-coded filenames. Focus stage resolves to the files below.
|
|
2024
|
-
2. **Never edit**
|
|
2025
|
-
3.
|
|
2026
|
-
4.
|
|
2342
|
+
2. **Never edit** \`${paths.constitution}\` (or any constitution path). Read it as constraints only.
|
|
2343
|
+
3. Stage-scoped refine brief lives at \`${paths.changes}/<id>/refine-brief.md\`.
|
|
2344
|
+
4. **Do not** run \`sdd next\`, \`sdd complete\`, or change \`meta.yaml\` stage/status.
|
|
2345
|
+
5. Process is **never blocked** by findings \u2014 report and improve docs only.
|
|
2027
2346
|
|
|
2028
2347
|
## Constitution (READ-ONLY)
|
|
2029
2348
|
|
|
@@ -2031,11 +2350,11 @@ ${constitutionExists ? `- Path: \`${constitutionPath}\` \u2014 **read only**. Co
|
|
|
2031
2350
|
|
|
2032
2351
|
## Focus stage artifacts (primary ${mode === "analyze" ? "review" : "edit"} target)
|
|
2033
2352
|
|
|
2034
|
-
${listMarkdown(focusArtifacts)}
|
|
2353
|
+
${listMarkdown(focusArtifacts, ctx.displayPath)}
|
|
2035
2354
|
|
|
2036
2355
|
## Prior stage artifacts (pack trail \u2014 always read)
|
|
2037
2356
|
|
|
2038
|
-
${listMarkdown(priorArtifacts)}
|
|
2357
|
+
${listMarkdown(priorArtifacts, ctx.displayPath)}
|
|
2039
2358
|
|
|
2040
2359
|
${focusOnly ? `**focusOnly:** Do **not** edit prior files. Still read them for consistency. Highlight conflicts for the human.` : `**Auto prior impact (required):** After refining the focus stage, use search (\`rg\` / grep) across the change pack for terms, claims, and names you changed. For each hit on a **prior** artifact:
|
|
2041
2360
|
- **Fix** mechanical inconsistencies (contradictions, renamed terms, outdated references that clearly disagree with the refined focus).
|
|
@@ -2044,7 +2363,7 @@ ${focusOnly ? `**focusOnly:** Do **not** edit prior files. Still read them for c
|
|
|
2044
2363
|
|
|
2045
2364
|
## Later stage artifacts (usually leave alone)
|
|
2046
2365
|
|
|
2047
|
-
${listMarkdown(laterArtifacts)}
|
|
2366
|
+
${listMarkdown(laterArtifacts, ctx.displayPath)}
|
|
2048
2367
|
|
|
2049
2368
|
Do **not** invent content for later stages that were not materialized yet. If a later file exists and contradicts the refined focus, **highlight** only (unless the human asked to align the whole pack).
|
|
2050
2369
|
|
|
@@ -2078,11 +2397,12 @@ Do **not** invent content for later stages that were not materialized yet. If a
|
|
|
2078
2397
|
async function writeRefineBrief(plan, projectRoot) {
|
|
2079
2398
|
await writeText(plan.briefPath, plan.briefMarkdown);
|
|
2080
2399
|
const pointer = join7(sddRoot(projectRoot), "refine-brief.md");
|
|
2400
|
+
const briefRel = relative3(projectRoot, plan.briefPath);
|
|
2081
2401
|
await writeText(
|
|
2082
2402
|
pointer,
|
|
2083
2403
|
`# Refine brief pointer
|
|
2084
2404
|
|
|
2085
|
-
Active refine brief: \`${
|
|
2405
|
+
Active refine brief: \`${briefRel}\`
|
|
2086
2406
|
|
|
2087
2407
|
Run: \`sdd refine${plan.focusStageId ? ` ${plan.focusStageId}` : ""}\`
|
|
2088
2408
|
|
|
@@ -2090,79 +2410,1644 @@ Run: \`sdd refine${plan.focusStageId ? ` ${plan.focusStageId}` : ""}\`
|
|
|
2090
2410
|
);
|
|
2091
2411
|
return plan.briefPath;
|
|
2092
2412
|
}
|
|
2413
|
+
|
|
2414
|
+
// src/role/handoff.ts
|
|
2415
|
+
import { join as join10 } from "pathe";
|
|
2416
|
+
import { parse as parseYaml3, stringify as stringifyYaml2 } from "yaml";
|
|
2417
|
+
|
|
2418
|
+
// src/role/profile.ts
|
|
2419
|
+
import { readFile, writeFile } from "fs/promises";
|
|
2420
|
+
import { join as join8 } from "pathe";
|
|
2421
|
+
import { parse as parseYaml2, stringify as stringifyYaml } from "yaml";
|
|
2422
|
+
|
|
2423
|
+
// src/role/types.ts
|
|
2424
|
+
var ROLE_TEXT = {
|
|
2425
|
+
needsJob: "A role needs a job.",
|
|
2426
|
+
notFound: "Role not found.",
|
|
2427
|
+
exists: "Role already exists.",
|
|
2428
|
+
badName: "A role name is not valid.",
|
|
2429
|
+
self: "A role cannot review itself.",
|
|
2430
|
+
x: "X is not a role tool.",
|
|
2431
|
+
needsSection: "This needs an accepted section before a draft.",
|
|
2432
|
+
noModel: "No role model is configured.",
|
|
2433
|
+
cannot: "A role cannot do that.",
|
|
2434
|
+
askFirst: "Ask first. This stays stopped.",
|
|
2435
|
+
choice: "Allow once, Always allow, or Deny.",
|
|
2436
|
+
unavailable: "This action is not available in this slice.",
|
|
2437
|
+
expired: "Expired. The action did not run.",
|
|
2438
|
+
runNotFound: "Run not found.",
|
|
2439
|
+
badYaml: "role.yaml is invalid"
|
|
2440
|
+
};
|
|
2441
|
+
|
|
2442
|
+
// src/role/profile.ts
|
|
2443
|
+
var NAME_LIMIT = 80;
|
|
2444
|
+
function canonicalRoleName(input) {
|
|
2445
|
+
const name = input.trim();
|
|
2446
|
+
if (!name || name === "." || name === "..") return null;
|
|
2447
|
+
if (name.length > NAME_LIMIT) return null;
|
|
2448
|
+
if (name.includes("/") || name.includes("\\") || name.includes("\0")) return null;
|
|
2449
|
+
return name;
|
|
2450
|
+
}
|
|
2451
|
+
function roleFilePath(root, name) {
|
|
2452
|
+
return join8(root, ".sdd", "roles", `${name}.md`);
|
|
2453
|
+
}
|
|
2454
|
+
function conversationDir(root, name) {
|
|
2455
|
+
return join8(root, ".sdd", "role-runs", "conversation", name);
|
|
2456
|
+
}
|
|
2457
|
+
function memoryDir2(root, name) {
|
|
2458
|
+
return join8(root, ".sdd", "role-runs", "memory", name);
|
|
2459
|
+
}
|
|
2460
|
+
function allowFilePath(root) {
|
|
2461
|
+
return join8(root, ".sdd", "role-runs", "allow.json");
|
|
2462
|
+
}
|
|
2463
|
+
function ok(stdout = "") {
|
|
2464
|
+
return { exitCode: 0, stdout };
|
|
2465
|
+
}
|
|
2466
|
+
function fail(stdout) {
|
|
2467
|
+
return { exitCode: 1, stdout };
|
|
2468
|
+
}
|
|
2469
|
+
function asText(value) {
|
|
2470
|
+
return typeof value === "string" ? value.trim() : "";
|
|
2471
|
+
}
|
|
2472
|
+
function parseRoleDocument(name, raw) {
|
|
2473
|
+
const text = raw.replace(/^\uFEFF/, "");
|
|
2474
|
+
const nl = text.indexOf("\n");
|
|
2475
|
+
if (nl < 0 || text.slice(0, nl).trim() !== "---") return null;
|
|
2476
|
+
const rest = text.slice(nl + 1);
|
|
2477
|
+
const close = /\r?\n---\s*(?:\r?\n|$)/.exec(rest);
|
|
2478
|
+
if (!close) return null;
|
|
2479
|
+
let data;
|
|
2480
|
+
try {
|
|
2481
|
+
data = parseYaml2(rest.slice(0, close.index));
|
|
2482
|
+
} catch {
|
|
2483
|
+
return null;
|
|
2484
|
+
}
|
|
2485
|
+
if (!data || typeof data !== "object" || Array.isArray(data)) return null;
|
|
2486
|
+
const rec = data;
|
|
2487
|
+
const body = rest.slice(close.index + close[0].length).trim();
|
|
2488
|
+
return {
|
|
2489
|
+
name,
|
|
2490
|
+
label: asText(rec.label),
|
|
2491
|
+
job: asText(rec.job),
|
|
2492
|
+
description: body,
|
|
2493
|
+
askFirst: rec.ask_first === true,
|
|
2494
|
+
pinned: rec.pinned === true,
|
|
2495
|
+
hidden: rec.hidden === true
|
|
2496
|
+
};
|
|
2497
|
+
}
|
|
2498
|
+
function renderRole(role) {
|
|
2499
|
+
const front = stringifyYaml(
|
|
2500
|
+
{
|
|
2501
|
+
name: role.name,
|
|
2502
|
+
label: role.label,
|
|
2503
|
+
job: role.job,
|
|
2504
|
+
ask_first: role.askFirst,
|
|
2505
|
+
pinned: role.pinned,
|
|
2506
|
+
hidden: role.hidden
|
|
2507
|
+
},
|
|
2508
|
+
{ lineWidth: 100 }
|
|
2509
|
+
).trimEnd();
|
|
2510
|
+
const description = role.description.trim();
|
|
2511
|
+
if (!description) return `---
|
|
2512
|
+
${front}
|
|
2513
|
+
---
|
|
2514
|
+
`;
|
|
2515
|
+
return `---
|
|
2516
|
+
${front}
|
|
2517
|
+
---
|
|
2518
|
+
${description}
|
|
2519
|
+
`;
|
|
2520
|
+
}
|
|
2521
|
+
async function readRole(root, name) {
|
|
2522
|
+
const canonical = canonicalRoleName(name);
|
|
2523
|
+
if (!canonical) return null;
|
|
2524
|
+
const path = roleFilePath(root, canonical);
|
|
2525
|
+
if (!await pathExists(path)) return null;
|
|
2526
|
+
try {
|
|
2527
|
+
return parseRoleDocument(canonical, await readText(path));
|
|
2528
|
+
} catch {
|
|
2529
|
+
return null;
|
|
2530
|
+
}
|
|
2531
|
+
}
|
|
2532
|
+
async function writeRole(root, role) {
|
|
2533
|
+
await writeText(roleFilePath(root, role.name), renderRole(role));
|
|
2534
|
+
}
|
|
2535
|
+
async function createRole(root, input) {
|
|
2536
|
+
const name = canonicalRoleName(input.name);
|
|
2537
|
+
if (!name) return fail(ROLE_TEXT.badName);
|
|
2538
|
+
const job = input.job.trim();
|
|
2539
|
+
if (!job) return fail(ROLE_TEXT.needsJob);
|
|
2540
|
+
if (await pathExists(roleFilePath(root, name))) return fail(ROLE_TEXT.exists);
|
|
2541
|
+
await writeRole(root, {
|
|
2542
|
+
name,
|
|
2543
|
+
label: input.label.trim(),
|
|
2544
|
+
job,
|
|
2545
|
+
description: (input.description ?? "").trim(),
|
|
2546
|
+
askFirst: input.askFirst === true,
|
|
2547
|
+
pinned: false,
|
|
2548
|
+
hidden: false
|
|
2549
|
+
});
|
|
2550
|
+
return ok();
|
|
2551
|
+
}
|
|
2552
|
+
async function editRole(root, nameInput, patch) {
|
|
2553
|
+
const name = canonicalRoleName(nameInput);
|
|
2554
|
+
if (!name) return fail(ROLE_TEXT.badName);
|
|
2555
|
+
const role = await readRole(root, name);
|
|
2556
|
+
if (!role) return fail(ROLE_TEXT.notFound);
|
|
2557
|
+
if (patch.job !== void 0 && !patch.job.trim()) return fail(ROLE_TEXT.needsJob);
|
|
2558
|
+
if (patch.label !== void 0) role.label = patch.label.trim();
|
|
2559
|
+
if (patch.job !== void 0) role.job = patch.job.trim();
|
|
2560
|
+
if (patch.description !== void 0) role.description = patch.description.trim();
|
|
2561
|
+
if (patch.askFirst !== void 0) role.askFirst = patch.askFirst;
|
|
2562
|
+
await writeRole(root, role);
|
|
2563
|
+
return ok();
|
|
2564
|
+
}
|
|
2565
|
+
async function setFlag(root, nameInput, flag) {
|
|
2566
|
+
const name = canonicalRoleName(nameInput);
|
|
2567
|
+
if (!name) return fail(ROLE_TEXT.badName);
|
|
2568
|
+
const role = await readRole(root, name);
|
|
2569
|
+
if (!role) return fail(ROLE_TEXT.notFound);
|
|
2570
|
+
role[flag] = true;
|
|
2571
|
+
await writeRole(root, role);
|
|
2572
|
+
return ok();
|
|
2573
|
+
}
|
|
2574
|
+
function pinRole(root, name) {
|
|
2575
|
+
return setFlag(root, name, "pinned");
|
|
2576
|
+
}
|
|
2577
|
+
function hideRole(root, name) {
|
|
2578
|
+
return setFlag(root, name, "hidden");
|
|
2579
|
+
}
|
|
2580
|
+
async function showRoles(root, opts = {}) {
|
|
2581
|
+
if (opts.name !== void 0) {
|
|
2582
|
+
const name = canonicalRoleName(opts.name);
|
|
2583
|
+
if (!name) return fail(ROLE_TEXT.badName);
|
|
2584
|
+
const path = roleFilePath(root, name);
|
|
2585
|
+
if (!await pathExists(path) || !await readRole(root, name)) return fail(ROLE_TEXT.notFound);
|
|
2586
|
+
return ok((await readText(path)).replace(/\s*$/, ""));
|
|
2587
|
+
}
|
|
2588
|
+
const dir = join8(root, ".sdd", "roles");
|
|
2589
|
+
if (!await pathExists(dir)) return ok();
|
|
2590
|
+
const roles = [];
|
|
2591
|
+
for (const file of await listFiles(dir)) {
|
|
2592
|
+
if (!file.endsWith(".md")) continue;
|
|
2593
|
+
const role = await readRole(root, file.slice(0, -".md".length));
|
|
2594
|
+
if (!role) continue;
|
|
2595
|
+
if (role.hidden && !opts.all) continue;
|
|
2596
|
+
roles.push(role);
|
|
2597
|
+
}
|
|
2598
|
+
roles.sort((a, b) => {
|
|
2599
|
+
if (a.pinned !== b.pinned) return a.pinned ? -1 : 1;
|
|
2600
|
+
if (a.name < b.name) return -1;
|
|
2601
|
+
if (a.name > b.name) return 1;
|
|
2602
|
+
return 0;
|
|
2603
|
+
});
|
|
2604
|
+
return ok(roles.map((role) => `${role.name} ${role.label}`).join("\n"));
|
|
2605
|
+
}
|
|
2606
|
+
async function duplicateRole(root, nameInput) {
|
|
2607
|
+
const name = canonicalRoleName(nameInput);
|
|
2608
|
+
if (!name) return fail(ROLE_TEXT.badName);
|
|
2609
|
+
const source = await readRole(root, name);
|
|
2610
|
+
if (!source) return fail(ROLE_TEXT.notFound);
|
|
2611
|
+
const copyName = canonicalRoleName(`${name} copy`);
|
|
2612
|
+
if (!copyName) return fail(ROLE_TEXT.badName);
|
|
2613
|
+
if (await pathExists(roleFilePath(root, copyName))) return fail(ROLE_TEXT.exists);
|
|
2614
|
+
await writeRole(root, { ...source, name: copyName });
|
|
2615
|
+
return ok();
|
|
2616
|
+
}
|
|
2617
|
+
async function deleteRole(root, nameInput) {
|
|
2618
|
+
const name = canonicalRoleName(nameInput);
|
|
2619
|
+
if (!name) return fail(ROLE_TEXT.badName);
|
|
2620
|
+
const path = roleFilePath(root, name);
|
|
2621
|
+
if (!await pathExists(path)) return fail(ROLE_TEXT.notFound);
|
|
2622
|
+
await removePath(path);
|
|
2623
|
+
await removePath(conversationDir(root, name));
|
|
2624
|
+
await removePath(memoryDir2(root, name));
|
|
2625
|
+
await removeAllowKey(root, name);
|
|
2626
|
+
return ok();
|
|
2627
|
+
}
|
|
2628
|
+
async function removeAllowKey(root, name) {
|
|
2629
|
+
const path = allowFilePath(root);
|
|
2630
|
+
if (!await pathExists(path)) return;
|
|
2631
|
+
let data;
|
|
2632
|
+
try {
|
|
2633
|
+
data = JSON.parse(await readText(path));
|
|
2634
|
+
} catch {
|
|
2635
|
+
return;
|
|
2636
|
+
}
|
|
2637
|
+
if (!data || typeof data !== "object" || Array.isArray(data)) return;
|
|
2638
|
+
const rec = data;
|
|
2639
|
+
if (!(name in rec)) return;
|
|
2640
|
+
delete rec[name];
|
|
2641
|
+
await writeFile(path, `${JSON.stringify(rec, null, 2)}
|
|
2642
|
+
`, "utf8");
|
|
2643
|
+
}
|
|
2644
|
+
async function listConversationRoles(root) {
|
|
2645
|
+
return listDirs(join8(root, ".sdd", "role-runs", "conversation"));
|
|
2646
|
+
}
|
|
2647
|
+
async function readAllowFile(root) {
|
|
2648
|
+
const path = allowFilePath(root);
|
|
2649
|
+
if (!await pathExists(path)) return null;
|
|
2650
|
+
try {
|
|
2651
|
+
const data = JSON.parse(await readFile(path, "utf8"));
|
|
2652
|
+
if (!data || typeof data !== "object" || Array.isArray(data)) return null;
|
|
2653
|
+
return data;
|
|
2654
|
+
} catch {
|
|
2655
|
+
return null;
|
|
2656
|
+
}
|
|
2657
|
+
}
|
|
2658
|
+
|
|
2659
|
+
// src/role/run.ts
|
|
2660
|
+
import { appendFile } from "fs/promises";
|
|
2661
|
+
import { isAbsolute as isAbsolute3, join as join9 } from "pathe";
|
|
2662
|
+
var DRAFT_KINDS = /* @__PURE__ */ new Set(["code", "scenario", "release"]);
|
|
2663
|
+
var FORBIDDEN = /* @__PURE__ */ new Set([
|
|
2664
|
+
"accept_spec",
|
|
2665
|
+
"replace_decision",
|
|
2666
|
+
"write_qa",
|
|
2667
|
+
"generate_release",
|
|
2668
|
+
"sdd_next",
|
|
2669
|
+
"sdd_complete",
|
|
2670
|
+
"create_change"
|
|
2671
|
+
]);
|
|
2672
|
+
var UNAVAILABLE = /* @__PURE__ */ new Set(["commit", "push", "tag", "pull_request", "merge", "deploy"]);
|
|
2673
|
+
var CLOSED = /* @__PURE__ */ new Set(["applied", "denied", "rejected", "unavailable", "stopped", "expired"]);
|
|
2674
|
+
function ok2(stdout = "", runId) {
|
|
2675
|
+
return runId ? { exitCode: 0, stdout, runId } : { exitCode: 0, stdout };
|
|
2676
|
+
}
|
|
2677
|
+
function fail2(stdout, runId) {
|
|
2678
|
+
return runId ? { exitCode: 1, stdout, runId } : { exitCode: 1, stdout };
|
|
2679
|
+
}
|
|
2680
|
+
function linesOf(...parts) {
|
|
2681
|
+
return parts.filter((part) => part.length > 0).join("\n");
|
|
2682
|
+
}
|
|
2683
|
+
function runIdFrom(date) {
|
|
2684
|
+
const iso = date.toISOString();
|
|
2685
|
+
const match = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})/.exec(iso);
|
|
2686
|
+
if (!match) return "unknown";
|
|
2687
|
+
return `${match[1]}${match[2]}${match[3]}T${match[4]}${match[5]}${match[6]}Z`;
|
|
2688
|
+
}
|
|
2689
|
+
function mentionsX(task) {
|
|
2690
|
+
return /@X|x\.com|twitter\.com/.test(task);
|
|
2691
|
+
}
|
|
2692
|
+
function safeChangeId(id) {
|
|
2693
|
+
if (!id || id.length > 200) return false;
|
|
2694
|
+
if (id === "." || id === "..") return false;
|
|
2695
|
+
return !id.includes("/") && !id.includes("\\") && !id.includes("\0");
|
|
2696
|
+
}
|
|
2697
|
+
function safeRunId(id) {
|
|
2698
|
+
if (!id || id.length > 80) return false;
|
|
2699
|
+
if (id === "." || id === "..") return false;
|
|
2700
|
+
return !id.includes("/") && !id.includes("\\") && !id.includes("\0");
|
|
2701
|
+
}
|
|
2702
|
+
function isSafeProductPath(rel) {
|
|
2703
|
+
if (typeof rel !== "string" || rel.length === 0 || rel.includes("\0")) return false;
|
|
2704
|
+
if (isAbsolute3(rel) || rel.startsWith("\\")) return false;
|
|
2705
|
+
if (/^[A-Za-z]:/.test(rel)) return false;
|
|
2706
|
+
const parts = rel.split(/[/\\]/);
|
|
2707
|
+
for (const part of parts) {
|
|
2708
|
+
if (part === "" || part === "." || part === ".." || part === ".active" || part === ".sdd") {
|
|
2709
|
+
return false;
|
|
2710
|
+
}
|
|
2711
|
+
}
|
|
2712
|
+
return parts[parts.length - 1] !== "accepted.md";
|
|
2713
|
+
}
|
|
2714
|
+
function canonicalJson(value) {
|
|
2715
|
+
return JSON.stringify(sortKeys(value));
|
|
2716
|
+
}
|
|
2717
|
+
function sortKeys(value) {
|
|
2718
|
+
if (Array.isArray(value)) return value.map(sortKeys);
|
|
2719
|
+
if (value && typeof value === "object") {
|
|
2720
|
+
const rec = value;
|
|
2721
|
+
const out = {};
|
|
2722
|
+
for (const key of Object.keys(rec).sort()) out[key] = sortKeys(rec[key]);
|
|
2723
|
+
return out;
|
|
2724
|
+
}
|
|
2725
|
+
return value;
|
|
2726
|
+
}
|
|
2727
|
+
function sameInputs(left, right) {
|
|
2728
|
+
return canonicalJson(left) === canonicalJson(right);
|
|
2729
|
+
}
|
|
2730
|
+
function sameCiteSet(changed, cites) {
|
|
2731
|
+
const left = [...new Set(changed)].sort();
|
|
2732
|
+
const right = [...new Set(cites)].sort();
|
|
2733
|
+
return left.length === right.length && left.every((cite, index) => cite === right[index]);
|
|
2734
|
+
}
|
|
2735
|
+
function parseSection(markdown, sectionId) {
|
|
2736
|
+
const rows = markdown.split(/\r?\n/);
|
|
2737
|
+
let start = -1;
|
|
2738
|
+
for (let i = 0; i < rows.length; i++) {
|
|
2739
|
+
const row = rows[i] ?? "";
|
|
2740
|
+
if (row.startsWith("## ") && row.slice(3).trim() === sectionId) {
|
|
2741
|
+
start = i;
|
|
2742
|
+
break;
|
|
2743
|
+
}
|
|
2744
|
+
}
|
|
2745
|
+
if (start < 0) return null;
|
|
2746
|
+
let end = rows.length;
|
|
2747
|
+
for (let i = start + 1; i < rows.length; i++) {
|
|
2748
|
+
if ((rows[i] ?? "").startsWith("## ")) {
|
|
2749
|
+
end = i;
|
|
2750
|
+
break;
|
|
2751
|
+
}
|
|
2752
|
+
}
|
|
2753
|
+
const body = rows.slice(start + 1, end);
|
|
2754
|
+
let decision = "";
|
|
2755
|
+
let accepted = false;
|
|
2756
|
+
const checks = [];
|
|
2757
|
+
for (const row of body) {
|
|
2758
|
+
const trimmed = row.trim();
|
|
2759
|
+
if (!decision && trimmed.startsWith("decision: "))
|
|
2760
|
+
decision = trimmed.slice("decision: ".length).trim();
|
|
2761
|
+
if (trimmed === "accepted: yes") accepted = true;
|
|
2762
|
+
if (trimmed.startsWith("- check: ")) {
|
|
2763
|
+
const id = trimmed.slice("- check: ".length).trim();
|
|
2764
|
+
if (id) checks.push(id);
|
|
2765
|
+
}
|
|
2766
|
+
}
|
|
2767
|
+
if (!decision || !accepted) return null;
|
|
2768
|
+
return { id: sectionId, decision, checks, body: body.join("\n") };
|
|
2769
|
+
}
|
|
2770
|
+
function parseReply(raw) {
|
|
2771
|
+
let value;
|
|
2772
|
+
try {
|
|
2773
|
+
value = JSON.parse(raw);
|
|
2774
|
+
} catch {
|
|
2775
|
+
return null;
|
|
2776
|
+
}
|
|
2777
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
2778
|
+
const rec = value;
|
|
2779
|
+
if (typeof rec.text !== "string") return null;
|
|
2780
|
+
const gaps = [];
|
|
2781
|
+
if (rec.gaps !== void 0) {
|
|
2782
|
+
if (!Array.isArray(rec.gaps)) return null;
|
|
2783
|
+
for (const gap of rec.gaps) {
|
|
2784
|
+
if (!gap || typeof gap !== "object" || Array.isArray(gap)) return null;
|
|
2785
|
+
const item = gap;
|
|
2786
|
+
if (typeof item.cite !== "string" || typeof item.text !== "string") return null;
|
|
2787
|
+
gaps.push({ cite: item.cite, text: item.text });
|
|
2788
|
+
}
|
|
2789
|
+
}
|
|
2790
|
+
let changed = null;
|
|
2791
|
+
if (rec.changed !== void 0) {
|
|
2792
|
+
if (!Array.isArray(rec.changed) || rec.changed.some((cite) => typeof cite !== "string"))
|
|
2793
|
+
return null;
|
|
2794
|
+
changed = rec.changed;
|
|
2795
|
+
}
|
|
2796
|
+
let action = null;
|
|
2797
|
+
if (rec.action !== void 0) {
|
|
2798
|
+
if (!rec.action || typeof rec.action !== "object" || Array.isArray(rec.action)) return null;
|
|
2799
|
+
const item = rec.action;
|
|
2800
|
+
if (typeof item.name !== "string" || !item.name) return null;
|
|
2801
|
+
if (item.words !== void 0 && typeof item.words !== "string") return null;
|
|
2802
|
+
if (item.inputs !== void 0 && (!item.inputs || typeof item.inputs !== "object" || Array.isArray(item.inputs))) {
|
|
2803
|
+
return null;
|
|
2804
|
+
}
|
|
2805
|
+
action = {
|
|
2806
|
+
name: item.name,
|
|
2807
|
+
words: typeof item.words === "string" ? item.words : "",
|
|
2808
|
+
inputs: item.inputs && typeof item.inputs === "object" && !Array.isArray(item.inputs) ? item.inputs : {}
|
|
2809
|
+
};
|
|
2810
|
+
}
|
|
2811
|
+
return { text: rec.text, gaps, changed, action };
|
|
2812
|
+
}
|
|
2813
|
+
async function appendJson(file, value) {
|
|
2814
|
+
await ensureDir(join9(file, ".."));
|
|
2815
|
+
await appendFile(file, `${JSON.stringify(value)}
|
|
2816
|
+
`, "utf8");
|
|
2817
|
+
}
|
|
2818
|
+
async function readJsonLines(file) {
|
|
2819
|
+
const raw = await readText(file);
|
|
2820
|
+
const lines = [];
|
|
2821
|
+
for (const line of raw.split(/\r?\n/)) {
|
|
2822
|
+
if (!line.trim()) continue;
|
|
2823
|
+
try {
|
|
2824
|
+
lines.push(JSON.parse(line));
|
|
2825
|
+
} catch {
|
|
2826
|
+
}
|
|
2827
|
+
}
|
|
2828
|
+
return lines;
|
|
2829
|
+
}
|
|
2830
|
+
async function timeoutMinutes(root) {
|
|
2831
|
+
const path = join9(root, ".sdd", "role.yaml");
|
|
2832
|
+
if (!await pathExists(path)) return 10;
|
|
2833
|
+
let data;
|
|
2834
|
+
try {
|
|
2835
|
+
data = await readYaml(path);
|
|
2836
|
+
} catch {
|
|
2837
|
+
return "invalid";
|
|
2838
|
+
}
|
|
2839
|
+
if (data == null) return 10;
|
|
2840
|
+
if (typeof data !== "object" || Array.isArray(data)) return "invalid";
|
|
2841
|
+
const rec = data;
|
|
2842
|
+
if (!("approval_timeout_minutes" in rec) || rec.approval_timeout_minutes == null) return 10;
|
|
2843
|
+
const minutes = rec.approval_timeout_minutes;
|
|
2844
|
+
if (typeof minutes !== "number" || !Number.isFinite(minutes) || minutes < 0) return "invalid";
|
|
2845
|
+
return minutes;
|
|
2846
|
+
}
|
|
2847
|
+
function citeOk(cite, section) {
|
|
2848
|
+
return section.checks.includes(cite) || cite === `${section.id}:${section.decision}`;
|
|
2849
|
+
}
|
|
2850
|
+
function authorPrompt(role, task, kind, section) {
|
|
2851
|
+
const rows = [
|
|
2852
|
+
`Job: ${role.job}`,
|
|
2853
|
+
`Description: ${role.description}`,
|
|
2854
|
+
`Task: ${task}`,
|
|
2855
|
+
`Kind: ${kind}`
|
|
2856
|
+
];
|
|
2857
|
+
if (section) {
|
|
2858
|
+
rows.push(
|
|
2859
|
+
`Section: ${section.id}`,
|
|
2860
|
+
`Decision: ${section.decision}`,
|
|
2861
|
+
"Check ids:",
|
|
2862
|
+
...section.checks
|
|
2863
|
+
);
|
|
2864
|
+
rows.push("Section text:", section.body);
|
|
2865
|
+
}
|
|
2866
|
+
return rows.join("\n");
|
|
2867
|
+
}
|
|
2868
|
+
function critiquePrompt(role, section, draft) {
|
|
2869
|
+
return [
|
|
2870
|
+
`Job: ${role.job}`,
|
|
2871
|
+
"Check ids:",
|
|
2872
|
+
...section.checks,
|
|
2873
|
+
`Cite: ${section.id}:${section.decision}`,
|
|
2874
|
+
"Draft:",
|
|
2875
|
+
draft
|
|
2876
|
+
].join("\n");
|
|
2877
|
+
}
|
|
2878
|
+
function revisionPrompt(role, draft, gaps) {
|
|
2879
|
+
return [
|
|
2880
|
+
`Job: ${role.job}`,
|
|
2881
|
+
"Draft:",
|
|
2882
|
+
draft,
|
|
2883
|
+
"Gaps:",
|
|
2884
|
+
...gaps.map((gap) => `${gap.cite}: ${gap.text}`),
|
|
2885
|
+
"changed must list every gap cite."
|
|
2886
|
+
].join("\n");
|
|
2887
|
+
}
|
|
2888
|
+
async function acceptedFile(root, changeId) {
|
|
2889
|
+
if (!await pathExists(configPath(root))) return join9(root, "changes", changeId, "accepted.md");
|
|
2890
|
+
const config = await loadConfig(root);
|
|
2891
|
+
return join9(changesDir(root, config), changeId, "accepted.md");
|
|
2892
|
+
}
|
|
2893
|
+
async function callModel(model, file, roleId, prompt, pass) {
|
|
2894
|
+
let raw;
|
|
2895
|
+
try {
|
|
2896
|
+
raw = await model.complete({ roleId, prompt });
|
|
2897
|
+
} catch {
|
|
2898
|
+
await appendJson(file, { type: "bad_call", roleId });
|
|
2899
|
+
return null;
|
|
2900
|
+
}
|
|
2901
|
+
const parsed = parseReply(raw);
|
|
2902
|
+
if (!parsed) {
|
|
2903
|
+
await appendJson(file, { type: "bad_call", roleId });
|
|
2904
|
+
return null;
|
|
2905
|
+
}
|
|
2906
|
+
await appendJson(file, { type: "model", pass, roleId, text: parsed.text });
|
|
2907
|
+
return parsed;
|
|
2908
|
+
}
|
|
2909
|
+
async function readRule(root, roleName) {
|
|
2910
|
+
const data = await readAllowFile(root);
|
|
2911
|
+
if (!data) return null;
|
|
2912
|
+
const entry = data[roleName];
|
|
2913
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry)) return null;
|
|
2914
|
+
const rec = entry;
|
|
2915
|
+
const name = typeof rec.tool === "string" ? rec.tool : typeof rec.name === "string" ? rec.name : "";
|
|
2916
|
+
if (!name) return null;
|
|
2917
|
+
if (!rec.inputs || typeof rec.inputs !== "object" || Array.isArray(rec.inputs)) return null;
|
|
2918
|
+
return { name, inputs: rec.inputs };
|
|
2919
|
+
}
|
|
2920
|
+
async function saveRule(root, roleName, rule) {
|
|
2921
|
+
const path = allowFilePath(root);
|
|
2922
|
+
const existing = await readAllowFile(root) ?? {};
|
|
2923
|
+
existing[roleName] = { tool: rule.name, inputs: rule.inputs };
|
|
2924
|
+
await writeText(path, `${JSON.stringify(existing, null, 2)}
|
|
2925
|
+
`);
|
|
2926
|
+
}
|
|
2927
|
+
async function applyAction(root, file, roleName, action, mode) {
|
|
2928
|
+
if (FORBIDDEN.has(action.name) || !UNAVAILABLE.has(action.name) && action.name !== "write_file") {
|
|
2929
|
+
await appendJson(file, { type: "rejected" });
|
|
2930
|
+
return { exitCode: 1, stdout: ROLE_TEXT.cannot, applied: false };
|
|
2931
|
+
}
|
|
2932
|
+
if (UNAVAILABLE.has(action.name)) {
|
|
2933
|
+
await appendJson(file, { type: "unavailable" });
|
|
2934
|
+
return { exitCode: 0, stdout: ROLE_TEXT.unavailable, applied: false };
|
|
2935
|
+
}
|
|
2936
|
+
const path = action.inputs.path;
|
|
2937
|
+
const contents = action.inputs.contents;
|
|
2938
|
+
if (typeof path !== "string" || typeof contents !== "string" || !isSafeProductPath(path)) {
|
|
2939
|
+
await appendJson(file, { type: "rejected" });
|
|
2940
|
+
return { exitCode: 1, stdout: ROLE_TEXT.cannot, applied: false };
|
|
2941
|
+
}
|
|
2942
|
+
await writeText(join9(root, ...path.split(/[/\\]/)), contents);
|
|
2943
|
+
if (mode === "always")
|
|
2944
|
+
await saveRule(root, roleName, { name: action.name, inputs: action.inputs });
|
|
2945
|
+
await appendJson(file, { type: "applied", name: "write_file" });
|
|
2946
|
+
return { exitCode: 0, stdout: "", applied: true };
|
|
2947
|
+
}
|
|
2948
|
+
async function finishRun(root, file, role, final, minutes, now, runId) {
|
|
2949
|
+
const action = final.action;
|
|
2950
|
+
if (!action) return ok2(final.text, runId);
|
|
2951
|
+
if (FORBIDDEN.has(action.name) || !UNAVAILABLE.has(action.name) && action.name !== "write_file") {
|
|
2952
|
+
await appendJson(file, { type: "rejected" });
|
|
2953
|
+
return fail2(linesOf(final.text, ROLE_TEXT.cannot), runId);
|
|
2954
|
+
}
|
|
2955
|
+
const rule = await readRule(root, role.name);
|
|
2956
|
+
if (!role.askFirst && rule && rule.name === action.name && sameInputs(rule.inputs, action.inputs)) {
|
|
2957
|
+
const applied = await applyAction(root, file, role.name, action, "always");
|
|
2958
|
+
const stdout = applied.applied ? final.text : linesOf(final.text, applied.stdout);
|
|
2959
|
+
return applied.exitCode === 0 ? ok2(stdout, runId) : fail2(stdout, runId);
|
|
2960
|
+
}
|
|
2961
|
+
const expiresAt = new Date(now.getTime() + minutes * 6e4).toISOString();
|
|
2962
|
+
await appendJson(file, {
|
|
2963
|
+
type: "pending",
|
|
2964
|
+
action: { name: action.name, words: action.words, inputs: action.inputs },
|
|
2965
|
+
expiresAt
|
|
2966
|
+
});
|
|
2967
|
+
if (role.askFirst) return ok2(linesOf(action.words, ROLE_TEXT.askFirst), runId);
|
|
2968
|
+
return ok2(linesOf(action.words, ROLE_TEXT.choice), runId);
|
|
2969
|
+
}
|
|
2970
|
+
async function settleRoleReply(root, file, role, reply, now, runId) {
|
|
2971
|
+
const minutes = await timeoutMinutes(root);
|
|
2972
|
+
if (minutes === "invalid") return fail2(ROLE_TEXT.badYaml, runId);
|
|
2973
|
+
return finishRun(
|
|
2974
|
+
root,
|
|
2975
|
+
file,
|
|
2976
|
+
role,
|
|
2977
|
+
{ text: reply.text, gaps: [], changed: null, action: reply.action },
|
|
2978
|
+
minutes,
|
|
2979
|
+
now,
|
|
2980
|
+
runId
|
|
2981
|
+
);
|
|
2982
|
+
}
|
|
2983
|
+
async function latestRunFile(root, name) {
|
|
2984
|
+
const dir = conversationDir(root, name);
|
|
2985
|
+
const files = (await listFiles(dir)).filter((file) => file.endsWith(".jsonl")).sort();
|
|
2986
|
+
const last = files[files.length - 1];
|
|
2987
|
+
return last ? join9(dir, last) : null;
|
|
2988
|
+
}
|
|
2989
|
+
async function runRole(root, input) {
|
|
2990
|
+
const name = canonicalRoleName(input.name);
|
|
2991
|
+
if (!name) return fail2(ROLE_TEXT.badName);
|
|
2992
|
+
if (input.stop) {
|
|
2993
|
+
if (!await readRole(root, name)) return fail2(ROLE_TEXT.notFound);
|
|
2994
|
+
const latest = await latestRunFile(root, name);
|
|
2995
|
+
if (!latest) return fail2(ROLE_TEXT.runNotFound);
|
|
2996
|
+
await appendJson(latest, { type: "stopped" });
|
|
2997
|
+
return ok2();
|
|
2998
|
+
}
|
|
2999
|
+
const role = await readRole(root, name);
|
|
3000
|
+
if (!role) return fail2(ROLE_TEXT.notFound);
|
|
3001
|
+
if (!role.job.trim()) return fail2(ROLE_TEXT.needsJob);
|
|
3002
|
+
const task = input.task ?? "";
|
|
3003
|
+
let withName = "";
|
|
3004
|
+
if (input.withRole !== void 0 && input.withRole.trim() !== "") {
|
|
3005
|
+
const canonical = canonicalRoleName(input.withRole);
|
|
3006
|
+
if (!canonical) return fail2(ROLE_TEXT.badName);
|
|
3007
|
+
if (canonical === name) return fail2(ROLE_TEXT.self);
|
|
3008
|
+
withName = canonical;
|
|
3009
|
+
}
|
|
3010
|
+
if (mentionsX(task)) return fail2(ROLE_TEXT.x);
|
|
3011
|
+
const kind = input.kind?.trim() || "reading";
|
|
3012
|
+
let section;
|
|
3013
|
+
if (kind !== "reading") {
|
|
3014
|
+
if (!DRAFT_KINDS.has(kind)) return fail2(ROLE_TEXT.needsSection);
|
|
3015
|
+
const changeId = input.change?.trim() ?? "";
|
|
3016
|
+
const sectionId = input.section?.trim() ?? "";
|
|
3017
|
+
if (!safeChangeId(changeId) || !sectionId) return fail2(ROLE_TEXT.needsSection);
|
|
3018
|
+
const acceptedPath = await acceptedFile(root, changeId);
|
|
3019
|
+
if (!await pathExists(acceptedPath)) return fail2(ROLE_TEXT.needsSection);
|
|
3020
|
+
const parsed = parseSection(await readText(acceptedPath), sectionId);
|
|
3021
|
+
if (!parsed) return fail2(ROLE_TEXT.needsSection);
|
|
3022
|
+
section = parsed;
|
|
3023
|
+
}
|
|
3024
|
+
let critic = null;
|
|
3025
|
+
if (withName && section) {
|
|
3026
|
+
critic = await readRole(root, withName);
|
|
3027
|
+
if (!critic) return fail2(ROLE_TEXT.notFound);
|
|
3028
|
+
}
|
|
3029
|
+
const minutes = await timeoutMinutes(root);
|
|
3030
|
+
if (minutes === "invalid") return fail2(ROLE_TEXT.badYaml);
|
|
3031
|
+
if (!input.model) return fail2(ROLE_TEXT.noModel);
|
|
3032
|
+
const now = (input.now ?? (() => /* @__PURE__ */ new Date()))();
|
|
3033
|
+
const runId = runIdFrom(now);
|
|
3034
|
+
const file = join9(conversationDir(root, name), `${runId}.jsonl`);
|
|
3035
|
+
if (section) {
|
|
3036
|
+
await appendJson(file, {
|
|
3037
|
+
type: "checks",
|
|
3038
|
+
ids: section.checks,
|
|
3039
|
+
section: section.id,
|
|
3040
|
+
decision: section.decision
|
|
3041
|
+
});
|
|
3042
|
+
}
|
|
3043
|
+
const draft = await callModel(
|
|
3044
|
+
input.model,
|
|
3045
|
+
file,
|
|
3046
|
+
role.name,
|
|
3047
|
+
authorPrompt(role, task, kind, section),
|
|
3048
|
+
1
|
|
3049
|
+
);
|
|
3050
|
+
if (!draft) return fail2("", runId);
|
|
3051
|
+
let final = draft;
|
|
3052
|
+
if (critic && section) {
|
|
3053
|
+
const accepted = section;
|
|
3054
|
+
const critique = await callModel(
|
|
3055
|
+
input.model,
|
|
3056
|
+
file,
|
|
3057
|
+
critic.name,
|
|
3058
|
+
critiquePrompt(critic, accepted, draft.text),
|
|
3059
|
+
2
|
|
3060
|
+
);
|
|
3061
|
+
if (!critique) return fail2(draft.text, runId);
|
|
3062
|
+
if (critique.gaps.some((gap) => !citeOk(gap.cite, accepted))) {
|
|
3063
|
+
await appendJson(file, { type: "bad_call", roleId: critic.name });
|
|
3064
|
+
return fail2(draft.text, runId);
|
|
3065
|
+
}
|
|
3066
|
+
if (critique.gaps.length > 0) {
|
|
3067
|
+
const revision = await callModel(
|
|
3068
|
+
input.model,
|
|
3069
|
+
file,
|
|
3070
|
+
role.name,
|
|
3071
|
+
revisionPrompt(role, draft.text, critique.gaps),
|
|
3072
|
+
3
|
|
3073
|
+
);
|
|
3074
|
+
if (!revision) return fail2(draft.text, runId);
|
|
3075
|
+
const cites = critique.gaps.map((gap) => gap.cite);
|
|
3076
|
+
if (revision.changed === null || !sameCiteSet(revision.changed, cites)) {
|
|
3077
|
+
await appendJson(file, { type: "bad_call", roleId: role.name });
|
|
3078
|
+
return fail2(draft.text, runId);
|
|
3079
|
+
}
|
|
3080
|
+
final = revision;
|
|
3081
|
+
}
|
|
3082
|
+
}
|
|
3083
|
+
return finishRun(root, file, role, final, minutes, now, runId);
|
|
3084
|
+
}
|
|
3085
|
+
function openPending(lines) {
|
|
3086
|
+
let pending = null;
|
|
3087
|
+
for (const line of lines) {
|
|
3088
|
+
if (!line || typeof line !== "object" || Array.isArray(line)) continue;
|
|
3089
|
+
const rec = line;
|
|
3090
|
+
if (rec.type === "pending") {
|
|
3091
|
+
const action = rec.action;
|
|
3092
|
+
if (!action || typeof action !== "object" || Array.isArray(action)) {
|
|
3093
|
+
pending = null;
|
|
3094
|
+
continue;
|
|
3095
|
+
}
|
|
3096
|
+
const item = action;
|
|
3097
|
+
if (typeof item.name !== "string") {
|
|
3098
|
+
pending = null;
|
|
3099
|
+
continue;
|
|
3100
|
+
}
|
|
3101
|
+
pending = {
|
|
3102
|
+
name: item.name,
|
|
3103
|
+
words: typeof item.words === "string" ? item.words : "",
|
|
3104
|
+
inputs: item.inputs && typeof item.inputs === "object" && !Array.isArray(item.inputs) ? item.inputs : {},
|
|
3105
|
+
expiresAt: typeof rec.expiresAt === "string" ? rec.expiresAt : ""
|
|
3106
|
+
};
|
|
3107
|
+
continue;
|
|
3108
|
+
}
|
|
3109
|
+
if (typeof rec.type === "string" && CLOSED.has(rec.type)) pending = null;
|
|
3110
|
+
}
|
|
3111
|
+
return pending;
|
|
3112
|
+
}
|
|
3113
|
+
async function findRun(root, runId) {
|
|
3114
|
+
if (!safeRunId(runId)) return null;
|
|
3115
|
+
const hits = [];
|
|
3116
|
+
for (const roleName of await listConversationRoles(root)) {
|
|
3117
|
+
const file = join9(conversationDir(root, roleName), `${runId}.jsonl`);
|
|
3118
|
+
if (await pathExists(file)) hits.push({ file, roleName });
|
|
3119
|
+
}
|
|
3120
|
+
if (hits.length === 1) return hits[0];
|
|
3121
|
+
const pendingHits = [];
|
|
3122
|
+
for (const hit of hits) {
|
|
3123
|
+
if (openPending(await readJsonLines(hit.file))) pendingHits.push(hit);
|
|
3124
|
+
}
|
|
3125
|
+
return pendingHits.length === 1 ? pendingHits[0] : null;
|
|
3126
|
+
}
|
|
3127
|
+
async function allowRole(root, input) {
|
|
3128
|
+
const found = await findRun(root, input.runId);
|
|
3129
|
+
if (!found) return fail2(ROLE_TEXT.runNotFound);
|
|
3130
|
+
const pending = openPending(await readJsonLines(found.file));
|
|
3131
|
+
if (!pending) return fail2(ROLE_TEXT.runNotFound);
|
|
3132
|
+
const now = (input.now ?? (() => /* @__PURE__ */ new Date()))();
|
|
3133
|
+
const expires = Date.parse(pending.expiresAt);
|
|
3134
|
+
if (!Number.isFinite(expires) || now.getTime() >= expires) {
|
|
3135
|
+
await appendJson(found.file, { type: "expired" });
|
|
3136
|
+
return fail2(ROLE_TEXT.expired);
|
|
3137
|
+
}
|
|
3138
|
+
if (input.mode === "deny") {
|
|
3139
|
+
await appendJson(found.file, { type: "denied" });
|
|
3140
|
+
return ok2("", input.runId);
|
|
3141
|
+
}
|
|
3142
|
+
const role = await readRole(root, found.roleName);
|
|
3143
|
+
if (!role) return fail2(ROLE_TEXT.notFound);
|
|
3144
|
+
if (role.askFirst) return ok2(ROLE_TEXT.askFirst, input.runId);
|
|
3145
|
+
const applied = await applyAction(root, found.file, found.roleName, pending, input.mode);
|
|
3146
|
+
return applied.exitCode === 0 ? ok2(applied.stdout, input.runId) : fail2(applied.stdout, input.runId);
|
|
3147
|
+
}
|
|
3148
|
+
|
|
3149
|
+
// src/role/handoff.ts
|
|
3150
|
+
var ID = /^[a-z][a-z0-9-]{0,39}$/;
|
|
3151
|
+
var ROLE_YAML = ["role.yaml"];
|
|
3152
|
+
function ok3(stdout = "", runId) {
|
|
3153
|
+
return runId ? { exitCode: 0, stdout, runId } : { exitCode: 0, stdout };
|
|
3154
|
+
}
|
|
3155
|
+
function fail3(stdout, runId) {
|
|
3156
|
+
return runId ? { exitCode: 1, stdout, runId } : { exitCode: 1, stdout };
|
|
3157
|
+
}
|
|
3158
|
+
function mentionsX2(task) {
|
|
3159
|
+
return /@X|x\.com|twitter\.com/.test(task);
|
|
3160
|
+
}
|
|
3161
|
+
function safeChangeId2(id) {
|
|
3162
|
+
if (!id || id.length > 200 || id === "." || id === "..") return false;
|
|
3163
|
+
return !id.includes("/") && !id.includes("\\") && !id.includes("\0");
|
|
3164
|
+
}
|
|
3165
|
+
function yamlPath(root) {
|
|
3166
|
+
return join10(root, ".sdd", ...ROLE_YAML);
|
|
3167
|
+
}
|
|
3168
|
+
function planPath(root, changeId, sectionId) {
|
|
3169
|
+
return join10(root, ".sdd", "role-runs", "plans", changeId, `${sectionId}.json`);
|
|
3170
|
+
}
|
|
3171
|
+
async function loadYaml(root) {
|
|
3172
|
+
const path = yamlPath(root);
|
|
3173
|
+
if (!await pathExists(path)) return "missing";
|
|
3174
|
+
let data;
|
|
3175
|
+
try {
|
|
3176
|
+
data = parseYaml3(await readText(path));
|
|
3177
|
+
} catch {
|
|
3178
|
+
return "invalid";
|
|
3179
|
+
}
|
|
3180
|
+
if (data == null) return {};
|
|
3181
|
+
if (typeof data !== "object" || Array.isArray(data)) return "invalid";
|
|
3182
|
+
const rec = data;
|
|
3183
|
+
if ("approval_timeout_minutes" in rec && rec.approval_timeout_minutes != null) {
|
|
3184
|
+
const minutes = rec.approval_timeout_minutes;
|
|
3185
|
+
if (typeof minutes !== "number" || !Number.isFinite(minutes) || minutes < 0) return "invalid";
|
|
3186
|
+
}
|
|
3187
|
+
if ("role_messaging" in rec && rec.role_messaging != null && typeof rec.role_messaging !== "boolean") {
|
|
3188
|
+
return "invalid";
|
|
3189
|
+
}
|
|
3190
|
+
return rec;
|
|
3191
|
+
}
|
|
3192
|
+
async function messagingOn(root) {
|
|
3193
|
+
const loaded = await loadYaml(root);
|
|
3194
|
+
if (loaded === "invalid") return "invalid";
|
|
3195
|
+
if (loaded === "missing") return true;
|
|
3196
|
+
return loaded.role_messaging !== false;
|
|
3197
|
+
}
|
|
3198
|
+
async function setRoleMessaging(root, enabled) {
|
|
3199
|
+
const loaded = await loadYaml(root);
|
|
3200
|
+
if (loaded === "invalid") return fail3(ROLE_TEXT.badYaml);
|
|
3201
|
+
const raw = loaded === "missing" ? {} : loaded;
|
|
3202
|
+
raw.role_messaging = enabled;
|
|
3203
|
+
await writeText(yamlPath(root), stringifyYaml2(raw));
|
|
3204
|
+
return ok3(enabled ? "Messaging between roles is on." : "Messaging between roles is off.");
|
|
3205
|
+
}
|
|
3206
|
+
function field(body, prefix) {
|
|
3207
|
+
const found = [];
|
|
3208
|
+
for (const row of body.split(/\r?\n/)) {
|
|
3209
|
+
const trimmed = row.trim();
|
|
3210
|
+
if (!trimmed.startsWith(prefix)) continue;
|
|
3211
|
+
const value = trimmed.slice(prefix.length).trim();
|
|
3212
|
+
if (value) found.push(value);
|
|
3213
|
+
}
|
|
3214
|
+
return found;
|
|
3215
|
+
}
|
|
3216
|
+
function unique(items) {
|
|
3217
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3218
|
+
const found = [];
|
|
3219
|
+
for (const item of items) {
|
|
3220
|
+
if (seen.has(item)) continue;
|
|
3221
|
+
seen.add(item);
|
|
3222
|
+
found.push(item);
|
|
3223
|
+
}
|
|
3224
|
+
return found;
|
|
3225
|
+
}
|
|
3226
|
+
function factsFrom(markdown) {
|
|
3227
|
+
const ids = [];
|
|
3228
|
+
for (const row of markdown.split(/\r?\n/)) {
|
|
3229
|
+
if (row.startsWith("## ")) {
|
|
3230
|
+
const id = row.slice(3).trim();
|
|
3231
|
+
if (id) ids.push(id);
|
|
3232
|
+
}
|
|
3233
|
+
}
|
|
3234
|
+
const facts = [];
|
|
3235
|
+
for (const id of ids) {
|
|
3236
|
+
const section = parseSection(markdown, id);
|
|
3237
|
+
if (!section) continue;
|
|
3238
|
+
const services = field(section.body, "service: ").filter((item) => ID.test(item));
|
|
3239
|
+
const modules = field(section.body, "module: ").filter((item) => ID.test(item));
|
|
3240
|
+
const owners = field(section.body, "owner: ").filter(
|
|
3241
|
+
(item) => canonicalRoleName(item) === item
|
|
3242
|
+
);
|
|
3243
|
+
const leads = field(section.body, "lead: ").filter((item) => canonicalRoleName(item) === item);
|
|
3244
|
+
const connection = connectionOf(section.body);
|
|
3245
|
+
facts.push({
|
|
3246
|
+
id: section.id,
|
|
3247
|
+
decision: section.decision,
|
|
3248
|
+
body: section.body,
|
|
3249
|
+
services,
|
|
3250
|
+
modules,
|
|
3251
|
+
owners,
|
|
3252
|
+
leads,
|
|
3253
|
+
connection,
|
|
3254
|
+
product: field(section.body, "scope: ").includes("product")
|
|
3255
|
+
});
|
|
3256
|
+
}
|
|
3257
|
+
return facts;
|
|
3258
|
+
}
|
|
3259
|
+
function connectionOf(body) {
|
|
3260
|
+
for (const value of field(body, "connection: ")) {
|
|
3261
|
+
const parts = value.split(/\s+/);
|
|
3262
|
+
if (parts.length === 2 && parts[0] && parts[1] && ID.test(parts[0]) && ID.test(parts[1])) {
|
|
3263
|
+
return [parts[0], parts[1]];
|
|
3264
|
+
}
|
|
3265
|
+
}
|
|
3266
|
+
return null;
|
|
3267
|
+
}
|
|
3268
|
+
async function acceptedFacts(root, changeId, sectionId) {
|
|
3269
|
+
if (!safeChangeId2(changeId) || !sectionId) return null;
|
|
3270
|
+
const path = await acceptedFile(root, changeId);
|
|
3271
|
+
if (!await pathExists(path)) return null;
|
|
3272
|
+
const markdown = await readText(path);
|
|
3273
|
+
const all = factsFrom(markdown);
|
|
3274
|
+
const current = all.find((item) => item.id === sectionId);
|
|
3275
|
+
if (!current) return null;
|
|
3276
|
+
return { markdown, current, all };
|
|
3277
|
+
}
|
|
3278
|
+
function handoffRecord(from, to, changeId, section) {
|
|
3279
|
+
return {
|
|
3280
|
+
type: "handoff",
|
|
3281
|
+
from,
|
|
3282
|
+
to,
|
|
3283
|
+
change: changeId,
|
|
3284
|
+
section: section.id,
|
|
3285
|
+
decision: section.decision
|
|
3286
|
+
};
|
|
3287
|
+
}
|
|
3288
|
+
function receiverPrompt(role, task, section) {
|
|
3289
|
+
return [
|
|
3290
|
+
`Job: ${role.job}`,
|
|
3291
|
+
`Description: ${role.description}`,
|
|
3292
|
+
`Task: ${task}`,
|
|
3293
|
+
"Kind: handoff",
|
|
3294
|
+
`Section: ${section.id}`,
|
|
3295
|
+
`Decision: ${section.decision}`,
|
|
3296
|
+
"Section text:",
|
|
3297
|
+
section.body
|
|
3298
|
+
].join("\n");
|
|
3299
|
+
}
|
|
3300
|
+
async function handoffRole(root, input) {
|
|
3301
|
+
const from = canonicalRoleName(input.from);
|
|
3302
|
+
const to = canonicalRoleName(input.to);
|
|
3303
|
+
if (!from || !to) return fail3(ROLE_TEXT.badName);
|
|
3304
|
+
if (from === to) return fail3("A role cannot hand off to itself.");
|
|
3305
|
+
const sender = await readRole(root, from);
|
|
3306
|
+
const receiver = await readRole(root, to);
|
|
3307
|
+
if (!sender || !receiver) return fail3(ROLE_TEXT.notFound);
|
|
3308
|
+
const task = input.task ?? "";
|
|
3309
|
+
if (mentionsX2(task)) return fail3(ROLE_TEXT.x);
|
|
3310
|
+
const loaded = await acceptedFacts(root, input.change?.trim() ?? "", input.section?.trim() ?? "");
|
|
3311
|
+
if (!loaded) return fail3(ROLE_TEXT.needsSection);
|
|
3312
|
+
if (loaded.current.owners.length > 0 && !loaded.current.owners.includes(to)) {
|
|
3313
|
+
return fail3("One owner handles this section.");
|
|
3314
|
+
}
|
|
3315
|
+
const messaging = await messagingOn(root);
|
|
3316
|
+
if (messaging === false) return ok3("Messaging between roles is off.");
|
|
3317
|
+
if (!input.model) return fail3(ROLE_TEXT.noModel);
|
|
3318
|
+
if (messaging === "invalid") return fail3(ROLE_TEXT.badYaml);
|
|
3319
|
+
const now = (input.now ?? (() => /* @__PURE__ */ new Date()))();
|
|
3320
|
+
const runId = runIdFrom(now);
|
|
3321
|
+
const senderFile = join10(conversationDir(root, from), `${runId}.jsonl`);
|
|
3322
|
+
const receiverFile = join10(conversationDir(root, to), `${runId}.jsonl`);
|
|
3323
|
+
const record = handoffRecord(from, to, input.change?.trim() ?? "", loaded.current);
|
|
3324
|
+
await appendJson(senderFile, record);
|
|
3325
|
+
await appendJson(receiverFile, record);
|
|
3326
|
+
let raw;
|
|
3327
|
+
try {
|
|
3328
|
+
raw = await input.model.complete({
|
|
3329
|
+
roleId: to,
|
|
3330
|
+
prompt: receiverPrompt(receiver, task, loaded.current)
|
|
3331
|
+
});
|
|
3332
|
+
} catch {
|
|
3333
|
+
await appendJson(receiverFile, { type: "bad_call", roleId: to });
|
|
3334
|
+
return fail3("", runId);
|
|
3335
|
+
}
|
|
3336
|
+
const parsed = parseReply(raw);
|
|
3337
|
+
if (!parsed) {
|
|
3338
|
+
await appendJson(receiverFile, { type: "bad_call", roleId: to });
|
|
3339
|
+
return fail3("", runId);
|
|
3340
|
+
}
|
|
3341
|
+
await appendJson(receiverFile, { type: "model", pass: 1, roleId: to, text: parsed.text });
|
|
3342
|
+
const settled = await settleRoleReply(root, receiverFile, receiver, parsed, now, runId);
|
|
3343
|
+
await appendJson(senderFile, {
|
|
3344
|
+
type: "handoff_reply",
|
|
3345
|
+
from: to,
|
|
3346
|
+
to: from,
|
|
3347
|
+
change: record.change,
|
|
3348
|
+
section: record.section,
|
|
3349
|
+
decision: record.decision,
|
|
3350
|
+
summary: parsed.text
|
|
3351
|
+
});
|
|
3352
|
+
return settled;
|
|
3353
|
+
}
|
|
3354
|
+
async function showRoleGroup(root, changeId) {
|
|
3355
|
+
const path = safeChangeId2(changeId) ? await acceptedFile(root, changeId) : "";
|
|
3356
|
+
if (!path || !await pathExists(path)) return ok3("No roles in this group.");
|
|
3357
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3358
|
+
const owners = [];
|
|
3359
|
+
for (const section of factsFrom(await readText(path))) {
|
|
3360
|
+
for (const owner of section.owners) {
|
|
3361
|
+
if (seen.has(owner)) continue;
|
|
3362
|
+
seen.add(owner);
|
|
3363
|
+
owners.push(owner);
|
|
3364
|
+
}
|
|
3365
|
+
}
|
|
3366
|
+
return ok3(owners.length > 0 ? owners.join("\n") : "No roles in this group.");
|
|
3367
|
+
}
|
|
3368
|
+
function distinctServices(all) {
|
|
3369
|
+
const services = /* @__PURE__ */ new Map();
|
|
3370
|
+
for (const section of all) {
|
|
3371
|
+
for (const service of section.services) {
|
|
3372
|
+
if (!services.has(service)) services.set(service, section);
|
|
3373
|
+
}
|
|
3374
|
+
}
|
|
3375
|
+
return services;
|
|
3376
|
+
}
|
|
3377
|
+
function nodeLine(node) {
|
|
3378
|
+
const after = node.after.length > 0 ? node.after.join(",") : "none";
|
|
3379
|
+
return `${node.scope} ${node.owner} ${node.decision} after ${after} artifact ${node.artifact} boundary ${node.boundary}`;
|
|
3380
|
+
}
|
|
3381
|
+
function planLines(nodes) {
|
|
3382
|
+
const lines = [];
|
|
3383
|
+
for (const node of nodes) {
|
|
3384
|
+
lines.push(nodeLine(node));
|
|
3385
|
+
lines.push(...planLines(node.children));
|
|
3386
|
+
}
|
|
3387
|
+
return lines;
|
|
3388
|
+
}
|
|
3389
|
+
function artifactsOf(nodes) {
|
|
3390
|
+
const artifacts = [];
|
|
3391
|
+
for (const node of nodes) {
|
|
3392
|
+
artifacts.push(node.artifact);
|
|
3393
|
+
artifacts.push(...artifactsOf(node.children));
|
|
3394
|
+
}
|
|
3395
|
+
return artifacts;
|
|
3396
|
+
}
|
|
3397
|
+
async function readPlan(root, changeId, sectionId) {
|
|
3398
|
+
const path = planPath(root, changeId, sectionId);
|
|
3399
|
+
if (!await pathExists(path)) return null;
|
|
3400
|
+
try {
|
|
3401
|
+
const data = JSON.parse(await readText(path));
|
|
3402
|
+
if (!data || !Array.isArray(data.nodes) || !Array.isArray(data.results)) return null;
|
|
3403
|
+
return data;
|
|
3404
|
+
} catch {
|
|
3405
|
+
return null;
|
|
3406
|
+
}
|
|
3407
|
+
}
|
|
3408
|
+
async function writePlan(root, changeId, sectionId, plan) {
|
|
3409
|
+
await writeText(planPath(root, changeId, sectionId), `${JSON.stringify(plan, null, 2)}
|
|
3410
|
+
`);
|
|
3411
|
+
}
|
|
3412
|
+
function contextOf(all) {
|
|
3413
|
+
const services = /* @__PURE__ */ new Set();
|
|
3414
|
+
const modules = /* @__PURE__ */ new Set();
|
|
3415
|
+
const decisions = /* @__PURE__ */ new Set();
|
|
3416
|
+
for (const section of all) {
|
|
3417
|
+
for (const service of section.services) services.add(service);
|
|
3418
|
+
for (const moduleId of section.modules) modules.add(moduleId);
|
|
3419
|
+
decisions.add(section.decision);
|
|
3420
|
+
}
|
|
3421
|
+
return { services, modules, decisions };
|
|
3422
|
+
}
|
|
3423
|
+
async function parsePlan(root, raw, ctx) {
|
|
3424
|
+
let value;
|
|
3425
|
+
try {
|
|
3426
|
+
value = JSON.parse(raw);
|
|
3427
|
+
} catch {
|
|
3428
|
+
return "invalid";
|
|
3429
|
+
}
|
|
3430
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return "invalid";
|
|
3431
|
+
const rec = value;
|
|
3432
|
+
if (typeof rec.text !== "string" || !Array.isArray(rec.plan)) return "invalid";
|
|
3433
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3434
|
+
const nodes = await parseNodes(root, rec.plan, ctx, seen, true);
|
|
3435
|
+
return nodes;
|
|
3436
|
+
}
|
|
3437
|
+
async function parseNodes(root, value, ctx, seen, roots) {
|
|
3438
|
+
if (!Array.isArray(value)) return "invalid";
|
|
3439
|
+
const nodes = [];
|
|
3440
|
+
for (const item of value) {
|
|
3441
|
+
const parsed = await parseNode(root, item, ctx, seen);
|
|
3442
|
+
if (parsed === "invalid" || parsed === "deep") return parsed;
|
|
3443
|
+
if (!roots && parsed.children.length > 0) return "deep";
|
|
3444
|
+
for (const child of parsed.children) {
|
|
3445
|
+
if (child.children.length > 0) return "deep";
|
|
3446
|
+
}
|
|
3447
|
+
nodes.push(parsed);
|
|
3448
|
+
}
|
|
3449
|
+
return nodes;
|
|
3450
|
+
}
|
|
3451
|
+
async function parseNode(root, value, ctx, seen) {
|
|
3452
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return "invalid";
|
|
3453
|
+
const rec = value;
|
|
3454
|
+
const scope = typeof rec.scope === "string" ? rec.scope : "";
|
|
3455
|
+
const owner = typeof rec.owner === "string" ? rec.owner : "";
|
|
3456
|
+
const decision = typeof rec.decision === "string" ? rec.decision : "";
|
|
3457
|
+
const artifact = typeof rec.artifact === "string" ? rec.artifact : "";
|
|
3458
|
+
const boundary = rec.boundary;
|
|
3459
|
+
if (!ctx.services.has(scope) && !ctx.modules.has(scope)) return "invalid";
|
|
3460
|
+
if (!await readRole(root, owner)) return "invalid";
|
|
3461
|
+
if (!ctx.decisions.has(decision) || seen.has(decision)) return "invalid";
|
|
3462
|
+
if (!Array.isArray(rec.after) || rec.after.some((id) => typeof id !== "string")) return "invalid";
|
|
3463
|
+
if (rec.after.some((id) => !ctx.decisions.has(id))) return "invalid";
|
|
3464
|
+
if (!isSafeProductPath(artifact)) return "invalid";
|
|
3465
|
+
if (boundary !== "ask" && boundary !== "stop") return "invalid";
|
|
3466
|
+
seen.add(decision);
|
|
3467
|
+
let children = [];
|
|
3468
|
+
if (rec.children !== void 0) {
|
|
3469
|
+
const parsed = await parseNodes(root, rec.children, ctx, seen, false);
|
|
3470
|
+
if (parsed === "invalid" || parsed === "deep") return parsed;
|
|
3471
|
+
children = parsed;
|
|
3472
|
+
}
|
|
3473
|
+
return {
|
|
3474
|
+
scope,
|
|
3475
|
+
owner,
|
|
3476
|
+
decision,
|
|
3477
|
+
after: rec.after,
|
|
3478
|
+
artifact,
|
|
3479
|
+
boundary,
|
|
3480
|
+
children
|
|
3481
|
+
};
|
|
3482
|
+
}
|
|
3483
|
+
function leadPrompt(task, services) {
|
|
3484
|
+
const rows = [`Task: ${task}`];
|
|
3485
|
+
for (const [service, section] of services) {
|
|
3486
|
+
const moduleId = section.modules[0] ?? "none";
|
|
3487
|
+
const owner = section.owners[0] ?? "none";
|
|
3488
|
+
rows.push(`${service} module ${moduleId} owner ${owner} decision ${section.decision}`);
|
|
3489
|
+
}
|
|
3490
|
+
return rows.join("\n");
|
|
3491
|
+
}
|
|
3492
|
+
async function connectionResult(root, changeId, section, now) {
|
|
3493
|
+
const messaging = await messagingOn(root);
|
|
3494
|
+
if (messaging === "invalid") return fail3(ROLE_TEXT.badYaml);
|
|
3495
|
+
if (!messaging) return ok3("Messaging between roles is off.");
|
|
3496
|
+
const owners = unique(section.owners);
|
|
3497
|
+
const runId = runIdFrom(now);
|
|
3498
|
+
for (let i = 0; i < owners.length - 1; i++) {
|
|
3499
|
+
const from = owners[i] ?? "";
|
|
3500
|
+
const to = owners[i + 1] ?? "";
|
|
3501
|
+
const record = handoffRecord(from, to, changeId, section);
|
|
3502
|
+
await appendJson(join10(conversationDir(root, from), `${runId}.jsonl`), record);
|
|
3503
|
+
await appendJson(join10(conversationDir(root, to), `${runId}.jsonl`), record);
|
|
3504
|
+
}
|
|
3505
|
+
const lines = ["Connection hands off. Lead is absent.", ...owners];
|
|
3506
|
+
return ok3(lines.join("\n"));
|
|
3507
|
+
}
|
|
3508
|
+
function freshPlan(task, nodes) {
|
|
3509
|
+
return { status: "pending", rebuilds: 0, stalled: false, task, nodes, results: [] };
|
|
3510
|
+
}
|
|
3511
|
+
async function planNetwork(root, input) {
|
|
3512
|
+
const changeId = input.change?.trim() ?? "";
|
|
3513
|
+
const sectionId = input.section?.trim() ?? "";
|
|
3514
|
+
const loaded = await acceptedFacts(root, changeId, sectionId);
|
|
3515
|
+
if (!loaded) return fail3(ROLE_TEXT.needsSection);
|
|
3516
|
+
const task = input.task ?? "";
|
|
3517
|
+
const now = (input.now ?? (() => /* @__PURE__ */ new Date()))();
|
|
3518
|
+
const connection = loaded.all.find(
|
|
3519
|
+
(item) => item.connection && (item.id === loaded.current.id || loaded.current.services.some((service) => item.connection?.includes(service)))
|
|
3520
|
+
);
|
|
3521
|
+
if (connection?.connection) return connectionResult(root, changeId, connection, now);
|
|
3522
|
+
const services = distinctServices(loaded.all);
|
|
3523
|
+
if (services.size === 0) return ok3("One scope. No lead.");
|
|
3524
|
+
if (services.size === 1) {
|
|
3525
|
+
const entry = [...services.entries()][0];
|
|
3526
|
+
if (!entry) return ok3("One scope. No lead.");
|
|
3527
|
+
const [name, section] = entry;
|
|
3528
|
+
const owner = section.owners[0];
|
|
3529
|
+
if (!owner) return fail3("This service needs an owner.");
|
|
3530
|
+
if (!await readRole(root, owner)) return fail3(ROLE_TEXT.notFound);
|
|
3531
|
+
return ok3(`Service ${name} runs ${owner}.`);
|
|
3532
|
+
}
|
|
3533
|
+
const picked = pickLead(loaded.all, services);
|
|
3534
|
+
if (picked.kind === "none") return ok3("One scope. No lead.");
|
|
3535
|
+
if (picked.kind === "missing") return fail3(picked.sentence);
|
|
3536
|
+
const lead = await readRole(root, picked.name);
|
|
3537
|
+
if (!lead) return fail3(ROLE_TEXT.notFound);
|
|
3538
|
+
if (await messagingOn(root) === "invalid") return fail3(ROLE_TEXT.badYaml);
|
|
3539
|
+
if (!input.model) return fail3(ROLE_TEXT.noModel);
|
|
3540
|
+
return storeLeadPlan(
|
|
3541
|
+
root,
|
|
3542
|
+
changeId,
|
|
3543
|
+
sectionId,
|
|
3544
|
+
task,
|
|
3545
|
+
lead,
|
|
3546
|
+
services,
|
|
3547
|
+
loaded.all,
|
|
3548
|
+
input.model,
|
|
3549
|
+
null
|
|
3550
|
+
);
|
|
3551
|
+
}
|
|
3552
|
+
async function storeLeadPlan(root, changeId, sectionId, task, lead, services, all, model, previous) {
|
|
3553
|
+
let raw;
|
|
3554
|
+
try {
|
|
3555
|
+
raw = await model.complete({ roleId: lead.name, prompt: leadPrompt(task, services) });
|
|
3556
|
+
} catch {
|
|
3557
|
+
return fail3("");
|
|
3558
|
+
}
|
|
3559
|
+
const parsed = await parsePlan(root, raw, contextOf(all));
|
|
3560
|
+
if (previous && (parsed === "invalid" || parsed === "deep")) return fail3(planFailure(parsed));
|
|
3561
|
+
if (parsed === "invalid") return fail3("The plan is not valid.");
|
|
3562
|
+
if (parsed === "deep") return fail3("A plan is deeper than 2.");
|
|
3563
|
+
if (previous && sameArtifacts(previous.nodes, parsed)) {
|
|
3564
|
+
previous.stalled = true;
|
|
3565
|
+
previous.rebuilds += 1;
|
|
3566
|
+
await writePlan(root, changeId, sectionId, previous);
|
|
3567
|
+
return ok3("Stopped. The plan made no progress.");
|
|
3568
|
+
}
|
|
3569
|
+
const next = previous ? {
|
|
3570
|
+
...previous,
|
|
3571
|
+
status: "pending",
|
|
3572
|
+
stalled: false,
|
|
3573
|
+
rebuilds: previous.rebuilds + 1,
|
|
3574
|
+
nodes: parsed,
|
|
3575
|
+
results: []
|
|
3576
|
+
} : freshPlan(task, parsed);
|
|
3577
|
+
await writePlan(root, changeId, sectionId, next);
|
|
3578
|
+
return ok3(["Accept this plan before anyone writes.", ...planLines(parsed)].join("\n"));
|
|
3579
|
+
}
|
|
3580
|
+
function planFailure(parsed) {
|
|
3581
|
+
return parsed === "deep" ? "A plan is deeper than 2." : "The plan is not valid.";
|
|
3582
|
+
}
|
|
3583
|
+
function sameArtifacts(left, right) {
|
|
3584
|
+
const a = artifactsOf(left).slice().sort();
|
|
3585
|
+
const b = artifactsOf(right).slice().sort();
|
|
3586
|
+
return a.length === b.length && a.every((item, index) => item === b[index]);
|
|
3587
|
+
}
|
|
3588
|
+
async function acceptNetwork(root, input) {
|
|
3589
|
+
const changeId = input.change?.trim() ?? "";
|
|
3590
|
+
const sectionId = input.section?.trim() ?? "";
|
|
3591
|
+
if (!safeChangeId2(changeId) || !sectionId) return fail3("No plan is stored.");
|
|
3592
|
+
const plan = await readPlan(root, changeId, sectionId);
|
|
3593
|
+
if (!plan) return fail3("No plan is stored.");
|
|
3594
|
+
if (plan.status !== "pending") return fail3("The plan is not waiting for acceptance.");
|
|
3595
|
+
plan.status = "accepted";
|
|
3596
|
+
await writePlan(root, changeId, sectionId, plan);
|
|
3597
|
+
return ok3("Plan accepted.");
|
|
3598
|
+
}
|
|
3599
|
+
function walk(nodes) {
|
|
3600
|
+
const flat = [];
|
|
3601
|
+
for (const node of nodes) {
|
|
3602
|
+
flat.push(node);
|
|
3603
|
+
flat.push(...walk(node.children));
|
|
3604
|
+
}
|
|
3605
|
+
return flat;
|
|
3606
|
+
}
|
|
3607
|
+
function ready(node, results) {
|
|
3608
|
+
if (results.some((item) => item.decision === node.decision)) return false;
|
|
3609
|
+
const afterOk = node.after.every((id) => results.some((item) => item.decision === id));
|
|
3610
|
+
const childrenOk = node.children.every(
|
|
3611
|
+
(child) => results.some((item) => item.decision === child.decision)
|
|
3612
|
+
);
|
|
3613
|
+
return afterOk && childrenOk;
|
|
3614
|
+
}
|
|
3615
|
+
async function runNetwork(root, input) {
|
|
3616
|
+
const changeId = input.change?.trim() ?? "";
|
|
3617
|
+
const sectionId = input.section?.trim() ?? "";
|
|
3618
|
+
if (!safeChangeId2(changeId) || !sectionId) return fail3("No plan is stored.");
|
|
3619
|
+
const plan = await readPlan(root, changeId, sectionId);
|
|
3620
|
+
if (!plan) return fail3("No plan is stored.");
|
|
3621
|
+
if (plan.status === "pending") return ok3("Accept this plan before anyone writes.");
|
|
3622
|
+
if (plan.status === "done") return ok3("The parent is done.");
|
|
3623
|
+
if (plan.status !== "accepted" && plan.status !== "open") return fail3("No plan is stored.");
|
|
3624
|
+
if (await messagingOn(root) === "invalid") return fail3(ROLE_TEXT.badYaml);
|
|
3625
|
+
const minutes = await timeoutMinutes(root);
|
|
3626
|
+
if (minutes === "invalid") return fail3(ROLE_TEXT.badYaml);
|
|
3627
|
+
if (!input.model) return fail3(ROLE_TEXT.noModel);
|
|
3628
|
+
const now = (input.now ?? (() => /* @__PURE__ */ new Date()))();
|
|
3629
|
+
const summaries = [];
|
|
3630
|
+
const called = /* @__PURE__ */ new Set();
|
|
3631
|
+
let index = 0;
|
|
3632
|
+
let progressed = true;
|
|
3633
|
+
let missingOwner = false;
|
|
3634
|
+
while (progressed && !missingOwner) {
|
|
3635
|
+
progressed = false;
|
|
3636
|
+
for (const node of walk(plan.nodes)) {
|
|
3637
|
+
if (!ready(node, plan.results) || called.has(node.decision)) continue;
|
|
3638
|
+
called.add(node.decision);
|
|
3639
|
+
const owner = await readRole(root, node.owner);
|
|
3640
|
+
if (!owner) {
|
|
3641
|
+
missingOwner = true;
|
|
3642
|
+
break;
|
|
3643
|
+
}
|
|
3644
|
+
const when = new Date(now.getTime() + index * 1e3);
|
|
3645
|
+
index += 1;
|
|
3646
|
+
const runId = runIdFrom(when);
|
|
3647
|
+
const file = join10(conversationDir(root, owner.name), `${runId}.jsonl`);
|
|
3648
|
+
let raw;
|
|
3649
|
+
try {
|
|
3650
|
+
raw = await input.model.complete({
|
|
3651
|
+
roleId: owner.name,
|
|
3652
|
+
prompt: [
|
|
3653
|
+
`Job: ${owner.job}`,
|
|
3654
|
+
`Description: ${owner.description}`,
|
|
3655
|
+
`Task: ${plan.task}`,
|
|
3656
|
+
`Artifact: ${node.artifact}`,
|
|
3657
|
+
`Boundary: ${node.boundary}`
|
|
3658
|
+
].join("\n")
|
|
3659
|
+
});
|
|
3660
|
+
} catch {
|
|
3661
|
+
continue;
|
|
3662
|
+
}
|
|
3663
|
+
const parsed = parseReply(raw);
|
|
3664
|
+
if (!parsed) continue;
|
|
3665
|
+
await appendJson(file, { type: "model", pass: 1, roleId: owner.name, text: parsed.text });
|
|
3666
|
+
if (parsed.action) {
|
|
3667
|
+
const expiresAt = new Date(when.getTime() + minutes * 6e4).toISOString();
|
|
3668
|
+
await appendJson(file, {
|
|
3669
|
+
type: "pending",
|
|
3670
|
+
action: {
|
|
3671
|
+
name: parsed.action.name,
|
|
3672
|
+
words: parsed.action.words,
|
|
3673
|
+
inputs: parsed.action.inputs
|
|
3674
|
+
},
|
|
3675
|
+
expiresAt
|
|
3676
|
+
});
|
|
3677
|
+
}
|
|
3678
|
+
plan.results.push({
|
|
3679
|
+
decision: node.decision,
|
|
3680
|
+
artifact: node.artifact,
|
|
3681
|
+
summary: parsed.text
|
|
3682
|
+
});
|
|
3683
|
+
summaries.push(`${owner.name}: ${parsed.text}`);
|
|
3684
|
+
progressed = true;
|
|
3685
|
+
}
|
|
3686
|
+
}
|
|
3687
|
+
const all = walk(plan.nodes);
|
|
3688
|
+
const waiting = all.some((node) => !plan.results.some((item) => item.decision === node.decision));
|
|
3689
|
+
plan.status = waiting ? "open" : "done";
|
|
3690
|
+
await writePlan(root, changeId, sectionId, plan);
|
|
3691
|
+
if (missingOwner) return fail3(ROLE_TEXT.notFound);
|
|
3692
|
+
const lines = [...summaries];
|
|
3693
|
+
if (waiting) {
|
|
3694
|
+
lines.push("A branch is waiting for its prerequisites.");
|
|
3695
|
+
lines.push("The parent stays open.");
|
|
3696
|
+
} else {
|
|
3697
|
+
lines.push("The parent is done.");
|
|
3698
|
+
}
|
|
3699
|
+
return ok3(lines.join("\n"));
|
|
3700
|
+
}
|
|
3701
|
+
async function rebuildNetwork(root, input) {
|
|
3702
|
+
const changeId = input.change?.trim() ?? "";
|
|
3703
|
+
const sectionId = input.section?.trim() ?? "";
|
|
3704
|
+
if (!safeChangeId2(changeId) || !sectionId) return fail3("No plan is stored.");
|
|
3705
|
+
const plan = await readPlan(root, changeId, sectionId);
|
|
3706
|
+
if (!plan) return fail3("No plan is stored.");
|
|
3707
|
+
const allowed = plan.rebuilds === 0 || plan.rebuilds === 1 && !plan.stalled && plan.status === "accepted";
|
|
3708
|
+
if (!allowed) return fail3("A second rebuild does not start.");
|
|
3709
|
+
const loaded = await acceptedFacts(root, changeId, sectionId);
|
|
3710
|
+
if (!loaded) return fail3(ROLE_TEXT.needsSection);
|
|
3711
|
+
if (!input.model) return fail3(ROLE_TEXT.noModel);
|
|
3712
|
+
const services = distinctServices(loaded.all);
|
|
3713
|
+
const picked = pickLead(loaded.all, services);
|
|
3714
|
+
if (picked.kind === "missing") return fail3(picked.sentence);
|
|
3715
|
+
if (picked.kind !== "lead") return fail3("This module needs a lead.");
|
|
3716
|
+
const lead = await readRole(root, picked.name);
|
|
3717
|
+
if (!lead) return fail3(ROLE_TEXT.notFound);
|
|
3718
|
+
const before = JSON.stringify(plan);
|
|
3719
|
+
const result = await storeLeadPlan(
|
|
3720
|
+
root,
|
|
3721
|
+
changeId,
|
|
3722
|
+
sectionId,
|
|
3723
|
+
plan.task,
|
|
3724
|
+
lead,
|
|
3725
|
+
services,
|
|
3726
|
+
loaded.all,
|
|
3727
|
+
input.model,
|
|
3728
|
+
plan
|
|
3729
|
+
);
|
|
3730
|
+
if (result.exitCode !== 0) {
|
|
3731
|
+
const current = await readPlan(root, changeId, sectionId);
|
|
3732
|
+
if (current && JSON.stringify(current) !== before) {
|
|
3733
|
+
await writePlan(root, changeId, sectionId, JSON.parse(before));
|
|
3734
|
+
}
|
|
3735
|
+
}
|
|
3736
|
+
return result;
|
|
3737
|
+
}
|
|
3738
|
+
function pickLead(all, services) {
|
|
3739
|
+
const modules = /* @__PURE__ */ new Set();
|
|
3740
|
+
let missingModule = false;
|
|
3741
|
+
for (const section of services.values()) {
|
|
3742
|
+
const moduleId2 = section.modules[0];
|
|
3743
|
+
if (!moduleId2) missingModule = true;
|
|
3744
|
+
else modules.add(moduleId2);
|
|
3745
|
+
}
|
|
3746
|
+
const moduleId = [...modules][0];
|
|
3747
|
+
if (!missingModule && modules.size === 1 && moduleId && services.size > 1) {
|
|
3748
|
+
const name = all.find((item) => item.modules.includes(moduleId) && item.leads[0])?.leads[0] ?? "";
|
|
3749
|
+
if (!name) return { kind: "missing", sentence: "This module needs a lead." };
|
|
3750
|
+
return { kind: "lead", name };
|
|
3751
|
+
}
|
|
3752
|
+
if (modules.size > 1) {
|
|
3753
|
+
const name = all.find((item) => item.product && item.leads[0])?.leads[0] ?? "";
|
|
3754
|
+
if (!name) return { kind: "missing", sentence: "This product needs a lead." };
|
|
3755
|
+
return { kind: "lead", name };
|
|
3756
|
+
}
|
|
3757
|
+
return { kind: "none" };
|
|
3758
|
+
}
|
|
3759
|
+
|
|
3760
|
+
// src/stories.ts
|
|
3761
|
+
import { join as join11 } from "pathe";
|
|
3762
|
+
var STORY_ID = /^S-\d+$/i;
|
|
3763
|
+
var EPIC_KEY = /^[A-Za-z0-9][A-Za-z0-9-]{0,39}$/;
|
|
3764
|
+
function validBranchName(name) {
|
|
3765
|
+
if (!name || name === "." || name === ".." || name.includes(" ") || name.includes("..")) {
|
|
3766
|
+
return false;
|
|
3767
|
+
}
|
|
3768
|
+
if (name.startsWith("/") || name.endsWith("/") || name.endsWith(".lock") || name.includes("@{")) {
|
|
3769
|
+
return false;
|
|
3770
|
+
}
|
|
3771
|
+
if (/[\\~^:?*[]/.test(name)) return false;
|
|
3772
|
+
return name.split("/").every((part) => part.length > 0 && part !== "." && part !== "..");
|
|
3773
|
+
}
|
|
3774
|
+
function parseStoryDraft(input) {
|
|
3775
|
+
const epic = input.epic?.trim() ?? "";
|
|
3776
|
+
const storyRaw = input.story?.trim() ?? "";
|
|
3777
|
+
const dependsRaw = (input.depends ?? []).map((item) => item.trim()).filter((item) => item.length > 0);
|
|
3778
|
+
if ((storyRaw || dependsRaw.length > 0) && !epic) {
|
|
3779
|
+
return { ok: false, stdout: "A story needs an epic." };
|
|
3780
|
+
}
|
|
3781
|
+
if (epic && !EPIC_KEY.test(epic)) return { ok: false, stdout: "An epic key is not valid." };
|
|
3782
|
+
if (dependsRaw.length > 0 && !storyRaw) return { ok: false, stdout: "A story id is not valid." };
|
|
3783
|
+
if (storyRaw && !STORY_ID.test(storyRaw))
|
|
3784
|
+
return { ok: false, stdout: "A story id is not valid." };
|
|
3785
|
+
const depends = [];
|
|
3786
|
+
for (const dep of dependsRaw) {
|
|
3787
|
+
if (!STORY_ID.test(dep)) return { ok: false, stdout: "A story id is not valid." };
|
|
3788
|
+
const id = dep.toUpperCase();
|
|
3789
|
+
if (!depends.includes(id)) depends.push(id);
|
|
3790
|
+
}
|
|
3791
|
+
return {
|
|
3792
|
+
ok: true,
|
|
3793
|
+
draft: {
|
|
3794
|
+
epic: epic || void 0,
|
|
3795
|
+
story: storyRaw ? storyRaw.toUpperCase() : void 0,
|
|
3796
|
+
depends
|
|
3797
|
+
}
|
|
3798
|
+
};
|
|
3799
|
+
}
|
|
3800
|
+
async function metasIn(dir) {
|
|
3801
|
+
const out = [];
|
|
3802
|
+
for (const id of await listDirs(dir)) {
|
|
3803
|
+
const path = join11(dir, id, "meta.yaml");
|
|
3804
|
+
if (!await pathExists(path)) continue;
|
|
3805
|
+
try {
|
|
3806
|
+
out.push(ChangeMetaSchema.parse(await readYaml(path)));
|
|
3807
|
+
} catch {
|
|
3808
|
+
}
|
|
3809
|
+
}
|
|
3810
|
+
return out;
|
|
3811
|
+
}
|
|
3812
|
+
async function loadEpicStories(projectRoot, config, epic) {
|
|
3813
|
+
const metas = [
|
|
3814
|
+
...await metasIn(changesDir(projectRoot, config)),
|
|
3815
|
+
...await metasIn(archiveDir(projectRoot, config))
|
|
3816
|
+
];
|
|
3817
|
+
const nodes = [];
|
|
3818
|
+
for (const meta of metas) {
|
|
3819
|
+
if (meta.epic !== epic || !meta.story) continue;
|
|
3820
|
+
nodes.push({
|
|
3821
|
+
changeId: meta.id,
|
|
3822
|
+
title: meta.title,
|
|
3823
|
+
story: meta.story.toUpperCase(),
|
|
3824
|
+
epic: meta.epic,
|
|
3825
|
+
depends: (meta.depends ?? []).map((item) => item.toUpperCase()),
|
|
3826
|
+
status: meta.status
|
|
3827
|
+
});
|
|
3828
|
+
}
|
|
3829
|
+
return nodes;
|
|
3830
|
+
}
|
|
3831
|
+
async function storyIdTaken(projectRoot, config, epic, story) {
|
|
3832
|
+
const nodes = await loadEpicStories(projectRoot, config, epic);
|
|
3833
|
+
return nodes.some((node) => node.story === story.toUpperCase());
|
|
3834
|
+
}
|
|
3835
|
+
function byStoryId(left, right) {
|
|
3836
|
+
if (left.story < right.story) return -1;
|
|
3837
|
+
if (left.story > right.story) return 1;
|
|
3838
|
+
return 0;
|
|
3839
|
+
}
|
|
3840
|
+
function formatEpicGraph(nodes) {
|
|
3841
|
+
const ordered = [...nodes].sort(byStoryId);
|
|
3842
|
+
const byStory = /* @__PURE__ */ new Map();
|
|
3843
|
+
for (const node of ordered) {
|
|
3844
|
+
if (byStory.has(node.story))
|
|
3845
|
+
return { exitCode: 1, stdout: `Story ${node.story} already exists.` };
|
|
3846
|
+
byStory.set(node.story, node);
|
|
3847
|
+
}
|
|
3848
|
+
for (const node of ordered) {
|
|
3849
|
+
for (const dep of [...node.depends].sort()) {
|
|
3850
|
+
if (!byStory.has(dep)) {
|
|
3851
|
+
return {
|
|
3852
|
+
exitCode: 1,
|
|
3853
|
+
stdout: `${node.story} depends on ${dep}, which is not in this epic.`
|
|
3854
|
+
};
|
|
3855
|
+
}
|
|
3856
|
+
}
|
|
3857
|
+
}
|
|
3858
|
+
const wave = /* @__PURE__ */ new Map();
|
|
3859
|
+
const stack = [];
|
|
3860
|
+
const visit = (id) => {
|
|
3861
|
+
if (wave.has(id)) return null;
|
|
3862
|
+
if (stack.includes(id)) {
|
|
3863
|
+
const at = stack.indexOf(id);
|
|
3864
|
+
return `Story cycle: ${[...stack.slice(at), id].join(" -> ")}`;
|
|
3865
|
+
}
|
|
3866
|
+
stack.push(id);
|
|
3867
|
+
const node = byStory.get(id);
|
|
3868
|
+
if (!node) return null;
|
|
3869
|
+
let deepest = 0;
|
|
3870
|
+
for (const dep of [...node.depends].sort()) {
|
|
3871
|
+
const err = visit(dep);
|
|
3872
|
+
if (err) return err;
|
|
3873
|
+
deepest = Math.max(deepest, wave.get(dep) ?? 0);
|
|
3874
|
+
}
|
|
3875
|
+
stack.pop();
|
|
3876
|
+
wave.set(id, deepest + 1);
|
|
3877
|
+
return null;
|
|
3878
|
+
};
|
|
3879
|
+
for (const node of ordered) {
|
|
3880
|
+
const err = visit(node.story);
|
|
3881
|
+
if (err) return { exitCode: 1, stdout: err };
|
|
3882
|
+
}
|
|
3883
|
+
const groups = /* @__PURE__ */ new Map();
|
|
3884
|
+
for (const node of ordered) {
|
|
3885
|
+
const number = wave.get(node.story) ?? 1;
|
|
3886
|
+
const list = groups.get(number) ?? [];
|
|
3887
|
+
list.push(node);
|
|
3888
|
+
groups.set(number, list);
|
|
3889
|
+
}
|
|
3890
|
+
const lines = [];
|
|
3891
|
+
const parallel = [];
|
|
3892
|
+
for (const number of [...groups.keys()].sort((left, right) => left - right)) {
|
|
3893
|
+
const group = groups.get(number) ?? [];
|
|
3894
|
+
lines.push(`Wave ${number}`);
|
|
3895
|
+
for (const node of group) {
|
|
3896
|
+
const deps = [...node.depends].sort();
|
|
3897
|
+
lines.push(
|
|
3898
|
+
deps.length > 0 ? `${node.story} ${node.title} after ${deps.join(", ")}` : `${node.story} ${node.title}`
|
|
3899
|
+
);
|
|
3900
|
+
}
|
|
3901
|
+
if (group.length > 1) parallel.push(group.map((node) => node.story).join(", "));
|
|
3902
|
+
}
|
|
3903
|
+
if (parallel.length === 0) lines.push("Parallel: none");
|
|
3904
|
+
for (const line of parallel) lines.push(`Parallel: ${line}`);
|
|
3905
|
+
const serial = ordered.filter((node) => node.depends.length > 0);
|
|
3906
|
+
if (serial.length === 0) lines.push("Serial: none");
|
|
3907
|
+
for (const node of serial) {
|
|
3908
|
+
lines.push(`Serial: ${node.story} after ${[...node.depends].sort().join(", ")}`);
|
|
3909
|
+
}
|
|
3910
|
+
return { exitCode: 0, stdout: lines.join("\n") };
|
|
3911
|
+
}
|
|
3912
|
+
function branchBlock(nodes, story) {
|
|
3913
|
+
const graph = formatEpicGraph(nodes);
|
|
3914
|
+
if (graph.exitCode !== 0) return graph.stdout;
|
|
3915
|
+
const node = nodes.find((item) => item.story === story);
|
|
3916
|
+
if (!node) return null;
|
|
3917
|
+
const waiting = [...node.depends].sort().filter((dep) => {
|
|
3918
|
+
const other = nodes.find((item) => item.story === dep);
|
|
3919
|
+
return !other || other.status !== "completed";
|
|
3920
|
+
});
|
|
3921
|
+
if (waiting.length === 0) return null;
|
|
3922
|
+
return `${story} waits on ${waiting.join(", ")}.`;
|
|
3923
|
+
}
|
|
2093
3924
|
export {
|
|
2094
3925
|
AGENT_INTEGRATIONS,
|
|
2095
3926
|
AGENT_TARGET_OPTIONS,
|
|
2096
3927
|
ALL_AGENT_TARGETS,
|
|
2097
3928
|
AdapterRegistry,
|
|
3929
|
+
AgentInstallPreflightError,
|
|
2098
3930
|
ArtifactDefSchema,
|
|
2099
3931
|
ChangeMetaSchema,
|
|
3932
|
+
ChangeMetricsSchema,
|
|
2100
3933
|
ConfigSchema,
|
|
2101
3934
|
DEFAULT_CODE_CONTEXT_CAPS,
|
|
2102
3935
|
DEFAULT_INIT_INTEGRATION,
|
|
3936
|
+
EMBEDDINGS_UNAVAILABLE,
|
|
3937
|
+
EMBEDDING_MODEL,
|
|
2103
3938
|
GateSchema,
|
|
2104
3939
|
McpConfigSchema,
|
|
2105
3940
|
McpInvokeSchema,
|
|
2106
3941
|
McpSourceSchema,
|
|
2107
3942
|
McpWhenSchema,
|
|
2108
3943
|
PROTOCOL_MD,
|
|
3944
|
+
PhaseMetricSchema,
|
|
3945
|
+
RRF_K,
|
|
2109
3946
|
SDD_AGENT_ROLES,
|
|
2110
3947
|
SDD_CORE_VERSION,
|
|
2111
3948
|
SDD_DIR,
|
|
3949
|
+
SUMMARY_TOKEN_CAP,
|
|
2112
3950
|
SddError,
|
|
3951
|
+
SearchConfigError,
|
|
2113
3952
|
StageSchema,
|
|
3953
|
+
TokenUsageMetricSchema,
|
|
2114
3954
|
VerifyCommandSchema,
|
|
2115
3955
|
WorkflowSchema,
|
|
3956
|
+
acceptNetwork,
|
|
2116
3957
|
activePointerPath,
|
|
2117
3958
|
activeStages,
|
|
2118
3959
|
addMcpSource,
|
|
2119
3960
|
advanceStage,
|
|
2120
3961
|
agentHostPaths,
|
|
2121
3962
|
agentKickoffMessage,
|
|
3963
|
+
allowRole,
|
|
2122
3964
|
approveGate,
|
|
2123
3965
|
archiveDir,
|
|
3966
|
+
assertAgentInstallable,
|
|
3967
|
+
assertSessionLogAllowed,
|
|
3968
|
+
branchBlock,
|
|
3969
|
+
budgetForStage,
|
|
2124
3970
|
buildAgentPrompt,
|
|
2125
3971
|
buildContext,
|
|
2126
3972
|
buildRefinePlan,
|
|
3973
|
+
buildRetrievedInner,
|
|
2127
3974
|
callMcpSourceTool,
|
|
2128
3975
|
canLeaveStage,
|
|
2129
3976
|
changeDirName,
|
|
2130
3977
|
changeMetaPath,
|
|
2131
3978
|
changePath,
|
|
2132
3979
|
changesDir,
|
|
3980
|
+
changesPathRel,
|
|
3981
|
+
chunkCode,
|
|
3982
|
+
chunkMarkdown,
|
|
2133
3983
|
classifyPath,
|
|
2134
3984
|
clearActiveIf,
|
|
3985
|
+
clearRegisteredSearchProviders,
|
|
3986
|
+
compareChangesByFirstPhase,
|
|
2135
3987
|
completeChange,
|
|
2136
3988
|
configPath,
|
|
3989
|
+
contentHash,
|
|
2137
3990
|
copyDir,
|
|
2138
3991
|
copyDirSkipExisting,
|
|
3992
|
+
costFor,
|
|
3993
|
+
countTokens,
|
|
3994
|
+
createBuiltinProvider,
|
|
2139
3995
|
createChange,
|
|
3996
|
+
createFastembedEmbedder,
|
|
3997
|
+
createRole,
|
|
2140
3998
|
defaultConfig,
|
|
2141
3999
|
defaultMcpConfig,
|
|
2142
4000
|
defaultMcpYamlPath,
|
|
2143
4001
|
defaultMemoryDir,
|
|
4002
|
+
defaultPricesYamlPath,
|
|
4003
|
+
defaultSearchYamlPath,
|
|
2144
4004
|
defaultTemplatesDir,
|
|
2145
4005
|
defaultWorkflowsDir,
|
|
2146
4006
|
defaultsRoot,
|
|
4007
|
+
deleteRole,
|
|
4008
|
+
describeFsError,
|
|
4009
|
+
describeKept,
|
|
4010
|
+
displayArchivePath,
|
|
4011
|
+
displayChangePath,
|
|
2147
4012
|
domainsDir,
|
|
4013
|
+
duplicateRole,
|
|
4014
|
+
editRole,
|
|
4015
|
+
embedSearchQuery,
|
|
4016
|
+
emptyChangeMetrics,
|
|
2148
4017
|
ensureDir,
|
|
2149
4018
|
ensureMcpConfig,
|
|
2150
4019
|
errorMessage,
|
|
4020
|
+
executeSearch,
|
|
2151
4021
|
explainMatch,
|
|
4022
|
+
extractiveSummary,
|
|
2152
4023
|
fallbackAdapter,
|
|
2153
4024
|
fallbackIncompleteStageId,
|
|
4025
|
+
fallbackWarning,
|
|
4026
|
+
fastembedImportCount,
|
|
2154
4027
|
findFeaturesBacklogPath,
|
|
2155
4028
|
firstActiveStageId,
|
|
4029
|
+
formatDuration,
|
|
4030
|
+
formatEpicGraph,
|
|
2156
4031
|
formatJsonSummary,
|
|
2157
4032
|
formatMcpContextForHandoff,
|
|
2158
4033
|
formatStatus,
|
|
4034
|
+
ftsTokens,
|
|
4035
|
+
fuseByRank,
|
|
4036
|
+
fuseHits,
|
|
2159
4037
|
gatherMcpContext,
|
|
2160
4038
|
gatherMcpContextForChange,
|
|
2161
4039
|
generateCodeContext,
|
|
4040
|
+
generatedState,
|
|
2162
4041
|
getActiveChangeId,
|
|
4042
|
+
getHeadSha,
|
|
2163
4043
|
getIntegration,
|
|
2164
4044
|
getStage,
|
|
4045
|
+
guidancePaths,
|
|
4046
|
+
handoffRole,
|
|
4047
|
+
hideRole,
|
|
4048
|
+
importSessionMetrics,
|
|
2165
4049
|
incompletePriorStageErrors,
|
|
4050
|
+
indexDbPath,
|
|
2166
4051
|
inferComplexity,
|
|
2167
4052
|
initProject,
|
|
2168
4053
|
installAgentIntegration,
|
|
@@ -2170,67 +4055,121 @@ export {
|
|
|
2170
4055
|
integrationKeyFor,
|
|
2171
4056
|
interpolate,
|
|
2172
4057
|
interpolateArgs,
|
|
4058
|
+
isHardDenied,
|
|
2173
4059
|
isIgnoredPath,
|
|
2174
4060
|
isInitialized,
|
|
4061
|
+
isPrunedDir,
|
|
2175
4062
|
isSddError,
|
|
2176
4063
|
isSecretPath,
|
|
2177
4064
|
isStageSkipped,
|
|
2178
4065
|
isSubstantiveArtifactContent,
|
|
4066
|
+
keywordHits,
|
|
2179
4067
|
listBacklogFeatures,
|
|
2180
4068
|
listChanges,
|
|
2181
4069
|
listDirs,
|
|
2182
4070
|
listFiles,
|
|
2183
4071
|
listMatchingSourceIds,
|
|
2184
4072
|
listMcpSourceTools,
|
|
4073
|
+
listScopeHits,
|
|
2185
4074
|
listWorkflowNames,
|
|
2186
4075
|
loadChangeMeta,
|
|
2187
4076
|
loadConfig,
|
|
4077
|
+
loadEpicStories,
|
|
4078
|
+
loadExcluder,
|
|
4079
|
+
loadGeneratedManifest,
|
|
2188
4080
|
loadInstalledAgent,
|
|
2189
4081
|
loadMcpConfig,
|
|
4082
|
+
loadPrices,
|
|
4083
|
+
loadSearchConfig,
|
|
2190
4084
|
loadWorkflow,
|
|
4085
|
+
markGenerated,
|
|
2191
4086
|
matchMcpSources,
|
|
2192
4087
|
mcpConfigPath,
|
|
2193
4088
|
mcpTimeoutMs,
|
|
2194
4089
|
memoryDir,
|
|
4090
|
+
memoryPathRel,
|
|
2195
4091
|
mergeCaps,
|
|
4092
|
+
mergeConfigDefaults,
|
|
4093
|
+
mergeRankedHits,
|
|
2196
4094
|
nextStageId,
|
|
2197
4095
|
nowIso,
|
|
2198
4096
|
optionForTarget,
|
|
4097
|
+
packHits,
|
|
2199
4098
|
parseAgentTargets,
|
|
2200
4099
|
parseFeaturesBacklog,
|
|
2201
4100
|
parseIntegration,
|
|
4101
|
+
parseStoryDraft,
|
|
2202
4102
|
pathExists,
|
|
4103
|
+
phaseTiming,
|
|
4104
|
+
pinRole,
|
|
4105
|
+
planAgentIntegration,
|
|
4106
|
+
planNetwork,
|
|
2203
4107
|
promoteGreenfieldToMemory,
|
|
4108
|
+
rankScope,
|
|
4109
|
+
readChangeMetrics,
|
|
4110
|
+
readHandoffCodePoints,
|
|
2204
4111
|
readText,
|
|
2205
4112
|
readYaml,
|
|
4113
|
+
rebuildNetwork,
|
|
2206
4114
|
recommendWorkflow,
|
|
4115
|
+
reconcileGeneratedTree,
|
|
2207
4116
|
refreshActiveAgentContext,
|
|
4117
|
+
refreshConfiguredGuidance,
|
|
4118
|
+
registerSearchProvider,
|
|
2208
4119
|
removeMcpSource,
|
|
2209
4120
|
removeOtherAgentHosts,
|
|
4121
|
+
removeOtherAgentHostsDetailed,
|
|
2210
4122
|
removePath,
|
|
4123
|
+
renderAgentsMd,
|
|
4124
|
+
renderBlock,
|
|
4125
|
+
renderChangeReport,
|
|
4126
|
+
renderMemoryIndex,
|
|
4127
|
+
renderProtocolMd,
|
|
4128
|
+
renderSddReadme,
|
|
2211
4129
|
renderThinAgent,
|
|
4130
|
+
repairRequiredIgnores,
|
|
4131
|
+
reportAllChanges,
|
|
4132
|
+
reportChange,
|
|
4133
|
+
resolveActiveDirectory,
|
|
2212
4134
|
resolveChangeId,
|
|
4135
|
+
resolveEmbedder,
|
|
2213
4136
|
resolveFocus,
|
|
4137
|
+
resolveSearchProvider,
|
|
2214
4138
|
resolveStages,
|
|
4139
|
+
rrf,
|
|
2215
4140
|
runLocalVerify,
|
|
4141
|
+
runNetwork,
|
|
4142
|
+
runRole,
|
|
2216
4143
|
saveChangeMeta,
|
|
2217
4144
|
saveConfig,
|
|
4145
|
+
saveGeneratedManifest,
|
|
2218
4146
|
saveMcpConfig,
|
|
4147
|
+
scopeForPath,
|
|
2219
4148
|
sddRoot,
|
|
4149
|
+
searchConfigPath,
|
|
4150
|
+
searchPreamble,
|
|
2220
4151
|
setActiveChange,
|
|
4152
|
+
setRoleMessaging,
|
|
2221
4153
|
shouldAutoSkip,
|
|
4154
|
+
showRoleGroup,
|
|
4155
|
+
showRoles,
|
|
2222
4156
|
skipStage,
|
|
2223
4157
|
slugify,
|
|
2224
4158
|
startFeatureFromBacklog,
|
|
2225
4159
|
startGreenfield,
|
|
4160
|
+
startPhase,
|
|
4161
|
+
storyIdTaken,
|
|
2226
4162
|
switchWorkflow,
|
|
4163
|
+
syncChangeHead,
|
|
2227
4164
|
templatesDir,
|
|
2228
4165
|
toProcessEnv,
|
|
2229
4166
|
todayIsoDate,
|
|
2230
4167
|
tokenizeKeywords,
|
|
4168
|
+
transitionPhase,
|
|
2231
4169
|
typescriptAdapter,
|
|
2232
4170
|
uniqueChangeDirName,
|
|
2233
4171
|
updateFeatureStatus,
|
|
4172
|
+
validBranchName,
|
|
2234
4173
|
withMcpSource,
|
|
2235
4174
|
workflowsDir,
|
|
2236
4175
|
writeAgentHandoff,
|