@structured-vibe-coding/core 0.18.0 → 0.19.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/teams/research/roles/researcher.md +29 -0
- package/defaults/teams/research/team.yaml +7 -0
- package/defaults/teams/software/roles/architect.md +31 -0
- package/defaults/teams/software/roles/dev-lead.md +32 -0
- package/defaults/teams/software/roles/developer.md +30 -0
- package/defaults/teams/software/roles/product-manager.md +32 -0
- package/defaults/teams/software/roles/product-owner.md +30 -0
- package/defaults/teams/software/roles/qa.md +35 -0
- package/defaults/teams/software/routing.yaml +28 -0
- package/defaults/teams/software/team.yaml +24 -0
- package/defaults/teams/solo/roles/developer.md +28 -0
- package/defaults/teams/solo/team.yaml +7 -0
- package/defaults/templates/plan.md +17 -0
- package/defaults/templates/review.md +11 -0
- package/defaults/templates/story.md +13 -0
- package/defaults/templates/task.md +11 -0
- package/defaults/workflows/team-software.yaml +153 -0
- package/dist/{agents-AKSUXQHN.js → agents-PSIOQEYH.js} +3 -3
- package/dist/{change-context-REE7UKU7.js → change-context-EZJMSGSP.js} +1 -1
- package/dist/{chunk-4PIRWYRA.js → chunk-26YG6A4M.js} +166 -81
- package/dist/{chunk-LHSR7JG2.js → chunk-2FJBFYVJ.js} +306 -31
- package/dist/{chunk-QGP43XCN.js → chunk-H55RFAEI.js} +1 -1
- package/dist/{chunk-QHV5KZKX.js → chunk-HG7TPW5V.js} +93 -8
- package/dist/{chunk-44LQ5BKZ.js → chunk-TGRV6WGS.js} +812 -4
- package/dist/{gather-ZNV2NAUN.js → gather-7TSVKQO2.js} +1 -1
- package/dist/{greenfield-I5S5OS2D.js → greenfield-QFA5WVHE.js} +3 -3
- package/dist/index.d.ts +804 -10
- package/dist/index.js +2765 -524
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -11,7 +11,7 @@ import {
|
|
|
11
11
|
mcpTimeoutMs,
|
|
12
12
|
toProcessEnv,
|
|
13
13
|
withMcpSource
|
|
14
|
-
} from "./chunk-
|
|
14
|
+
} from "./chunk-H55RFAEI.js";
|
|
15
15
|
import {
|
|
16
16
|
McpConfigSchema,
|
|
17
17
|
McpInvokeSchema,
|
|
@@ -39,13 +39,6 @@ import {
|
|
|
39
39
|
agentKickoffMessage,
|
|
40
40
|
assertAgentInstallable,
|
|
41
41
|
contentHash,
|
|
42
|
-
defaultMcpYamlPath,
|
|
43
|
-
defaultMemoryDir,
|
|
44
|
-
defaultPricesYamlPath,
|
|
45
|
-
defaultSearchYamlPath,
|
|
46
|
-
defaultTemplatesDir,
|
|
47
|
-
defaultWorkflowsDir,
|
|
48
|
-
defaultsRoot,
|
|
49
42
|
describeFsError,
|
|
50
43
|
describeKept,
|
|
51
44
|
generatedState,
|
|
@@ -55,11 +48,13 @@ import {
|
|
|
55
48
|
integrationKeyFor,
|
|
56
49
|
loadGeneratedManifest,
|
|
57
50
|
loadInstalledAgent,
|
|
51
|
+
loadProductTeam,
|
|
58
52
|
markGenerated,
|
|
59
53
|
optionForTarget,
|
|
60
54
|
parseAgentTargets,
|
|
61
55
|
parseIntegration,
|
|
62
56
|
planAgentIntegration,
|
|
57
|
+
productTeamHome,
|
|
63
58
|
reconcileGeneratedTree,
|
|
64
59
|
refreshActiveAgentContext,
|
|
65
60
|
refreshConfiguredGuidance,
|
|
@@ -67,14 +62,16 @@ import {
|
|
|
67
62
|
removeOtherAgentHostsDetailed,
|
|
68
63
|
renderAgentsMd,
|
|
69
64
|
renderMemoryIndex,
|
|
65
|
+
renderProductTeamSection,
|
|
70
66
|
renderProtocolMd,
|
|
71
67
|
renderSddReadme,
|
|
72
68
|
renderThinAgent,
|
|
73
69
|
saveGeneratedManifest,
|
|
74
70
|
writeAgentHandoff
|
|
75
|
-
} from "./chunk-
|
|
71
|
+
} from "./chunk-26YG6A4M.js";
|
|
76
72
|
import {
|
|
77
73
|
ChangeMetricsSchema,
|
|
74
|
+
DecisionMetricSchema,
|
|
78
75
|
PhaseMetricSchema,
|
|
79
76
|
TokenUsageMetricSchema,
|
|
80
77
|
advanceStage,
|
|
@@ -93,6 +90,9 @@ import {
|
|
|
93
90
|
parseFeaturesBacklog,
|
|
94
91
|
promoteGreenfieldToMemory,
|
|
95
92
|
readChangeMetrics,
|
|
93
|
+
recordDecisionMetric,
|
|
94
|
+
recordDecisionRequest,
|
|
95
|
+
recordSignoff,
|
|
96
96
|
skipStage,
|
|
97
97
|
startFeatureFromBacklog,
|
|
98
98
|
startGreenfield,
|
|
@@ -100,60 +100,95 @@ import {
|
|
|
100
100
|
switchWorkflow,
|
|
101
101
|
transitionPhase,
|
|
102
102
|
updateFeatureStatus
|
|
103
|
-
} from "./chunk-
|
|
103
|
+
} from "./chunk-2FJBFYVJ.js";
|
|
104
104
|
import {
|
|
105
105
|
EMBEDDINGS_UNAVAILABLE,
|
|
106
106
|
EMBEDDING_MODEL,
|
|
107
|
+
ModelsConfigSchema,
|
|
108
|
+
ROLE_TEXT,
|
|
107
109
|
RRF_K,
|
|
108
110
|
SUMMARY_TOKEN_CAP,
|
|
109
111
|
SearchConfigError,
|
|
112
|
+
TeamSchema,
|
|
113
|
+
WORK_TYPES,
|
|
114
|
+
allowFilePath,
|
|
110
115
|
budgetForStage,
|
|
111
116
|
buildAgentPrompt,
|
|
112
117
|
buildRetrievedInner,
|
|
118
|
+
canonicalRoleName,
|
|
119
|
+
checkModels,
|
|
120
|
+
checkTeam,
|
|
113
121
|
chunkCode,
|
|
114
122
|
chunkMarkdown,
|
|
115
123
|
classifyPath,
|
|
116
124
|
clearRegisteredSearchProviders,
|
|
125
|
+
conversationDir,
|
|
117
126
|
countTokens,
|
|
118
127
|
createBuiltinProvider,
|
|
119
128
|
createFastembedEmbedder,
|
|
129
|
+
createRole,
|
|
130
|
+
createRoleFromTemplate,
|
|
120
131
|
defaultConfig,
|
|
132
|
+
deleteRole,
|
|
133
|
+
duplicateRole,
|
|
134
|
+
editRole,
|
|
121
135
|
embedSearchQuery,
|
|
122
136
|
executeSearch,
|
|
123
137
|
extractiveSummary,
|
|
124
138
|
fallbackWarning,
|
|
125
139
|
fastembedImportCount,
|
|
126
140
|
formatStatus,
|
|
141
|
+
formatTeam,
|
|
127
142
|
ftsTokens,
|
|
128
143
|
fuseByRank,
|
|
129
144
|
fuseHits,
|
|
130
145
|
getHeadSha,
|
|
146
|
+
gitUserName,
|
|
147
|
+
hideRole,
|
|
148
|
+
initTeam,
|
|
131
149
|
isHardDenied,
|
|
132
150
|
isIgnoredPath,
|
|
133
151
|
isInitialized,
|
|
134
152
|
isPrunedDir,
|
|
135
153
|
isSecretPath,
|
|
154
|
+
isWorkType,
|
|
136
155
|
keywordHits,
|
|
156
|
+
listConversationRoles,
|
|
157
|
+
listRoleTemplates,
|
|
137
158
|
listScopeHits,
|
|
159
|
+
listTeamTemplates,
|
|
138
160
|
loadConfig,
|
|
139
161
|
loadExcluder,
|
|
162
|
+
loadModels,
|
|
140
163
|
loadSearchConfig,
|
|
164
|
+
memoryDir as memoryDir2,
|
|
141
165
|
mergeConfigDefaults,
|
|
142
166
|
mergeRankedHits,
|
|
143
167
|
packHits,
|
|
168
|
+
parseRoleDocument,
|
|
169
|
+
pinRole,
|
|
144
170
|
rankScope,
|
|
171
|
+
readAllowFile,
|
|
145
172
|
readPackedCache,
|
|
173
|
+
readRole,
|
|
174
|
+
readTeamConfig,
|
|
146
175
|
registerSearchProvider,
|
|
147
176
|
renderBlock,
|
|
148
177
|
resolveActiveDirectory,
|
|
178
|
+
resolveBot,
|
|
149
179
|
resolveEmbedder,
|
|
150
180
|
resolveSearchProvider,
|
|
181
|
+
roleFilePath,
|
|
151
182
|
rrf,
|
|
152
183
|
saveConfig,
|
|
153
184
|
scopeForPath,
|
|
154
185
|
searchPreamble,
|
|
186
|
+
searchProject,
|
|
187
|
+
showRoles,
|
|
188
|
+
showTeam,
|
|
189
|
+
workKind,
|
|
155
190
|
writePackedCache
|
|
156
|
-
} from "./chunk-
|
|
191
|
+
} from "./chunk-TGRV6WGS.js";
|
|
157
192
|
import {
|
|
158
193
|
ArtifactDefSchema,
|
|
159
194
|
ChangeMetaSchema,
|
|
@@ -166,15 +201,25 @@ import {
|
|
|
166
201
|
buildContext,
|
|
167
202
|
changeDirName,
|
|
168
203
|
clearActiveIf,
|
|
204
|
+
defaultMcpYamlPath,
|
|
205
|
+
defaultMemoryDir,
|
|
206
|
+
defaultPricesYamlPath,
|
|
207
|
+
defaultSearchYamlPath,
|
|
208
|
+
defaultTeamsDir,
|
|
209
|
+
defaultTemplatesDir,
|
|
210
|
+
defaultWorkflowsDir,
|
|
211
|
+
defaultsRoot,
|
|
169
212
|
firstActiveStageId,
|
|
170
213
|
getActiveChangeId,
|
|
171
214
|
getStage,
|
|
172
215
|
inferComplexity,
|
|
173
216
|
isStageSkipped,
|
|
217
|
+
listBundledWorkflowNames,
|
|
174
218
|
listChanges,
|
|
175
219
|
listWorkflowNames,
|
|
176
220
|
loadChangeMeta,
|
|
177
221
|
loadWorkflow,
|
|
222
|
+
missingBundledWorkflows,
|
|
178
223
|
nextStageId,
|
|
179
224
|
nowIso,
|
|
180
225
|
recommendWorkflow,
|
|
@@ -185,8 +230,9 @@ import {
|
|
|
185
230
|
shouldAutoSkip,
|
|
186
231
|
slugify,
|
|
187
232
|
todayIsoDate,
|
|
188
|
-
uniqueChangeDirName
|
|
189
|
-
|
|
233
|
+
uniqueChangeDirName,
|
|
234
|
+
workflowUnavailableMessage
|
|
235
|
+
} from "./chunk-HG7TPW5V.js";
|
|
190
236
|
import {
|
|
191
237
|
SddError,
|
|
192
238
|
errorMessage,
|
|
@@ -245,6 +291,7 @@ var PricesSchema = z.object({
|
|
|
245
291
|
}).catchall(PriceCardSchema)
|
|
246
292
|
});
|
|
247
293
|
var TOKEN_HEADER = "source phase provider model requests input cache_read cache_write output usd inr";
|
|
294
|
+
var DECISION_HEADER = "work_type kind provider model calls input cache_read cache_write output usd inr";
|
|
248
295
|
function finiteMs(startIso, endMs) {
|
|
249
296
|
const start = Date.parse(startIso);
|
|
250
297
|
if (!Number.isFinite(start) || !Number.isFinite(endMs)) return 0;
|
|
@@ -291,14 +338,14 @@ function stripLeadingZeros(digits) {
|
|
|
291
338
|
}
|
|
292
339
|
function rateUnits(rate) {
|
|
293
340
|
if (!Number.isFinite(rate) || rate < 0) throw new Error("prices.yaml is invalid");
|
|
294
|
-
let
|
|
341
|
+
let text2;
|
|
295
342
|
try {
|
|
296
|
-
|
|
343
|
+
text2 = rate.toFixed(9);
|
|
297
344
|
} catch {
|
|
298
345
|
throw new Error("prices.yaml is invalid");
|
|
299
346
|
}
|
|
300
|
-
const negative =
|
|
301
|
-
const unsigned = negative ?
|
|
347
|
+
const negative = text2.startsWith("-");
|
|
348
|
+
const unsigned = negative ? text2.slice(1) : text2;
|
|
302
349
|
const dot = unsigned.indexOf(".");
|
|
303
350
|
const whole = dot === -1 ? unsigned : unsigned.slice(0, dot);
|
|
304
351
|
const frac = dot === -1 ? "" : unsigned.slice(dot + 1, dot + 10);
|
|
@@ -476,9 +523,67 @@ function renderChangeReport(input) {
|
|
|
476
523
|
for (const request of input.metrics.requests) addRequest(total, request);
|
|
477
524
|
lines.push(` ${totalLine(total, input.metrics, input.prices)}`);
|
|
478
525
|
}
|
|
526
|
+
const decisions = decisionSection(input.metrics, input.prices);
|
|
527
|
+
if (decisions.length > 0) {
|
|
528
|
+
lines.push("Decisions:");
|
|
529
|
+
lines.push(` ${DECISION_HEADER}`);
|
|
530
|
+
for (const line of decisions) lines.push(` ${line}`);
|
|
531
|
+
}
|
|
479
532
|
if (input.prices === null) lines.push("prices.yaml is missing");
|
|
480
533
|
return lines.join("\n");
|
|
481
534
|
}
|
|
535
|
+
function groupDecisions(metrics) {
|
|
536
|
+
const groups = /* @__PURE__ */ new Map();
|
|
537
|
+
for (const row of metrics.decisions ?? []) {
|
|
538
|
+
const model = row.model ?? null;
|
|
539
|
+
const key = `${row.workType}\0${row.kind}\0${row.provider}\0${model ?? ""}`;
|
|
540
|
+
let group = groups.get(key);
|
|
541
|
+
if (!group) {
|
|
542
|
+
group = {
|
|
543
|
+
workType: row.workType,
|
|
544
|
+
kind: row.kind,
|
|
545
|
+
provider: row.provider,
|
|
546
|
+
model,
|
|
547
|
+
totals: emptyTotals()
|
|
548
|
+
};
|
|
549
|
+
groups.set(key, group);
|
|
550
|
+
}
|
|
551
|
+
group.totals.requests += 1;
|
|
552
|
+
group.totals.input = addCount(group.totals.input, row.inputTokens);
|
|
553
|
+
group.totals.cacheRead = addCount(group.totals.cacheRead, row.cacheReadTokens);
|
|
554
|
+
group.totals.cacheWrite = addCount(group.totals.cacheWrite, row.cacheWriteTokens);
|
|
555
|
+
group.totals.output = addCount(group.totals.output, row.outputTokens);
|
|
556
|
+
}
|
|
557
|
+
const order = ["route", "classify", "check", "draft", "design", "code", "review"];
|
|
558
|
+
return [...groups.values()].sort((left, right) => {
|
|
559
|
+
const workDelta = order.indexOf(left.workType) - order.indexOf(right.workType);
|
|
560
|
+
if (workDelta !== 0) return workDelta;
|
|
561
|
+
const kindDelta = left.kind.localeCompare(right.kind);
|
|
562
|
+
if (kindDelta !== 0) return kindDelta;
|
|
563
|
+
const providerDelta = left.provider.localeCompare(right.provider);
|
|
564
|
+
if (providerDelta !== 0) return providerDelta;
|
|
565
|
+
return (left.model ?? "").localeCompare(right.model ?? "");
|
|
566
|
+
});
|
|
567
|
+
}
|
|
568
|
+
function decisionSection(metrics, prices) {
|
|
569
|
+
return groupDecisions(metrics).map((group) => {
|
|
570
|
+
const card = prices ? cardFor(prices, group.provider, group.model) : null;
|
|
571
|
+
const cost = prices && card ? costFor(group.totals, card, prices.usd_to_inr) : { usd: "n/a", inr: "n/a" };
|
|
572
|
+
return [
|
|
573
|
+
group.workType,
|
|
574
|
+
group.kind,
|
|
575
|
+
group.provider,
|
|
576
|
+
modelCell(group.model),
|
|
577
|
+
String(group.totals.requests),
|
|
578
|
+
cell(group.totals.input),
|
|
579
|
+
cell(group.totals.cacheRead),
|
|
580
|
+
cell(group.totals.cacheWrite),
|
|
581
|
+
cell(group.totals.output),
|
|
582
|
+
cost.usd,
|
|
583
|
+
cost.inr
|
|
584
|
+
].join(" ");
|
|
585
|
+
});
|
|
586
|
+
}
|
|
482
587
|
function requestUsdNumerator(request, card) {
|
|
483
588
|
const parts = [
|
|
484
589
|
[request.inputTokens, card.input_per_million_usd],
|
|
@@ -535,8 +640,8 @@ function compareChangesByFirstPhase(left, right) {
|
|
|
535
640
|
}
|
|
536
641
|
async function readHandoffCodePoints(projectRoot) {
|
|
537
642
|
try {
|
|
538
|
-
const
|
|
539
|
-
return Array.from(
|
|
643
|
+
const text2 = await readText(join(sddRoot(projectRoot), "handoff.md"));
|
|
644
|
+
return Array.from(text2).length;
|
|
540
645
|
} catch (err) {
|
|
541
646
|
const code = typeof err === "object" && err !== null && "code" in err ? err.code : void 0;
|
|
542
647
|
if (code === "ENOENT") return null;
|
|
@@ -612,13 +717,13 @@ metrics.json is invalid`
|
|
|
612
717
|
import { join as join2 } from "pathe";
|
|
613
718
|
var VERIFY_MARKER = /<!-- sdd:verify stage="([^"]+)" head_sha="(?:none|[0-9a-f]{40}|[0-9a-f]{64})" result_count="(\d+)" -->/gi;
|
|
614
719
|
async function syncChangeHead(projectRoot, config, changeId, opts) {
|
|
615
|
-
const
|
|
616
|
-
if (!
|
|
720
|
+
const headSha2 = opts?.headSha !== void 0 ? opts.headSha : await getHeadSha(projectRoot);
|
|
721
|
+
if (!headSha2) throw new Error("Cannot sync: Git HEAD is unavailable in this project.");
|
|
617
722
|
const ctx = await buildContext(projectRoot, config, changeId);
|
|
618
723
|
let updatedResults = 0;
|
|
619
|
-
for (const
|
|
620
|
-
if (!
|
|
621
|
-
|
|
724
|
+
for (const result2 of Object.values(ctx.meta.verify_results ?? {})) {
|
|
725
|
+
if (!result2.results.length || result2.head_sha === headSha2) continue;
|
|
726
|
+
result2.head_sha = headSha2;
|
|
622
727
|
updatedResults += 1;
|
|
623
728
|
}
|
|
624
729
|
if (updatedResults) await saveChangeMeta(projectRoot, config, ctx.meta);
|
|
@@ -631,7 +736,7 @@ async function syncChangeHead(projectRoot, config, changeId, opts) {
|
|
|
631
736
|
if (markers.length) {
|
|
632
737
|
const updated = report.replace(
|
|
633
738
|
VERIFY_MARKER,
|
|
634
|
-
(_marker, stage, resultCount) => `<!-- sdd:verify stage="${stage}" head_sha="${
|
|
739
|
+
(_marker, stage, resultCount) => `<!-- sdd:verify stage="${stage}" head_sha="${headSha2}" result_count="${resultCount}" -->`
|
|
635
740
|
);
|
|
636
741
|
if (updated !== report) {
|
|
637
742
|
await writeText(reportPath, updated);
|
|
@@ -641,20 +746,144 @@ async function syncChangeHead(projectRoot, config, changeId, opts) {
|
|
|
641
746
|
reportPreserved = true;
|
|
642
747
|
}
|
|
643
748
|
}
|
|
644
|
-
return { changeId, headSha, updatedResults, reportUpdated, reportPreserved };
|
|
749
|
+
return { changeId, headSha: headSha2, updatedResults, reportUpdated, reportPreserved };
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
// src/token-inventory.ts
|
|
753
|
+
import { join as join3 } from "pathe";
|
|
754
|
+
async function readIfExists(path) {
|
|
755
|
+
if (!await pathExists(path)) return null;
|
|
756
|
+
return readText(path);
|
|
757
|
+
}
|
|
758
|
+
async function workflowFileTokens(projectRoot, name) {
|
|
759
|
+
const dir = workflowsDir(projectRoot);
|
|
760
|
+
for (const ext of ["yaml", "yml"]) {
|
|
761
|
+
const text2 = await readIfExists(join3(dir, `${name}.${ext}`));
|
|
762
|
+
if (text2 !== null) return countTokens(text2);
|
|
763
|
+
}
|
|
764
|
+
return null;
|
|
765
|
+
}
|
|
766
|
+
async function templateBody(projectRoot, artifact) {
|
|
767
|
+
const dir = templatesDir(projectRoot);
|
|
768
|
+
const names = artifact.template ? [artifact.template, artifact.template.replace(/^templates\//, ""), `${artifact.id}.md`] : [`${artifact.id}.md`];
|
|
769
|
+
const seen = /* @__PURE__ */ new Set();
|
|
770
|
+
for (const name of names) {
|
|
771
|
+
const path = join3(dir, name);
|
|
772
|
+
if (seen.has(path)) continue;
|
|
773
|
+
seen.add(path);
|
|
774
|
+
const text2 = await readIfExists(path);
|
|
775
|
+
if (text2 !== null) return text2;
|
|
776
|
+
}
|
|
777
|
+
return null;
|
|
778
|
+
}
|
|
779
|
+
function pushArtifactLines(lines, stageId, path, label) {
|
|
780
|
+
lines.push(` ${stageId} ${path} ${label}`);
|
|
781
|
+
const tokens = Number(label);
|
|
782
|
+
return Number.isInteger(tokens) && String(tokens) === label ? tokens : 0;
|
|
783
|
+
}
|
|
784
|
+
async function workflowLines(projectRoot, name) {
|
|
785
|
+
let workflow;
|
|
786
|
+
try {
|
|
787
|
+
workflow = await loadWorkflow(projectRoot, name);
|
|
788
|
+
} catch {
|
|
789
|
+
return [`${name} unreadable`];
|
|
790
|
+
}
|
|
791
|
+
const fileTokens = await workflowFileTokens(projectRoot, name);
|
|
792
|
+
const lines = [`${name} workflow ${fileTokens === null ? "missing" : fileTokens}`];
|
|
793
|
+
let total = 0;
|
|
794
|
+
let any = false;
|
|
795
|
+
for (const stage of workflow.stages) {
|
|
796
|
+
for (const artifact of stage.artifacts) {
|
|
797
|
+
any = true;
|
|
798
|
+
if (artifact.path.endsWith("/")) {
|
|
799
|
+
total += pushArtifactLines(lines, stage.id, artifact.path, "directory");
|
|
800
|
+
continue;
|
|
801
|
+
}
|
|
802
|
+
const body = await templateBody(projectRoot, artifact);
|
|
803
|
+
total += pushArtifactLines(
|
|
804
|
+
lines,
|
|
805
|
+
stage.id,
|
|
806
|
+
artifact.path,
|
|
807
|
+
body === null ? "missing" : String(countTokens(body))
|
|
808
|
+
);
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
lines.push(any ? ` artifacts ${total}` : " (no artifacts)");
|
|
812
|
+
return lines;
|
|
813
|
+
}
|
|
814
|
+
async function changeLines(projectRoot, config, id) {
|
|
815
|
+
const dir = changePath(projectRoot, config, id);
|
|
816
|
+
let workflowName = "";
|
|
817
|
+
let workflow;
|
|
818
|
+
try {
|
|
819
|
+
const meta = ChangeMetaSchema.parse(await readYaml(join3(dir, "meta.yaml")));
|
|
820
|
+
workflowName = meta.workflow;
|
|
821
|
+
workflow = await loadWorkflow(projectRoot, meta.workflow);
|
|
822
|
+
} catch {
|
|
823
|
+
return [`Change ${id} unreadable`];
|
|
824
|
+
}
|
|
825
|
+
const lines = [`Change ${id} workflow ${workflowName}`];
|
|
826
|
+
let total = 0;
|
|
827
|
+
let any = false;
|
|
828
|
+
for (const stage of workflow.stages) {
|
|
829
|
+
for (const artifact of stage.artifacts) {
|
|
830
|
+
any = true;
|
|
831
|
+
if (artifact.path.endsWith("/")) {
|
|
832
|
+
const folder = join3(dir, artifact.path);
|
|
833
|
+
const names = (await listFiles(folder)).sort();
|
|
834
|
+
if (names.length === 0) {
|
|
835
|
+
total += pushArtifactLines(lines, stage.id, artifact.path, "directory");
|
|
836
|
+
continue;
|
|
837
|
+
}
|
|
838
|
+
for (const name of names) {
|
|
839
|
+
const text3 = await readText(join3(folder, name));
|
|
840
|
+
total += pushArtifactLines(
|
|
841
|
+
lines,
|
|
842
|
+
stage.id,
|
|
843
|
+
`${artifact.path}${name}`,
|
|
844
|
+
String(countTokens(text3))
|
|
845
|
+
);
|
|
846
|
+
}
|
|
847
|
+
continue;
|
|
848
|
+
}
|
|
849
|
+
const text2 = await readIfExists(join3(dir, artifact.path));
|
|
850
|
+
total += pushArtifactLines(
|
|
851
|
+
lines,
|
|
852
|
+
stage.id,
|
|
853
|
+
artifact.path,
|
|
854
|
+
text2 === null ? "not written" : String(countTokens(text2))
|
|
855
|
+
);
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
lines.push(any ? ` artifacts ${total}` : " (no artifacts)");
|
|
859
|
+
return lines;
|
|
860
|
+
}
|
|
861
|
+
async function formatTokenInventory(projectRoot, config) {
|
|
862
|
+
const lines = ["Tokens (cl100k):"];
|
|
863
|
+
const names = await listWorkflowNames(projectRoot);
|
|
864
|
+
if (names.length === 0) lines.push("No workflows.");
|
|
865
|
+
for (const name of names) {
|
|
866
|
+
lines.push(...await workflowLines(projectRoot, name));
|
|
867
|
+
}
|
|
868
|
+
const active = await getActiveChangeId(projectRoot, config);
|
|
869
|
+
if (active) {
|
|
870
|
+
lines.push("");
|
|
871
|
+
lines.push(...await changeLines(projectRoot, config, active));
|
|
872
|
+
}
|
|
873
|
+
return lines.join("\n");
|
|
645
874
|
}
|
|
646
875
|
|
|
647
876
|
// src/init.ts
|
|
648
877
|
import { spawn } from "child_process";
|
|
649
878
|
import { lstat } from "fs/promises";
|
|
650
|
-
import { join as
|
|
879
|
+
import { join as join4 } from "pathe";
|
|
651
880
|
async function initProject(opts) {
|
|
652
881
|
const { projectRoot, force = false } = opts;
|
|
653
882
|
const created = [];
|
|
654
883
|
const kept = [];
|
|
655
884
|
const warnings = [];
|
|
656
885
|
const root = sddRoot(projectRoot);
|
|
657
|
-
const configFile =
|
|
886
|
+
const configFile = join4(root, "config.yaml");
|
|
658
887
|
if (await pathExists(configFile) && !force) {
|
|
659
888
|
await repairRequiredIgnores(projectRoot);
|
|
660
889
|
return { created, config: await loadConfig(projectRoot), kept, warnings };
|
|
@@ -670,12 +899,12 @@ async function initProject(opts) {
|
|
|
670
899
|
const config = await pathExists(configFile) ? mergeConfigDefaults(await readYaml(configFile)) : defaultConfig();
|
|
671
900
|
if (opts.layoutRoot !== void 0) {
|
|
672
901
|
const layoutRoot = validateLayoutRoot(opts.layoutRoot);
|
|
673
|
-
config.memory_path =
|
|
674
|
-
config.changes_path =
|
|
675
|
-
config.domains_path =
|
|
676
|
-
config.archive_path =
|
|
902
|
+
config.memory_path = join4(layoutRoot, "memory");
|
|
903
|
+
config.changes_path = join4(layoutRoot, "changes");
|
|
904
|
+
config.domains_path = join4(layoutRoot, "domains");
|
|
905
|
+
config.archive_path = join4(layoutRoot, "archive");
|
|
677
906
|
}
|
|
678
|
-
const manifestPath =
|
|
907
|
+
const manifestPath = join4(root, ".generated.json");
|
|
679
908
|
const manifestExisted = await pathExists(manifestPath);
|
|
680
909
|
const manifest = await loadGeneratedManifest(manifestPath);
|
|
681
910
|
if (opts.agents) {
|
|
@@ -683,11 +912,11 @@ async function initProject(opts) {
|
|
|
683
912
|
}
|
|
684
913
|
await ensureDir(root);
|
|
685
914
|
await saveConfig(projectRoot, config);
|
|
686
|
-
created.push(
|
|
915
|
+
created.push(join4(root, "config.yaml"));
|
|
687
916
|
const wfDest = workflowsDir(projectRoot);
|
|
688
917
|
const wfSrc = defaultWorkflowsDir();
|
|
689
918
|
if (await pathExists(wfSrc)) {
|
|
690
|
-
const
|
|
919
|
+
const result2 = await reconcileGeneratedTree(
|
|
691
920
|
projectRoot,
|
|
692
921
|
wfSrc,
|
|
693
922
|
wfDest,
|
|
@@ -695,7 +924,7 @@ async function initProject(opts) {
|
|
|
695
924
|
manifest,
|
|
696
925
|
force
|
|
697
926
|
);
|
|
698
|
-
kept.push(...
|
|
927
|
+
kept.push(...result2.kept);
|
|
699
928
|
created.push(wfDest);
|
|
700
929
|
} else {
|
|
701
930
|
await ensureDir(wfDest);
|
|
@@ -703,7 +932,7 @@ async function initProject(opts) {
|
|
|
703
932
|
const tDest = templatesDir(projectRoot);
|
|
704
933
|
const tSrc = defaultTemplatesDir();
|
|
705
934
|
if (await pathExists(tSrc)) {
|
|
706
|
-
const
|
|
935
|
+
const result2 = await reconcileGeneratedTree(
|
|
707
936
|
projectRoot,
|
|
708
937
|
tSrc,
|
|
709
938
|
tDest,
|
|
@@ -711,7 +940,7 @@ async function initProject(opts) {
|
|
|
711
940
|
manifest,
|
|
712
941
|
force
|
|
713
942
|
);
|
|
714
|
-
kept.push(...
|
|
943
|
+
kept.push(...result2.kept);
|
|
715
944
|
created.push(tDest);
|
|
716
945
|
} else {
|
|
717
946
|
await ensureDir(tDest);
|
|
@@ -725,13 +954,13 @@ async function initProject(opts) {
|
|
|
725
954
|
if (wrote) created.push(mcpDest);
|
|
726
955
|
}
|
|
727
956
|
const pricesSrc = defaultPricesYamlPath();
|
|
728
|
-
const pricesDest =
|
|
957
|
+
const pricesDest = join4(root, "prices.yaml");
|
|
729
958
|
if (await pathExists(pricesSrc)) {
|
|
730
959
|
const wrote = await writeTextIfMissing(pricesDest, await readText(pricesSrc));
|
|
731
960
|
if (wrote) created.push(pricesDest);
|
|
732
961
|
}
|
|
733
962
|
const searchSrc = defaultSearchYamlPath();
|
|
734
|
-
const searchDest =
|
|
963
|
+
const searchDest = join4(root, "search.yaml");
|
|
735
964
|
if (await pathExists(searchSrc)) {
|
|
736
965
|
const wrote = await writeTextIfMissing(searchDest, await readText(searchSrc));
|
|
737
966
|
if (wrote) created.push(searchDest);
|
|
@@ -749,7 +978,7 @@ async function initProject(opts) {
|
|
|
749
978
|
await copyDirSkipExisting(mSrc, mDest);
|
|
750
979
|
} else {
|
|
751
980
|
await writeTextIfMissing(
|
|
752
|
-
|
|
981
|
+
join4(mDest, "index.md"),
|
|
753
982
|
`# Documentation map
|
|
754
983
|
|
|
755
984
|
Parent page for **stable** project docs. Start here, then open only what you need.
|
|
@@ -765,11 +994,11 @@ Active work: \`.sdd/active-context.md\` and the configured changes folder.
|
|
|
765
994
|
`
|
|
766
995
|
);
|
|
767
996
|
await writeTextIfMissing(
|
|
768
|
-
|
|
997
|
+
join4(mDest, "product.md"),
|
|
769
998
|
"# Product\n\n<!-- What are you building? -->\n"
|
|
770
999
|
);
|
|
771
1000
|
await writeTextIfMissing(
|
|
772
|
-
|
|
1001
|
+
join4(mDest, "constitution.md"),
|
|
773
1002
|
`# Constitution
|
|
774
1003
|
|
|
775
1004
|
<!-- Non-negotiables agents must not violate. Broader style lives in conventions.md. -->
|
|
@@ -796,21 +1025,21 @@ Active work: \`.sdd/active-context.md\` and the configured changes folder.
|
|
|
796
1025
|
`
|
|
797
1026
|
);
|
|
798
1027
|
await writeTextIfMissing(
|
|
799
|
-
|
|
1028
|
+
join4(mDest, "architecture.md"),
|
|
800
1029
|
"# Architecture\n\n<!-- High-level system shape -->\n"
|
|
801
1030
|
);
|
|
802
1031
|
await writeTextIfMissing(
|
|
803
|
-
|
|
1032
|
+
join4(mDest, "conventions.md"),
|
|
804
1033
|
"# Conventions\n\n<!-- Coding and process conventions -->\n"
|
|
805
1034
|
);
|
|
806
1035
|
}
|
|
807
|
-
if (!await pathExists(
|
|
808
|
-
const fromDefault =
|
|
1036
|
+
if (!await pathExists(join4(mDest, "constitution.md"))) {
|
|
1037
|
+
const fromDefault = join4(mSrc, "constitution.md");
|
|
809
1038
|
if (await pathExists(fromDefault)) {
|
|
810
1039
|
await copyDirSkipExisting(mSrc, mDest);
|
|
811
1040
|
} else {
|
|
812
1041
|
await writeTextIfMissing(
|
|
813
|
-
|
|
1042
|
+
join4(mDest, "constitution.md"),
|
|
814
1043
|
`# Constitution
|
|
815
1044
|
|
|
816
1045
|
<!-- Non-negotiables agents must not violate. Broader style lives in conventions.md. -->
|
|
@@ -843,10 +1072,10 @@ Active work: \`.sdd/active-context.md\` and the configured changes folder.
|
|
|
843
1072
|
await ensureDir(domainsDir(projectRoot, config));
|
|
844
1073
|
created.push(changesDir(projectRoot, config));
|
|
845
1074
|
created.push(domainsDir(projectRoot, config));
|
|
846
|
-
await writeTextIfMissing(
|
|
847
|
-
await writeTextIfMissing(
|
|
848
|
-
const sddIgnore =
|
|
849
|
-
const changesIgnore =
|
|
1075
|
+
await writeTextIfMissing(join4(changesDir(projectRoot, config), ".gitkeep"), "");
|
|
1076
|
+
await writeTextIfMissing(join4(domainsDir(projectRoot, config), ".gitkeep"), "");
|
|
1077
|
+
const sddIgnore = join4(root, ".gitignore");
|
|
1078
|
+
const changesIgnore = join4(changesDir(projectRoot, config), ".gitignore");
|
|
850
1079
|
if ((await ensureIgnoreEntries(sddIgnore, SDD_IGNORE_LINES)).created) {
|
|
851
1080
|
created.push(sddIgnore);
|
|
852
1081
|
}
|
|
@@ -867,7 +1096,7 @@ Active work: \`.sdd/active-context.md\` and the configured changes folder.
|
|
|
867
1096
|
);
|
|
868
1097
|
}
|
|
869
1098
|
}
|
|
870
|
-
const sddReadme =
|
|
1099
|
+
const sddReadme = join4(root, "README.md");
|
|
871
1100
|
const readmeState = await pathExists(sddReadme) ? generatedState(await readText(sddReadme)) : null;
|
|
872
1101
|
if (readmeState === "edited" || readmeState === "not-generated") {
|
|
873
1102
|
kept.push({ path: ".sdd/README.md", reason: readmeState });
|
|
@@ -901,8 +1130,8 @@ var SDD_IGNORE_LINES = [
|
|
|
901
1130
|
async function repairRequiredIgnores(projectRoot) {
|
|
902
1131
|
const config = await loadConfig(projectRoot);
|
|
903
1132
|
const root = sddRoot(projectRoot);
|
|
904
|
-
const sdd = await ensureIgnoreEntries(
|
|
905
|
-
const changes = await ensureIgnoreEntries(
|
|
1133
|
+
const sdd = await ensureIgnoreEntries(join4(root, ".gitignore"), SDD_IGNORE_LINES);
|
|
1134
|
+
const changes = await ensureIgnoreEntries(join4(changesDir(projectRoot, config), ".gitignore"), [
|
|
906
1135
|
".active"
|
|
907
1136
|
]);
|
|
908
1137
|
return sdd.added || changes.added;
|
|
@@ -957,7 +1186,7 @@ async function listRelativeFiles(directory, prefix = "") {
|
|
|
957
1186
|
);
|
|
958
1187
|
const nested = await Promise.all(
|
|
959
1188
|
(await listDirs(directory)).map(
|
|
960
|
-
(name) => listRelativeFiles(
|
|
1189
|
+
(name) => listRelativeFiles(join4(directory, name), [prefix, name].filter(Boolean).join("/"))
|
|
961
1190
|
)
|
|
962
1191
|
);
|
|
963
1192
|
return [...files, ...nested.flat()];
|
|
@@ -965,7 +1194,7 @@ async function listRelativeFiles(directory, prefix = "") {
|
|
|
965
1194
|
|
|
966
1195
|
// src/verify.ts
|
|
967
1196
|
import { spawn as spawn2 } from "child_process";
|
|
968
|
-
import { join as
|
|
1197
|
+
import { join as join5 } from "pathe";
|
|
969
1198
|
function runCommand(command, cwd) {
|
|
970
1199
|
return new Promise((resolve) => {
|
|
971
1200
|
const child = spawn2(command, {
|
|
@@ -1013,7 +1242,7 @@ var STATUS_LABEL = {
|
|
|
1013
1242
|
fail: "FAIL",
|
|
1014
1243
|
not_run: "NOT RUN"
|
|
1015
1244
|
};
|
|
1016
|
-
function buildSummaryLines(title, stageId,
|
|
1245
|
+
function buildSummaryLines(title, stageId, headSha2, status, reason, results, checklist) {
|
|
1017
1246
|
const lines = [
|
|
1018
1247
|
`<!-- sdd:verify-report:start -->`,
|
|
1019
1248
|
``,
|
|
@@ -1021,7 +1250,7 @@ function buildSummaryLines(title, stageId, headSha, status, reason, results, che
|
|
|
1021
1250
|
``,
|
|
1022
1251
|
`> Change: ${title}`,
|
|
1023
1252
|
`> Stage: ${stageId}`,
|
|
1024
|
-
`<!-- sdd:verify stage="${stageId}" head_sha="${
|
|
1253
|
+
`<!-- sdd:verify stage="${stageId}" head_sha="${headSha2 ?? "none"}" result_count="${results.length}" -->`,
|
|
1025
1254
|
`> At: ${nowIso()}`,
|
|
1026
1255
|
`> Overall: ${STATUS_LABEL[status]}${reason ? ` \u2014 ${reason}` : ""}`,
|
|
1027
1256
|
``,
|
|
@@ -1072,23 +1301,23 @@ async function runLocalVerify(projectRoot, config, changeId, opts) {
|
|
|
1072
1301
|
ok: false
|
|
1073
1302
|
};
|
|
1074
1303
|
}
|
|
1075
|
-
const
|
|
1304
|
+
const headSha2 = opts?.headSha !== void 0 ? opts.headSha : await getHeadSha(projectRoot);
|
|
1076
1305
|
const anyRecordedSha = Object.entries(ctx.meta.verify_results ?? {}).find(
|
|
1077
1306
|
([, recorded]) => recorded.head_sha
|
|
1078
1307
|
);
|
|
1079
|
-
if (anyRecordedSha && !
|
|
1308
|
+
if (anyRecordedSha && !headSha2) {
|
|
1080
1309
|
throw new Error(
|
|
1081
|
-
`Cannot validate recorded SHA in ${
|
|
1310
|
+
`Cannot validate recorded SHA in ${join5(ctx.path, "meta.yaml")} at verify_results.${anyRecordedSha[0]}.head_sha because Git HEAD is unavailable.`
|
|
1082
1311
|
);
|
|
1083
1312
|
}
|
|
1084
1313
|
for (const [resultStage, recorded] of Object.entries(ctx.meta.verify_results ?? {})) {
|
|
1085
|
-
if (recorded.head_sha &&
|
|
1314
|
+
if (recorded.head_sha && headSha2 && recorded.head_sha !== headSha2) {
|
|
1086
1315
|
throw new Error(
|
|
1087
|
-
`Stale SHA in ${
|
|
1316
|
+
`Stale SHA in ${join5(ctx.path, "meta.yaml")} at verify_results.${resultStage}.head_sha: recorded ${recorded.head_sha}, current HEAD ${headSha2}. Run sdd sync ${changeId}.`
|
|
1088
1317
|
);
|
|
1089
1318
|
}
|
|
1090
1319
|
}
|
|
1091
|
-
const summaryPath =
|
|
1320
|
+
const summaryPath = join5(ctx.path, "local-test-results.md");
|
|
1092
1321
|
if (await pathExists(summaryPath)) {
|
|
1093
1322
|
const existing = await readText(summaryPath);
|
|
1094
1323
|
const markerPattern = /<!-- sdd:verify stage="([^"]+)" head_sha="(none|[0-9a-f]{40}|[0-9a-f]{64})" result_count="(\d+)" -->/gi;
|
|
@@ -1099,17 +1328,17 @@ async function runLocalVerify(projectRoot, config, changeId, opts) {
|
|
|
1099
1328
|
const reportResult = ctx.meta.verify_results?.[reportStage];
|
|
1100
1329
|
if (!reportResult) {
|
|
1101
1330
|
throw new Error(
|
|
1102
|
-
`Missing verification metadata in ${
|
|
1331
|
+
`Missing verification metadata in ${join5(ctx.path, "meta.yaml")} for ${summaryPath}:${line} (${reportStage}).`
|
|
1103
1332
|
);
|
|
1104
1333
|
}
|
|
1105
|
-
if (reportSha !== "none" && !
|
|
1334
|
+
if (reportSha !== "none" && !headSha2) {
|
|
1106
1335
|
throw new Error(
|
|
1107
1336
|
`Cannot validate SHA in ${summaryPath}:${line} because Git HEAD is unavailable.`
|
|
1108
1337
|
);
|
|
1109
1338
|
}
|
|
1110
|
-
if (
|
|
1339
|
+
if (headSha2 && reportSha !== "none" && reportSha !== headSha2) {
|
|
1111
1340
|
throw new Error(
|
|
1112
|
-
`Stale SHA in ${summaryPath}:${line}: recorded ${reportSha}, current HEAD ${
|
|
1341
|
+
`Stale SHA in ${summaryPath}:${line}: recorded ${reportSha}, current HEAD ${headSha2}. Run sdd sync ${changeId}.`
|
|
1113
1342
|
);
|
|
1114
1343
|
}
|
|
1115
1344
|
if (reportResult && Number(marker[3]) !== reportResult.results.length) {
|
|
@@ -1119,13 +1348,13 @@ async function runLocalVerify(projectRoot, config, changeId, opts) {
|
|
|
1119
1348
|
}
|
|
1120
1349
|
if (reportResult?.head_sha && reportSha !== reportResult.head_sha) {
|
|
1121
1350
|
throw new Error(
|
|
1122
|
-
`SHA mismatch between ${
|
|
1351
|
+
`SHA mismatch between ${join5(ctx.path, "meta.yaml")} and ${summaryPath}:${line}.`
|
|
1123
1352
|
);
|
|
1124
1353
|
}
|
|
1125
1354
|
}
|
|
1126
1355
|
}
|
|
1127
1356
|
const evidenceDirRaw = stage.verify?.evidence_dir?.trim();
|
|
1128
|
-
const evidenceDir = evidenceDirRaw && evidenceDirRaw.length > 0 ?
|
|
1357
|
+
const evidenceDir = evidenceDirRaw && evidenceDirRaw.length > 0 ? join5(ctx.path, evidenceDirRaw) : null;
|
|
1129
1358
|
if (evidenceDir) {
|
|
1130
1359
|
await ensureDir(evidenceDir);
|
|
1131
1360
|
}
|
|
@@ -1142,7 +1371,7 @@ async function runLocalVerify(projectRoot, config, changeId, opts) {
|
|
|
1142
1371
|
output: output.slice(0, 5e4)
|
|
1143
1372
|
});
|
|
1144
1373
|
if (evidenceDir) {
|
|
1145
|
-
const logPath =
|
|
1374
|
+
const logPath = join5(evidenceDir, `${cmd.name.replace(/\s+/g, "-")}.log`);
|
|
1146
1375
|
await writeText(logPath, `# ${cmd.name}
|
|
1147
1376
|
$ ${cmd.run}
|
|
1148
1377
|
exit: ${exitCode}
|
|
@@ -1164,12 +1393,12 @@ ${output}`);
|
|
|
1164
1393
|
status = failed.length ? "fail" : "pass";
|
|
1165
1394
|
reason = failed.length ? `failed: ${failed.join(", ")}` : void 0;
|
|
1166
1395
|
}
|
|
1167
|
-
const
|
|
1396
|
+
const ok8 = status === "pass";
|
|
1168
1397
|
const checklist = stage.gate?.checklist ?? [];
|
|
1169
1398
|
const lines = buildSummaryLines(
|
|
1170
1399
|
ctx.meta.title,
|
|
1171
1400
|
stage.id,
|
|
1172
|
-
results.length ?
|
|
1401
|
+
results.length ? headSha2 : null,
|
|
1173
1402
|
status,
|
|
1174
1403
|
reason,
|
|
1175
1404
|
results,
|
|
@@ -1191,7 +1420,7 @@ ${output}`);
|
|
|
1191
1420
|
``,
|
|
1192
1421
|
`> Overall: ${STATUS_LABEL[status]}${reason ? ` \u2014 ${reason}` : ""}`,
|
|
1193
1422
|
`> Stage: ${stage.id}`,
|
|
1194
|
-
`<!-- sdd:verify stage="${stage.id}" head_sha="${results.length ?
|
|
1423
|
+
`<!-- sdd:verify stage="${stage.id}" head_sha="${results.length ? headSha2 ?? "none" : "none"}" result_count="${results.length}" -->`,
|
|
1195
1424
|
``
|
|
1196
1425
|
];
|
|
1197
1426
|
if (results.length === 0) {
|
|
@@ -1207,7 +1436,7 @@ ${output}`);
|
|
|
1207
1436
|
}
|
|
1208
1437
|
if (evidenceDir) {
|
|
1209
1438
|
const stamp = nowIso();
|
|
1210
|
-
await writeText(
|
|
1439
|
+
await writeText(join5(evidenceDir, `run-${stamp.replace(/[:.]/g, "-")}.md`), summaryBody);
|
|
1211
1440
|
}
|
|
1212
1441
|
} else {
|
|
1213
1442
|
await writeText(summaryPath, summaryBody);
|
|
@@ -1215,9 +1444,9 @@ ${output}`);
|
|
|
1215
1444
|
ctx.meta.verify_results = {
|
|
1216
1445
|
...ctx.meta.verify_results,
|
|
1217
1446
|
[stage.id]: {
|
|
1218
|
-
ok:
|
|
1447
|
+
ok: ok8,
|
|
1219
1448
|
status,
|
|
1220
|
-
...results.length &&
|
|
1449
|
+
...results.length && headSha2 ? { head_sha: headSha2 } : {},
|
|
1221
1450
|
at: nowIso(),
|
|
1222
1451
|
results: results.map((r) => ({ name: r.name, exitCode: r.exitCode }))
|
|
1223
1452
|
}
|
|
@@ -1231,17 +1460,17 @@ ${output}`);
|
|
|
1231
1460
|
checklist,
|
|
1232
1461
|
evidencePath: evidenceDir,
|
|
1233
1462
|
resultsPath: summaryPath,
|
|
1234
|
-
ok:
|
|
1463
|
+
ok: ok8
|
|
1235
1464
|
};
|
|
1236
1465
|
}
|
|
1237
1466
|
|
|
1238
1467
|
// src/code-context/index.ts
|
|
1239
1468
|
import { stat } from "fs/promises";
|
|
1240
|
-
import { extname as extname3, isAbsolute as isAbsolute2, join as
|
|
1469
|
+
import { extname as extname3, isAbsolute as isAbsolute2, join as join7, relative as relative2 } from "pathe";
|
|
1241
1470
|
|
|
1242
1471
|
// src/code-context/focus.ts
|
|
1243
1472
|
import { promises as fs } from "fs";
|
|
1244
|
-
import { isAbsolute, join as
|
|
1473
|
+
import { isAbsolute, join as join6, normalize, relative } from "pathe";
|
|
1245
1474
|
var KEYWORD_STOPWORDS = new Set(
|
|
1246
1475
|
[
|
|
1247
1476
|
"a",
|
|
@@ -1304,8 +1533,8 @@ var KEYWORD_STOPWORDS = new Set(
|
|
|
1304
1533
|
);
|
|
1305
1534
|
var SOURCE_ROOTS = ["packages", "src", "lib"];
|
|
1306
1535
|
var PATH_LIKE_RE = /(?:packages|src|lib)\/[\w./@+-]+\.\w{1,10}|`([^`]+\.\w{1,10})`|([\w./@+-]+\.(?:ts|tsx|js|jsx|mts|cts|mjs|cjs))/g;
|
|
1307
|
-
function tokenizeKeywords(
|
|
1308
|
-
const raw =
|
|
1536
|
+
function tokenizeKeywords(text2) {
|
|
1537
|
+
const raw = text2.toLowerCase().replace(/[^a-z0-9_./-]+/g, " ").split(/\s+/).filter(Boolean);
|
|
1309
1538
|
const out = [];
|
|
1310
1539
|
const seen = /* @__PURE__ */ new Set();
|
|
1311
1540
|
for (const t of raw) {
|
|
@@ -1318,12 +1547,12 @@ function tokenizeKeywords(text) {
|
|
|
1318
1547
|
}
|
|
1319
1548
|
return out;
|
|
1320
1549
|
}
|
|
1321
|
-
function extractPathLikeTokens(
|
|
1550
|
+
function extractPathLikeTokens(text2) {
|
|
1322
1551
|
const found = [];
|
|
1323
1552
|
const seen = /* @__PURE__ */ new Set();
|
|
1324
1553
|
let m;
|
|
1325
1554
|
const re = new RegExp(PATH_LIKE_RE.source, "g");
|
|
1326
|
-
while ((m = re.exec(
|
|
1555
|
+
while ((m = re.exec(text2)) !== null) {
|
|
1327
1556
|
const p = (m[1] || m[2] || m[0]).replace(/`/g, "");
|
|
1328
1557
|
if (!p || seen.has(p)) continue;
|
|
1329
1558
|
seen.add(p);
|
|
@@ -1332,12 +1561,12 @@ function extractPathLikeTokens(text) {
|
|
|
1332
1561
|
return found;
|
|
1333
1562
|
}
|
|
1334
1563
|
function toRepoRelative(projectRoot, p) {
|
|
1335
|
-
const abs = isAbsolute(p) ? p :
|
|
1564
|
+
const abs = isAbsolute(p) ? p : join6(projectRoot, p);
|
|
1336
1565
|
const rel = relative(projectRoot, abs);
|
|
1337
1566
|
return normalize(rel).replace(/\\/g, "/");
|
|
1338
1567
|
}
|
|
1339
1568
|
function toAbsolute(projectRoot, p) {
|
|
1340
|
-
return isAbsolute(p) ? p :
|
|
1569
|
+
return isAbsolute(p) ? p : join6(projectRoot, p);
|
|
1341
1570
|
}
|
|
1342
1571
|
async function walkFiles(absDir, projectRoot, maxFiles, out, gaps) {
|
|
1343
1572
|
if (out.length >= maxFiles) return;
|
|
@@ -1350,7 +1579,7 @@ async function walkFiles(absDir, projectRoot, maxFiles, out, gaps) {
|
|
|
1350
1579
|
}
|
|
1351
1580
|
for (const entry of entries) {
|
|
1352
1581
|
if (out.length >= maxFiles) return;
|
|
1353
|
-
const abs =
|
|
1582
|
+
const abs = join6(absDir, entry.name);
|
|
1354
1583
|
const rel = toRepoRelative(projectRoot, abs);
|
|
1355
1584
|
const kind = classifyPath(rel);
|
|
1356
1585
|
if (kind === "secret" || kind === "ignored") continue;
|
|
@@ -1415,8 +1644,8 @@ async function collectUnderPaths(projectRoot, paths, maxWalk, gaps) {
|
|
|
1415
1644
|
async function readBounded(absPath, maxChars) {
|
|
1416
1645
|
if (!await pathExists(absPath)) return null;
|
|
1417
1646
|
try {
|
|
1418
|
-
const
|
|
1419
|
-
return
|
|
1647
|
+
const text2 = await readText(absPath);
|
|
1648
|
+
return text2.slice(0, maxChars);
|
|
1420
1649
|
} catch {
|
|
1421
1650
|
return null;
|
|
1422
1651
|
}
|
|
@@ -1444,7 +1673,7 @@ async function resolveChangeMeta(projectRoot, changeId, gaps) {
|
|
|
1444
1673
|
return null;
|
|
1445
1674
|
}
|
|
1446
1675
|
const cpath = changePath(projectRoot, config, id);
|
|
1447
|
-
const metaPath =
|
|
1676
|
+
const metaPath = join6(cpath, "meta.yaml");
|
|
1448
1677
|
if (!await pathExists(metaPath)) {
|
|
1449
1678
|
if (!await pathExists(cpath)) {
|
|
1450
1679
|
gaps.push({
|
|
@@ -1477,7 +1706,7 @@ async function searchByKeywords(projectRoot, keywords, symbols, maxFiles) {
|
|
|
1477
1706
|
const terms = [...keywords, ...symbols.map((s) => s.toLowerCase())];
|
|
1478
1707
|
if (!terms.length) return out;
|
|
1479
1708
|
for (const root of SOURCE_ROOTS) {
|
|
1480
|
-
const abs =
|
|
1709
|
+
const abs = join6(projectRoot, root);
|
|
1481
1710
|
if (!await pathExists(abs)) continue;
|
|
1482
1711
|
const files = [];
|
|
1483
1712
|
await walkFiles(abs, projectRoot, maxFiles * 4, files, []);
|
|
@@ -1491,8 +1720,8 @@ async function searchByKeywords(projectRoot, keywords, symbols, maxFiles) {
|
|
|
1491
1720
|
}
|
|
1492
1721
|
if (symbols.length) {
|
|
1493
1722
|
try {
|
|
1494
|
-
const
|
|
1495
|
-
if (symbols.some((s) =>
|
|
1723
|
+
const text2 = await readText(join6(projectRoot, rel));
|
|
1724
|
+
if (symbols.some((s) => text2.includes(s))) {
|
|
1496
1725
|
out.push(rel);
|
|
1497
1726
|
}
|
|
1498
1727
|
} catch {
|
|
@@ -1527,7 +1756,7 @@ async function resolveFocus(request, options) {
|
|
|
1527
1756
|
}
|
|
1528
1757
|
notes.push(`Change signals from: ${change.id}`);
|
|
1529
1758
|
for (const artifact of ["tasks.md", "feature.md", "stories.md", "lld.md"]) {
|
|
1530
|
-
const body = await readBounded(
|
|
1759
|
+
const body = await readBounded(join6(change.path, artifact), 8e3);
|
|
1531
1760
|
if (!body) continue;
|
|
1532
1761
|
if (!request.paths?.length) {
|
|
1533
1762
|
for (const p of extractPathLikeTokens(body)) {
|
|
@@ -1644,10 +1873,10 @@ function formatMarkdown(input) {
|
|
|
1644
1873
|
`;
|
|
1645
1874
|
return { markdown: packed ? frame + packed : frame, truncated: input.summary.truncated };
|
|
1646
1875
|
}
|
|
1647
|
-
function formatJsonSummary(summary, gaps, slices,
|
|
1876
|
+
function formatJsonSummary(summary, gaps, slices, ok8) {
|
|
1648
1877
|
return JSON.stringify(
|
|
1649
1878
|
{
|
|
1650
|
-
ok:
|
|
1879
|
+
ok: ok8,
|
|
1651
1880
|
summary,
|
|
1652
1881
|
gaps,
|
|
1653
1882
|
slices: slices.map((s) => ({
|
|
@@ -1938,7 +2167,7 @@ function hardFailure(gaps, message, summary) {
|
|
|
1938
2167
|
};
|
|
1939
2168
|
}
|
|
1940
2169
|
function toRepoPath(projectRoot, input) {
|
|
1941
|
-
const abs = isAbsolute2(input) ? input :
|
|
2170
|
+
const abs = isAbsolute2(input) ? input : join7(projectRoot, input);
|
|
1942
2171
|
let rel = relative2(projectRoot, abs).replaceAll("\\", "/");
|
|
1943
2172
|
while (rel.startsWith("./")) rel = rel.slice(2);
|
|
1944
2173
|
return rel;
|
|
@@ -1979,7 +2208,7 @@ async function generateCodeContext(request, deps) {
|
|
|
1979
2208
|
let changeTitle = plan.changeId;
|
|
1980
2209
|
let stage;
|
|
1981
2210
|
if (request.changeId) {
|
|
1982
|
-
const mp =
|
|
2211
|
+
const mp = join7(changePath(projectRoot, config, request.changeId), "meta.yaml");
|
|
1983
2212
|
if (await pathExists(mp)) {
|
|
1984
2213
|
const meta = await readYaml(mp);
|
|
1985
2214
|
changeTitle = meta.title ?? request.changeId;
|
|
@@ -1996,7 +2225,7 @@ async function generateCodeContext(request, deps) {
|
|
|
1996
2225
|
let query = request.query?.trim() ? request.query : "";
|
|
1997
2226
|
if (!query && request.symbols?.length) query = request.symbols.join(" ");
|
|
1998
2227
|
if (!query && changeTitle && request.changeId) query = changeTitle;
|
|
1999
|
-
const
|
|
2228
|
+
const activeChange2 = await resolveActiveDirectory(projectRoot, config);
|
|
2000
2229
|
const cacheRequest = {
|
|
2001
2230
|
projectRoot,
|
|
2002
2231
|
query: query.trim() ? query : "",
|
|
@@ -2005,7 +2234,7 @@ async function generateCodeContext(request, deps) {
|
|
|
2005
2234
|
k,
|
|
2006
2235
|
budget,
|
|
2007
2236
|
embeddings: searchConfig.embeddings,
|
|
2008
|
-
activeChange,
|
|
2237
|
+
activeChange: activeChange2,
|
|
2009
2238
|
filterPaths: request.paths ?? [],
|
|
2010
2239
|
filterSymbols: request.symbols ?? [],
|
|
2011
2240
|
scopes: ["code"]
|
|
@@ -2041,7 +2270,7 @@ async function generateCodeContext(request, deps) {
|
|
|
2041
2270
|
const rel = toRepoPath(projectRoot, input);
|
|
2042
2271
|
let info = null;
|
|
2043
2272
|
try {
|
|
2044
|
-
info = await stat(
|
|
2273
|
+
info = await stat(join7(projectRoot, rel));
|
|
2045
2274
|
} catch {
|
|
2046
2275
|
info = null;
|
|
2047
2276
|
}
|
|
@@ -2206,7 +2435,7 @@ async function generateCodeContext(request, deps) {
|
|
|
2206
2435
|
}
|
|
2207
2436
|
|
|
2208
2437
|
// src/refine.ts
|
|
2209
|
-
import { join as
|
|
2438
|
+
import { join as join8, relative as relative3 } from "pathe";
|
|
2210
2439
|
function stageArtifactRefs(ctx, stageId) {
|
|
2211
2440
|
const stage = getStage(ctx.workflow, ctx.meta, stageId);
|
|
2212
2441
|
if (!stage) return [];
|
|
@@ -2214,7 +2443,7 @@ function stageArtifactRefs(ctx, stageId) {
|
|
|
2214
2443
|
stageId,
|
|
2215
2444
|
path: a.path,
|
|
2216
2445
|
required: Boolean(a.required),
|
|
2217
|
-
absPath:
|
|
2446
|
+
absPath: join8(ctx.path, a.path)
|
|
2218
2447
|
}));
|
|
2219
2448
|
}
|
|
2220
2449
|
function listMarkdown(refs, displayDir) {
|
|
@@ -2255,11 +2484,11 @@ async function buildRefinePlan(opts) {
|
|
|
2255
2484
|
laterArtifacts.push(...stageArtifactRefs(ctx, s.id));
|
|
2256
2485
|
}
|
|
2257
2486
|
const focusArtifacts = stageArtifactRefs(ctx, focusStageId);
|
|
2258
|
-
const constitutionPath =
|
|
2487
|
+
const constitutionPath = join8(memoryDir(opts.projectRoot, opts.config), "constitution.md");
|
|
2259
2488
|
const constitutionExists = await pathExists(constitutionPath);
|
|
2260
|
-
const briefPath =
|
|
2261
|
-
const reportPath =
|
|
2262
|
-
const openItemsPath =
|
|
2489
|
+
const briefPath = join8(ctx.path, "refine-brief.md");
|
|
2490
|
+
const reportPath = join8(ctx.path, "quality-report.md");
|
|
2491
|
+
const openItemsPath = join8(ctx.path, "open-items.md");
|
|
2263
2492
|
const paths = guidancePaths(opts.config);
|
|
2264
2493
|
const briefRel = `${ctx.displayPath}/refine-brief.md`;
|
|
2265
2494
|
const reportRel = `${ctx.displayPath}/quality-report.md`;
|
|
@@ -2396,7 +2625,7 @@ Do **not** invent content for later stages that were not materialized yet. If a
|
|
|
2396
2625
|
}
|
|
2397
2626
|
async function writeRefineBrief(plan, projectRoot) {
|
|
2398
2627
|
await writeText(plan.briefPath, plan.briefMarkdown);
|
|
2399
|
-
const pointer =
|
|
2628
|
+
const pointer = join8(sddRoot(projectRoot), "refine-brief.md");
|
|
2400
2629
|
const briefRel = relative3(projectRoot, plan.briefPath);
|
|
2401
2630
|
await writeText(
|
|
2402
2631
|
pointer,
|
|
@@ -2412,310 +2641,1740 @@ Run: \`sdd refine${plan.focusStageId ? ` ${plan.focusStageId}` : ""}\`
|
|
|
2412
2641
|
}
|
|
2413
2642
|
|
|
2414
2643
|
// src/role/handoff.ts
|
|
2415
|
-
import { join as
|
|
2416
|
-
import { parse as parseYaml3, stringify as
|
|
2644
|
+
import { join as join16 } from "pathe";
|
|
2645
|
+
import { parse as parseYaml3, stringify as stringifyYaml } from "yaml";
|
|
2417
2646
|
|
|
2418
|
-
// src/role/
|
|
2419
|
-
import {
|
|
2420
|
-
import {
|
|
2421
|
-
import {
|
|
2647
|
+
// src/role/routing.ts
|
|
2648
|
+
import { join as join15 } from "pathe";
|
|
2649
|
+
import { parse as parseYaml2 } from "yaml";
|
|
2650
|
+
import { z as z6 } from "zod";
|
|
2422
2651
|
|
|
2423
|
-
// src/role/
|
|
2424
|
-
|
|
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
|
-
};
|
|
2652
|
+
// src/role/models/decide.ts
|
|
2653
|
+
import { z as z2 } from "zod";
|
|
2441
2654
|
|
|
2442
|
-
// src/role/
|
|
2443
|
-
|
|
2444
|
-
function
|
|
2445
|
-
|
|
2446
|
-
|
|
2447
|
-
|
|
2448
|
-
|
|
2449
|
-
|
|
2450
|
-
}
|
|
2451
|
-
|
|
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");
|
|
2655
|
+
// src/role/models/command.ts
|
|
2656
|
+
import { spawn as spawn3 } from "child_process";
|
|
2657
|
+
function decideStdin(input) {
|
|
2658
|
+
return `${JSON.stringify({
|
|
2659
|
+
mode: "decide",
|
|
2660
|
+
state: input.state,
|
|
2661
|
+
question: input.question,
|
|
2662
|
+
options: input.options
|
|
2663
|
+
})}
|
|
2664
|
+
`;
|
|
2462
2665
|
}
|
|
2463
|
-
function
|
|
2464
|
-
return
|
|
2666
|
+
function spawnModelCommand(command, stdin) {
|
|
2667
|
+
return new Promise((resolve, reject) => {
|
|
2668
|
+
const child = spawn3(command, { shell: true, stdio: ["pipe", "pipe", "ignore"] });
|
|
2669
|
+
const out = [];
|
|
2670
|
+
child.stdout?.on("data", (chunk) => out.push(chunk));
|
|
2671
|
+
child.on("error", reject);
|
|
2672
|
+
child.stdin?.on("error", () => void 0);
|
|
2673
|
+
child.on("close", (code) => {
|
|
2674
|
+
if (code !== 0) {
|
|
2675
|
+
reject(new Error(`role model exited ${code ?? "unknown"}`));
|
|
2676
|
+
return;
|
|
2677
|
+
}
|
|
2678
|
+
resolve(Buffer.concat(out).toString("utf8").trim());
|
|
2679
|
+
});
|
|
2680
|
+
child.stdin?.write(stdin);
|
|
2681
|
+
child.stdin?.end();
|
|
2682
|
+
});
|
|
2465
2683
|
}
|
|
2466
|
-
function
|
|
2467
|
-
|
|
2684
|
+
function modelFromEnv(env) {
|
|
2685
|
+
const command = env.SDD_ROLE_MODEL?.trim();
|
|
2686
|
+
if (!command) return void 0;
|
|
2687
|
+
return {
|
|
2688
|
+
async complete(input) {
|
|
2689
|
+
const text2 = await spawnModelCommand(
|
|
2690
|
+
command,
|
|
2691
|
+
`${JSON.stringify({ roleId: input.roleId, prompt: input.prompt })}
|
|
2692
|
+
`
|
|
2693
|
+
);
|
|
2694
|
+
let parsed;
|
|
2695
|
+
try {
|
|
2696
|
+
parsed = JSON.parse(text2);
|
|
2697
|
+
} catch {
|
|
2698
|
+
throw new Error("role model stdout is not JSON");
|
|
2699
|
+
}
|
|
2700
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
2701
|
+
throw new Error("role model stdout is not a JSON object");
|
|
2702
|
+
}
|
|
2703
|
+
return text2;
|
|
2704
|
+
}
|
|
2705
|
+
};
|
|
2468
2706
|
}
|
|
2469
|
-
|
|
2470
|
-
|
|
2707
|
+
|
|
2708
|
+
// src/role/models/ollama.ts
|
|
2709
|
+
function count(value) {
|
|
2710
|
+
return typeof value === "number" && Number.isInteger(value) && value >= 0 ? value : null;
|
|
2471
2711
|
}
|
|
2472
|
-
function
|
|
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;
|
|
2712
|
+
function chatUrl(base) {
|
|
2480
2713
|
try {
|
|
2481
|
-
|
|
2714
|
+
const trimmed = base.trim();
|
|
2715
|
+
const absolute = trimmed !== "" && !/^https?:/i.test(trimmed) ? `http://${trimmed}` : trimmed;
|
|
2716
|
+
const withSlash = absolute.endsWith("/") ? absolute : `${absolute}/`;
|
|
2717
|
+
return new URL("api/chat", withSlash).toString();
|
|
2482
2718
|
} catch {
|
|
2483
2719
|
return null;
|
|
2484
2720
|
}
|
|
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
2721
|
}
|
|
2498
|
-
function
|
|
2499
|
-
const
|
|
2500
|
-
|
|
2501
|
-
|
|
2502
|
-
|
|
2503
|
-
|
|
2504
|
-
|
|
2505
|
-
|
|
2506
|
-
|
|
2722
|
+
async function ollamaDecision(input) {
|
|
2723
|
+
const url = chatUrl(input.baseUrl);
|
|
2724
|
+
if (!url) {
|
|
2725
|
+
return {
|
|
2726
|
+
called: false,
|
|
2727
|
+
model: input.model,
|
|
2728
|
+
content: null,
|
|
2729
|
+
inputTokens: null,
|
|
2730
|
+
outputTokens: null
|
|
2731
|
+
};
|
|
2732
|
+
}
|
|
2733
|
+
const body = JSON.stringify({
|
|
2734
|
+
model: input.model,
|
|
2735
|
+
stream: false,
|
|
2736
|
+
format: {
|
|
2737
|
+
type: "object",
|
|
2738
|
+
properties: {
|
|
2739
|
+
pick: { type: "string" },
|
|
2740
|
+
probs: { type: "object", additionalProperties: { type: "number" } }
|
|
2741
|
+
},
|
|
2742
|
+
required: ["pick", "probs"]
|
|
2507
2743
|
},
|
|
2508
|
-
|
|
2509
|
-
|
|
2510
|
-
|
|
2511
|
-
|
|
2512
|
-
|
|
2513
|
-
|
|
2514
|
-
|
|
2515
|
-
|
|
2516
|
-
|
|
2517
|
-
|
|
2518
|
-
|
|
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;
|
|
2744
|
+
messages: [
|
|
2745
|
+
{
|
|
2746
|
+
role: "user",
|
|
2747
|
+
content: JSON.stringify({
|
|
2748
|
+
state: input.state,
|
|
2749
|
+
question: input.question,
|
|
2750
|
+
options: input.options
|
|
2751
|
+
})
|
|
2752
|
+
}
|
|
2753
|
+
]
|
|
2754
|
+
});
|
|
2526
2755
|
try {
|
|
2527
|
-
|
|
2756
|
+
const response = await input.fetch(url, {
|
|
2757
|
+
method: "POST",
|
|
2758
|
+
headers: { "content-type": "application/json" },
|
|
2759
|
+
body
|
|
2760
|
+
});
|
|
2761
|
+
const text2 = await response.text();
|
|
2762
|
+
if (!response.ok) {
|
|
2763
|
+
return {
|
|
2764
|
+
called: true,
|
|
2765
|
+
model: input.model,
|
|
2766
|
+
content: null,
|
|
2767
|
+
inputTokens: null,
|
|
2768
|
+
outputTokens: null
|
|
2769
|
+
};
|
|
2770
|
+
}
|
|
2771
|
+
let parsed;
|
|
2772
|
+
try {
|
|
2773
|
+
parsed = JSON.parse(text2);
|
|
2774
|
+
} catch {
|
|
2775
|
+
return {
|
|
2776
|
+
called: true,
|
|
2777
|
+
model: input.model,
|
|
2778
|
+
content: null,
|
|
2779
|
+
inputTokens: null,
|
|
2780
|
+
outputTokens: null
|
|
2781
|
+
};
|
|
2782
|
+
}
|
|
2783
|
+
const record = parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
|
|
2784
|
+
const message = record.message && typeof record.message === "object" && !Array.isArray(record.message) ? record.message : {};
|
|
2785
|
+
const content = typeof message.content === "string" ? message.content : message.content && typeof message.content === "object" ? JSON.stringify(message.content) : null;
|
|
2786
|
+
const model = typeof record.model === "string" && record.model.trim() ? record.model : input.model;
|
|
2787
|
+
return {
|
|
2788
|
+
called: true,
|
|
2789
|
+
model,
|
|
2790
|
+
content,
|
|
2791
|
+
inputTokens: count(record.prompt_eval_count),
|
|
2792
|
+
outputTokens: count(record.eval_count)
|
|
2793
|
+
};
|
|
2528
2794
|
} catch {
|
|
2529
|
-
return
|
|
2795
|
+
return {
|
|
2796
|
+
called: true,
|
|
2797
|
+
model: input.model,
|
|
2798
|
+
content: null,
|
|
2799
|
+
inputTokens: null,
|
|
2800
|
+
outputTokens: null
|
|
2801
|
+
};
|
|
2530
2802
|
}
|
|
2531
2803
|
}
|
|
2532
|
-
|
|
2533
|
-
|
|
2804
|
+
|
|
2805
|
+
// src/role/models/rules.ts
|
|
2806
|
+
function rulesDecision(options) {
|
|
2807
|
+
return { pick: options.includes("human") ? "human" : "human" };
|
|
2534
2808
|
}
|
|
2535
|
-
|
|
2536
|
-
|
|
2537
|
-
|
|
2538
|
-
const
|
|
2539
|
-
|
|
2540
|
-
|
|
2541
|
-
|
|
2542
|
-
|
|
2543
|
-
|
|
2544
|
-
|
|
2545
|
-
|
|
2546
|
-
|
|
2547
|
-
|
|
2548
|
-
|
|
2549
|
-
|
|
2550
|
-
|
|
2551
|
-
|
|
2552
|
-
|
|
2553
|
-
|
|
2554
|
-
if (
|
|
2555
|
-
|
|
2556
|
-
|
|
2557
|
-
if (
|
|
2558
|
-
|
|
2559
|
-
|
|
2560
|
-
if (
|
|
2561
|
-
|
|
2562
|
-
|
|
2563
|
-
|
|
2564
|
-
|
|
2565
|
-
|
|
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;
|
|
2809
|
+
|
|
2810
|
+
// src/role/models/select.ts
|
|
2811
|
+
function slot(map, workType) {
|
|
2812
|
+
const id = map?.[workType]?.trim();
|
|
2813
|
+
return id || void 0;
|
|
2814
|
+
}
|
|
2815
|
+
function selectModel(config, ctx) {
|
|
2816
|
+
const bot = ctx.bot?.trim();
|
|
2817
|
+
const stage = ctx.stage?.trim();
|
|
2818
|
+
const role = ctx.role?.trim();
|
|
2819
|
+
const fromBot = bot ? slot(config.overrides?.bots?.[bot], ctx.workType) : void 0;
|
|
2820
|
+
const fromStage = stage ? slot(config.overrides?.stages?.[stage], ctx.workType) : void 0;
|
|
2821
|
+
const fromRole = role ? slot(config.overrides?.roles?.[role], ctx.workType) : void 0;
|
|
2822
|
+
const fromWork = slot(config.defaults.by_work_type, ctx.workType);
|
|
2823
|
+
let id;
|
|
2824
|
+
let why;
|
|
2825
|
+
if (fromBot && bot) {
|
|
2826
|
+
id = fromBot;
|
|
2827
|
+
why = `bot ${bot}`;
|
|
2828
|
+
} else if (fromStage && stage) {
|
|
2829
|
+
id = fromStage;
|
|
2830
|
+
why = `stage ${stage}`;
|
|
2831
|
+
} else if (fromRole && role) {
|
|
2832
|
+
id = fromRole;
|
|
2833
|
+
why = `role ${role}`;
|
|
2834
|
+
} else if (fromWork) {
|
|
2835
|
+
id = fromWork;
|
|
2836
|
+
why = "work-type default";
|
|
2837
|
+
} else {
|
|
2838
|
+
id = workKind(ctx.workType) === "decide" ? config.defaults.fallback_decide : config.defaults.fallback_reason;
|
|
2839
|
+
why = "fallback";
|
|
2636
2840
|
}
|
|
2637
|
-
|
|
2638
|
-
|
|
2639
|
-
|
|
2640
|
-
|
|
2641
|
-
|
|
2642
|
-
|
|
2841
|
+
const model = config.models[id];
|
|
2842
|
+
if (!model) return { id, kind: workKind(ctx.workType), provider: "rules", why };
|
|
2843
|
+
return { id, kind: model.kind, provider: model.provider, why };
|
|
2844
|
+
}
|
|
2845
|
+
|
|
2846
|
+
// src/role/models/decide.ts
|
|
2847
|
+
var tokenField = z2.number().int().nonnegative().nullable().optional();
|
|
2848
|
+
var DecisionReplySchema = z2.object({
|
|
2849
|
+
pick: z2.string(),
|
|
2850
|
+
probs: z2.record(z2.string(), z2.number()),
|
|
2851
|
+
model: z2.string().trim().min(1).optional(),
|
|
2852
|
+
input_tokens: tokenField,
|
|
2853
|
+
cache_read_tokens: tokenField,
|
|
2854
|
+
cache_write_tokens: tokenField,
|
|
2855
|
+
output_tokens: tokenField
|
|
2856
|
+
}).strict();
|
|
2857
|
+
function minimumOf(entry) {
|
|
2858
|
+
if (!entry || entry.provider === "rules") return void 0;
|
|
2859
|
+
return entry.min_confidence;
|
|
2860
|
+
}
|
|
2861
|
+
function humanOutcome(choice, confidence2) {
|
|
2862
|
+
return { pick: "human", source: "human", choice, confidence: confidence2 };
|
|
2863
|
+
}
|
|
2864
|
+
function emptyUsage() {
|
|
2865
|
+
return {
|
|
2866
|
+
inputTokens: null,
|
|
2867
|
+
cacheReadTokens: null,
|
|
2868
|
+
cacheWriteTokens: null,
|
|
2869
|
+
outputTokens: null
|
|
2870
|
+
};
|
|
2643
2871
|
}
|
|
2644
|
-
|
|
2645
|
-
return
|
|
2872
|
+
function usageFrom(reply, base) {
|
|
2873
|
+
return {
|
|
2874
|
+
model: reply.model ?? base.model,
|
|
2875
|
+
inputTokens: reply.input_tokens === void 0 ? base.inputTokens : reply.input_tokens,
|
|
2876
|
+
cacheReadTokens: reply.cache_read_tokens === void 0 ? base.cacheReadTokens : reply.cache_read_tokens,
|
|
2877
|
+
cacheWriteTokens: reply.cache_write_tokens === void 0 ? base.cacheWriteTokens : reply.cache_write_tokens,
|
|
2878
|
+
outputTokens: reply.output_tokens === void 0 ? base.outputTokens : reply.output_tokens
|
|
2879
|
+
};
|
|
2646
2880
|
}
|
|
2647
|
-
|
|
2648
|
-
|
|
2649
|
-
if (!await pathExists(path)) return null;
|
|
2881
|
+
function scoreReply(raw, options, base) {
|
|
2882
|
+
let value;
|
|
2650
2883
|
try {
|
|
2651
|
-
|
|
2652
|
-
if (!data || typeof data !== "object" || Array.isArray(data)) return null;
|
|
2653
|
-
return data;
|
|
2884
|
+
value = JSON.parse(raw);
|
|
2654
2885
|
} catch {
|
|
2655
|
-
return
|
|
2886
|
+
return { ok: false, usage: base };
|
|
2887
|
+
}
|
|
2888
|
+
const parsed = DecisionReplySchema.safeParse(value);
|
|
2889
|
+
if (!parsed.success) return { ok: false, usage: base };
|
|
2890
|
+
const reply = parsed.data;
|
|
2891
|
+
const usage = usageFrom(reply, base);
|
|
2892
|
+
if (!options.includes(reply.pick)) return { ok: false, usage };
|
|
2893
|
+
const keys = Object.keys(reply.probs);
|
|
2894
|
+
if (keys.length !== options.length) return { ok: false, usage };
|
|
2895
|
+
let sum = 0;
|
|
2896
|
+
for (const option of options) {
|
|
2897
|
+
const probability = reply.probs[option];
|
|
2898
|
+
if (typeof probability !== "number" || probability < 0 || probability > 1) {
|
|
2899
|
+
return { ok: false, usage };
|
|
2900
|
+
}
|
|
2901
|
+
sum += probability;
|
|
2656
2902
|
}
|
|
2903
|
+
if (Math.abs(sum - 1) > 0.01 + 1e-9) return { ok: false, usage };
|
|
2904
|
+
const confidence2 = reply.probs[reply.pick];
|
|
2905
|
+
if (typeof confidence2 !== "number") return { ok: false, usage };
|
|
2906
|
+
return { ok: true, pick: reply.pick, confidence: confidence2, usage };
|
|
2657
2907
|
}
|
|
2658
|
-
|
|
2659
|
-
|
|
2660
|
-
|
|
2661
|
-
|
|
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 };
|
|
2908
|
+
function storedModel(entry, usage) {
|
|
2909
|
+
if (usage.model) return usage.model;
|
|
2910
|
+
if (entry.provider === "ollama") return entry.model;
|
|
2911
|
+
return null;
|
|
2676
2912
|
}
|
|
2677
|
-
function
|
|
2678
|
-
|
|
2913
|
+
async function recordCall(input, entry, usage) {
|
|
2914
|
+
if (!input.metrics) return;
|
|
2915
|
+
if (entry.provider === "rules") return;
|
|
2916
|
+
await recordDecisionMetric(
|
|
2917
|
+
input.metrics.projectRoot,
|
|
2918
|
+
input.metrics.config,
|
|
2919
|
+
input.metrics.changeId,
|
|
2920
|
+
{
|
|
2921
|
+
workType: input.ctx.workType,
|
|
2922
|
+
kind: entry.kind,
|
|
2923
|
+
provider: entry.provider,
|
|
2924
|
+
model: storedModel(entry, usage),
|
|
2925
|
+
startedAt: (input.now ?? (() => /* @__PURE__ */ new Date()))().toISOString(),
|
|
2926
|
+
inputTokens: usage.inputTokens,
|
|
2927
|
+
cacheReadTokens: usage.cacheReadTokens,
|
|
2928
|
+
cacheWriteTokens: usage.cacheWriteTokens,
|
|
2929
|
+
outputTokens: usage.outputTokens
|
|
2930
|
+
}
|
|
2931
|
+
);
|
|
2679
2932
|
}
|
|
2680
|
-
function
|
|
2681
|
-
|
|
2933
|
+
function judge(choice, entry, scored, floor) {
|
|
2934
|
+
const minimum = minimumOf(entry) ?? floor;
|
|
2935
|
+
if (!scored.ok) {
|
|
2936
|
+
return {
|
|
2937
|
+
kept: false,
|
|
2938
|
+
missing: false,
|
|
2939
|
+
escalate: minimum !== void 0,
|
|
2940
|
+
confidence: 0,
|
|
2941
|
+
outcome: humanOutcome(choice, 0)
|
|
2942
|
+
};
|
|
2943
|
+
}
|
|
2944
|
+
if (minimum !== void 0 && scored.confidence < minimum) {
|
|
2945
|
+
return {
|
|
2946
|
+
kept: false,
|
|
2947
|
+
missing: false,
|
|
2948
|
+
escalate: true,
|
|
2949
|
+
confidence: scored.confidence,
|
|
2950
|
+
outcome: humanOutcome(choice, scored.confidence)
|
|
2951
|
+
};
|
|
2952
|
+
}
|
|
2953
|
+
const source = choice.kind === "reason" ? "reason" : "decide";
|
|
2954
|
+
return {
|
|
2955
|
+
kept: true,
|
|
2956
|
+
missing: false,
|
|
2957
|
+
escalate: false,
|
|
2958
|
+
confidence: scored.confidence,
|
|
2959
|
+
outcome: { pick: scored.pick, source, choice, confidence: scored.confidence }
|
|
2960
|
+
};
|
|
2682
2961
|
}
|
|
2683
|
-
function
|
|
2684
|
-
const
|
|
2685
|
-
|
|
2686
|
-
|
|
2687
|
-
|
|
2962
|
+
async function runOne(input, choice, floor) {
|
|
2963
|
+
const entry = input.models?.models[choice.id];
|
|
2964
|
+
if (!entry) {
|
|
2965
|
+
return {
|
|
2966
|
+
kept: false,
|
|
2967
|
+
missing: true,
|
|
2968
|
+
escalate: false,
|
|
2969
|
+
confidence: null,
|
|
2970
|
+
outcome: humanOutcome(choice, null)
|
|
2971
|
+
};
|
|
2972
|
+
}
|
|
2973
|
+
if (entry.provider === "rules") {
|
|
2974
|
+
rulesDecision(input.options);
|
|
2975
|
+
return {
|
|
2976
|
+
kept: true,
|
|
2977
|
+
missing: false,
|
|
2978
|
+
escalate: false,
|
|
2979
|
+
confidence: null,
|
|
2980
|
+
outcome: { pick: "human", source: "rules", choice, confidence: null }
|
|
2981
|
+
};
|
|
2982
|
+
}
|
|
2983
|
+
if (entry.provider === "command") {
|
|
2984
|
+
const command = input.env[entry.command_env]?.trim();
|
|
2985
|
+
if (!command || !input.runCommand) {
|
|
2986
|
+
return {
|
|
2987
|
+
kept: false,
|
|
2988
|
+
missing: true,
|
|
2989
|
+
escalate: false,
|
|
2990
|
+
confidence: null,
|
|
2991
|
+
outcome: humanOutcome(choice, null)
|
|
2992
|
+
};
|
|
2993
|
+
}
|
|
2994
|
+
let stdout = "";
|
|
2995
|
+
try {
|
|
2996
|
+
stdout = await input.runCommand(command, decideStdin(input));
|
|
2997
|
+
} catch {
|
|
2998
|
+
await recordCall(input, entry, emptyUsage());
|
|
2999
|
+
return judge(choice, entry, { ok: false }, floor);
|
|
3000
|
+
}
|
|
3001
|
+
const scored2 = scoreReply(stdout, input.options, emptyUsage());
|
|
3002
|
+
await recordCall(input, entry, scored2.usage);
|
|
3003
|
+
return judge(choice, entry, scored2, floor);
|
|
3004
|
+
}
|
|
3005
|
+
const baseUrl = input.env[entry.base_url_env]?.trim();
|
|
3006
|
+
if (!baseUrl || !input.fetch) {
|
|
3007
|
+
return {
|
|
3008
|
+
kept: false,
|
|
3009
|
+
missing: true,
|
|
3010
|
+
escalate: false,
|
|
3011
|
+
confidence: null,
|
|
3012
|
+
outcome: humanOutcome(choice, null)
|
|
3013
|
+
};
|
|
3014
|
+
}
|
|
3015
|
+
const remote = await ollamaDecision({
|
|
3016
|
+
baseUrl,
|
|
3017
|
+
model: entry.model,
|
|
3018
|
+
state: input.state,
|
|
3019
|
+
question: input.question,
|
|
3020
|
+
options: input.options,
|
|
3021
|
+
fetch: input.fetch
|
|
3022
|
+
});
|
|
3023
|
+
if (!remote.called) {
|
|
3024
|
+
return {
|
|
3025
|
+
kept: false,
|
|
3026
|
+
missing: true,
|
|
3027
|
+
escalate: false,
|
|
3028
|
+
confidence: null,
|
|
3029
|
+
outcome: humanOutcome(choice, null)
|
|
3030
|
+
};
|
|
3031
|
+
}
|
|
3032
|
+
const base = {
|
|
3033
|
+
model: remote.model,
|
|
3034
|
+
inputTokens: remote.inputTokens,
|
|
3035
|
+
cacheReadTokens: null,
|
|
3036
|
+
cacheWriteTokens: null,
|
|
3037
|
+
outputTokens: remote.outputTokens
|
|
3038
|
+
};
|
|
3039
|
+
const scored = remote.content ? scoreReply(remote.content, input.options, base) : { ok: false, usage: base };
|
|
3040
|
+
await recordCall(input, entry, scored.usage);
|
|
3041
|
+
return judge(choice, entry, scored, floor);
|
|
3042
|
+
}
|
|
3043
|
+
async function decide(input) {
|
|
3044
|
+
if (!input.models) return { pick: "human", source: "human", choice: null, confidence: null };
|
|
3045
|
+
const choice = selectModel(input.models, input.ctx);
|
|
3046
|
+
const first = await runOne(input, choice);
|
|
3047
|
+
if (first.kept) return first.outcome;
|
|
3048
|
+
if (first.missing || !first.escalate) return first.outcome;
|
|
3049
|
+
const floor = minimumOf(input.models.models[choice.id]);
|
|
3050
|
+
const fallbackId = input.models.defaults.fallback_reason;
|
|
3051
|
+
if (choice.id === fallbackId) return first.outcome;
|
|
3052
|
+
const fallbackEntry = input.models.models[fallbackId];
|
|
3053
|
+
if (!fallbackEntry) return first.outcome;
|
|
3054
|
+
const fallback = {
|
|
3055
|
+
id: fallbackId,
|
|
3056
|
+
kind: fallbackEntry.kind,
|
|
3057
|
+
provider: fallbackEntry.provider,
|
|
3058
|
+
why: "fallback"
|
|
3059
|
+
};
|
|
3060
|
+
const second = await runOne(input, fallback, floor);
|
|
3061
|
+
if (second.kept) return second.outcome;
|
|
3062
|
+
return second.outcome;
|
|
2688
3063
|
}
|
|
2689
|
-
|
|
2690
|
-
|
|
3064
|
+
|
|
3065
|
+
// src/role/run.ts
|
|
3066
|
+
import { appendFile as appendFile5 } from "fs/promises";
|
|
3067
|
+
import { isAbsolute as isAbsolute4, join as join14 } from "pathe";
|
|
3068
|
+
|
|
3069
|
+
// src/team/facts.ts
|
|
3070
|
+
import { appendFile } from "fs/promises";
|
|
3071
|
+
import { join as join9 } from "pathe";
|
|
3072
|
+
function ok(stdout = "") {
|
|
3073
|
+
return { exitCode: 0, stdout };
|
|
2691
3074
|
}
|
|
2692
|
-
function
|
|
2693
|
-
|
|
2694
|
-
if (id === "." || id === "..") return false;
|
|
2695
|
-
return !id.includes("/") && !id.includes("\\") && !id.includes("\0");
|
|
3075
|
+
function fail(stdout) {
|
|
3076
|
+
return { exitCode: 1, stdout };
|
|
2696
3077
|
}
|
|
2697
|
-
function
|
|
2698
|
-
|
|
2699
|
-
if (id === "." || id === "..") return false;
|
|
2700
|
-
return !id.includes("/") && !id.includes("\\") && !id.includes("\0");
|
|
3078
|
+
function factsPath(root) {
|
|
3079
|
+
return join9(root, ".sdd", "facts.jsonl");
|
|
2701
3080
|
}
|
|
2702
|
-
function
|
|
2703
|
-
|
|
2704
|
-
|
|
2705
|
-
|
|
2706
|
-
const parts = rel.split(/[/\\]/);
|
|
2707
|
-
for (const part of parts) {
|
|
2708
|
-
if (part === "" || part === "." || part === ".." || part === ".active" || part === ".sdd") {
|
|
2709
|
-
return false;
|
|
2710
|
-
}
|
|
3081
|
+
function sharesWords(left, right) {
|
|
3082
|
+
const bag = wordsOf(left);
|
|
3083
|
+
for (const word of wordsOf(right)) {
|
|
3084
|
+
if (bag.has(word)) return true;
|
|
2711
3085
|
}
|
|
2712
|
-
return
|
|
3086
|
+
return false;
|
|
2713
3087
|
}
|
|
2714
|
-
function
|
|
2715
|
-
return
|
|
3088
|
+
function wordsOf(text2) {
|
|
3089
|
+
return new Set(text2.toLowerCase().match(/[\p{L}\p{N}_]{4,}/gu) ?? []);
|
|
2716
3090
|
}
|
|
2717
|
-
function
|
|
2718
|
-
|
|
3091
|
+
async function readFactLines(root) {
|
|
3092
|
+
const file = factsPath(root);
|
|
3093
|
+
if (!await pathExists(file)) return [];
|
|
3094
|
+
const out = [];
|
|
3095
|
+
for (const line of (await readText(file)).split(/\r?\n/)) {
|
|
3096
|
+
if (!line.trim()) continue;
|
|
3097
|
+
try {
|
|
3098
|
+
const value = JSON.parse(line);
|
|
3099
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) continue;
|
|
3100
|
+
const rec = value;
|
|
3101
|
+
if (typeof rec.key !== "string" || typeof rec.value !== "string") continue;
|
|
3102
|
+
out.push({
|
|
3103
|
+
key: rec.key,
|
|
3104
|
+
value: rec.value,
|
|
3105
|
+
ts: typeof rec.ts === "string" ? rec.ts : "",
|
|
3106
|
+
by: typeof rec.by === "string" ? rec.by : "",
|
|
3107
|
+
source: typeof rec.source === "string" ? rec.source : ""
|
|
3108
|
+
});
|
|
3109
|
+
} catch {
|
|
3110
|
+
}
|
|
3111
|
+
}
|
|
3112
|
+
return out;
|
|
3113
|
+
}
|
|
3114
|
+
async function getFacts(root, query) {
|
|
3115
|
+
const lines = await readFactLines(root);
|
|
3116
|
+
const order = [];
|
|
3117
|
+
const newest = /* @__PURE__ */ new Map();
|
|
3118
|
+
for (const fact of lines) {
|
|
3119
|
+
if (!newest.has(fact.key)) order.push(fact.key);
|
|
3120
|
+
newest.set(fact.key, fact);
|
|
3121
|
+
}
|
|
3122
|
+
let facts = order.map((key) => newest.get(key));
|
|
3123
|
+
if (query?.keys) {
|
|
3124
|
+
const want = new Set(query.keys);
|
|
3125
|
+
facts = facts.filter((fact) => want.has(fact.key));
|
|
3126
|
+
}
|
|
3127
|
+
if (query?.query) {
|
|
3128
|
+
const text2 = query.query;
|
|
3129
|
+
facts = facts.filter((fact) => sharesWords(text2, `${fact.key} ${fact.value}`));
|
|
3130
|
+
}
|
|
3131
|
+
return facts;
|
|
3132
|
+
}
|
|
3133
|
+
async function setFact(root, input) {
|
|
3134
|
+
const rawKey = input.key ?? "";
|
|
3135
|
+
const rawValue = input.value ?? "";
|
|
3136
|
+
const rawSource = input.source ?? "";
|
|
3137
|
+
if (rawKey.includes("\n") || rawKey.includes("\r") || rawValue.includes("\n") || rawValue.includes("\r") || rawSource.includes("\n") || rawSource.includes("\r")) {
|
|
3138
|
+
return fail("A fact is one line.");
|
|
3139
|
+
}
|
|
3140
|
+
const key = rawKey.trim();
|
|
3141
|
+
const value = rawValue.trim();
|
|
3142
|
+
if (!key || !value) return fail("A fact needs a key and a value.");
|
|
3143
|
+
if (key.includes("/") || key.includes("\\") || key.includes("\0")) {
|
|
3144
|
+
return fail("A fact key is not valid.");
|
|
3145
|
+
}
|
|
3146
|
+
if (await contradicts(root, value)) return escalate(root, key);
|
|
3147
|
+
const fact = {
|
|
3148
|
+
key,
|
|
3149
|
+
value,
|
|
3150
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3151
|
+
by: gitUserName(root) ?? "unknown",
|
|
3152
|
+
source: rawSource.trim() || "person"
|
|
3153
|
+
};
|
|
3154
|
+
const file = factsPath(root);
|
|
3155
|
+
await ensureDir(join9(file, ".."));
|
|
3156
|
+
await appendFile(file, `${JSON.stringify(fact)}
|
|
3157
|
+
`, "utf8");
|
|
3158
|
+
return ok("Fact set.");
|
|
3159
|
+
}
|
|
3160
|
+
async function showFact(root, key) {
|
|
3161
|
+
const name = (key ?? "").trim();
|
|
3162
|
+
if (!name) return fail("A fact needs a key and a value.");
|
|
3163
|
+
const facts = await getFacts(root, { keys: [name] });
|
|
3164
|
+
const hit = facts[0];
|
|
3165
|
+
if (!hit) return fail("Fact not set.");
|
|
3166
|
+
return ok(hit.value);
|
|
3167
|
+
}
|
|
3168
|
+
async function listFacts(root, query) {
|
|
3169
|
+
const facts = await getFacts(root, query ? { query } : void 0);
|
|
3170
|
+
if (facts.length === 0) return ok("No facts.");
|
|
3171
|
+
return ok(facts.map((fact) => `${fact.key}: ${fact.value}`).join("\n"));
|
|
3172
|
+
}
|
|
3173
|
+
async function contradicts(root, value) {
|
|
3174
|
+
if (!/\b(ignore|skip|override)\b/i.test(value)) return false;
|
|
3175
|
+
let text2 = "";
|
|
3176
|
+
try {
|
|
3177
|
+
const config = await loadConfig(root);
|
|
3178
|
+
const file = join9(root, guidancePaths(config).constitution);
|
|
3179
|
+
if (await pathExists(file)) text2 = await readText(file);
|
|
3180
|
+
} catch {
|
|
3181
|
+
const file = join9(root, "memory", "constitution.md");
|
|
3182
|
+
if (await pathExists(file)) text2 = await readText(file);
|
|
3183
|
+
}
|
|
3184
|
+
for (const line of text2.split(/\r?\n/)) {
|
|
3185
|
+
if (!line.startsWith("- ")) continue;
|
|
3186
|
+
const bullet = line.slice(2).trim();
|
|
3187
|
+
if (bullet.length >= 12 && value.includes(bullet)) return true;
|
|
3188
|
+
}
|
|
3189
|
+
return false;
|
|
3190
|
+
}
|
|
3191
|
+
async function escalate(root, key) {
|
|
3192
|
+
const note = `fact ${key} contradicts constitution`;
|
|
3193
|
+
let changeId = null;
|
|
3194
|
+
try {
|
|
3195
|
+
const config = await loadConfig(root);
|
|
3196
|
+
changeId = await getActiveChangeId(root, config);
|
|
3197
|
+
} catch {
|
|
3198
|
+
changeId = null;
|
|
3199
|
+
}
|
|
3200
|
+
if (!changeId) return fail("A fact contradicts the constitution.");
|
|
3201
|
+
return recordDecisionRequest(root, { note, change: changeId });
|
|
3202
|
+
}
|
|
3203
|
+
|
|
3204
|
+
// src/team/memory.ts
|
|
3205
|
+
import { appendFile as appendFile2 } from "fs/promises";
|
|
3206
|
+
import { join as join10 } from "pathe";
|
|
3207
|
+
var BRAIN_FILES = ["memory.md", "lessons.jsonl"];
|
|
3208
|
+
function ok2(stdout = "") {
|
|
3209
|
+
return { exitCode: 0, stdout };
|
|
3210
|
+
}
|
|
3211
|
+
function fail2(stdout) {
|
|
3212
|
+
return { exitCode: 1, stdout };
|
|
3213
|
+
}
|
|
3214
|
+
async function knownBot(root, name) {
|
|
3215
|
+
const team = await readTeamConfig(root);
|
|
3216
|
+
if ("error" in team) return fail2(team.error);
|
|
3217
|
+
if (!team.team.bots.some((bot) => bot.name === name)) return fail2("Bot not found.");
|
|
3218
|
+
return null;
|
|
3219
|
+
}
|
|
3220
|
+
async function readMemory(root, bot) {
|
|
3221
|
+
const file = join10(memoryDir2(root, bot, true), "memory.md");
|
|
3222
|
+
if (!await pathExists(file)) return "";
|
|
3223
|
+
return readText(file);
|
|
3224
|
+
}
|
|
3225
|
+
async function readLessons(root, bot) {
|
|
3226
|
+
const file = join10(memoryDir2(root, bot, true), "lessons.jsonl");
|
|
3227
|
+
if (!await pathExists(file)) return [];
|
|
3228
|
+
const lessons = [];
|
|
3229
|
+
for (const line of (await readText(file)).split(/\r?\n/)) {
|
|
3230
|
+
if (!line.trim()) continue;
|
|
3231
|
+
try {
|
|
3232
|
+
const value = JSON.parse(line);
|
|
3233
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) continue;
|
|
3234
|
+
const rec = value;
|
|
3235
|
+
if (typeof rec.text !== "string" || !rec.text.trim()) continue;
|
|
3236
|
+
const tags = Array.isArray(rec.tags) ? rec.tags.filter((tag) => typeof tag === "string") : void 0;
|
|
3237
|
+
lessons.push({
|
|
3238
|
+
ts: typeof rec.ts === "string" ? rec.ts : "",
|
|
3239
|
+
by: typeof rec.by === "string" ? rec.by : "",
|
|
3240
|
+
text: rec.text,
|
|
3241
|
+
source: typeof rec.source === "string" ? rec.source : void 0,
|
|
3242
|
+
tags
|
|
3243
|
+
});
|
|
3244
|
+
} catch {
|
|
3245
|
+
}
|
|
3246
|
+
}
|
|
3247
|
+
return lessons;
|
|
3248
|
+
}
|
|
3249
|
+
async function appendLesson(root, bot, lesson) {
|
|
3250
|
+
const file = join10(memoryDir2(root, bot, true), "lessons.jsonl");
|
|
3251
|
+
await ensureDir(join10(file, ".."));
|
|
3252
|
+
await appendFile2(file, `${JSON.stringify(lesson)}
|
|
3253
|
+
`, "utf8");
|
|
3254
|
+
}
|
|
3255
|
+
async function showMemory(root, bot) {
|
|
3256
|
+
const name = canonicalRoleName(bot);
|
|
3257
|
+
if (!name) return fail2(ROLE_TEXT.badName);
|
|
3258
|
+
const missing = await knownBot(root, name);
|
|
3259
|
+
if (missing) return missing;
|
|
3260
|
+
const text2 = await readMemory(root, name);
|
|
3261
|
+
if (!text2) return ok2("No memory.");
|
|
3262
|
+
return ok2(text2.endsWith("\n") ? text2.slice(0, -1) : text2);
|
|
3263
|
+
}
|
|
3264
|
+
async function addLesson(root, bot, text2) {
|
|
3265
|
+
const name = canonicalRoleName(bot);
|
|
3266
|
+
if (!name) return fail2(ROLE_TEXT.badName);
|
|
3267
|
+
const missing = await knownBot(root, name);
|
|
3268
|
+
if (missing) return missing;
|
|
3269
|
+
if (text2.includes("\n") || text2.includes("\r")) return fail2("A lesson is one line.");
|
|
3270
|
+
const words = text2.trim();
|
|
3271
|
+
if (!words) return fail2("A lesson needs text.");
|
|
3272
|
+
await appendLesson(root, name, {
|
|
3273
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3274
|
+
by: gitUserName(root) ?? "unknown",
|
|
3275
|
+
text: words,
|
|
3276
|
+
source: "person"
|
|
3277
|
+
});
|
|
3278
|
+
return ok2("Lesson added.");
|
|
3279
|
+
}
|
|
3280
|
+
async function addReviewerLesson(root, bot, lesson) {
|
|
3281
|
+
const text2 = lesson.text.trim();
|
|
3282
|
+
if (!text2 || text2.includes("\n") || text2.includes("\r")) return;
|
|
3283
|
+
const existing = await readLessons(root, bot);
|
|
3284
|
+
if (existing.some((item) => item.text === text2)) return;
|
|
3285
|
+
await appendLesson(root, bot, {
|
|
3286
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3287
|
+
by: lesson.by || "reviewer",
|
|
3288
|
+
text: text2,
|
|
3289
|
+
source: "reviewer"
|
|
3290
|
+
});
|
|
3291
|
+
}
|
|
3292
|
+
async function migrateTeamMemory(root) {
|
|
3293
|
+
const team = await readTeamConfig(root);
|
|
3294
|
+
if ("error" in team) return fail2(team.error);
|
|
3295
|
+
const base = join10(root, ".sdd", "role-runs", "memory");
|
|
3296
|
+
const lines = [];
|
|
3297
|
+
for (const name of (await listDirs(base)).sort()) {
|
|
3298
|
+
if (!canonicalRoleName(name)) continue;
|
|
3299
|
+
for (const file of BRAIN_FILES) {
|
|
3300
|
+
const from = join10(base, name, file);
|
|
3301
|
+
if (!await pathExists(from)) continue;
|
|
3302
|
+
const fromRel = `.sdd/role-runs/memory/${name}/${file}`;
|
|
3303
|
+
const toRel = `.sdd/agents/${name}/${file}`;
|
|
3304
|
+
const to = join10(memoryDir2(root, name, true), file);
|
|
3305
|
+
if (await pathExists(to)) {
|
|
3306
|
+
lines.push(`left ${fromRel}`);
|
|
3307
|
+
continue;
|
|
3308
|
+
}
|
|
3309
|
+
await ensureDir(join10(to, ".."));
|
|
3310
|
+
await writeText(to, await readText(from));
|
|
3311
|
+
await removePath(from);
|
|
3312
|
+
lines.push(`moved ${fromRel} -> ${toRel}`);
|
|
3313
|
+
}
|
|
3314
|
+
if ((await listFiles(join10(base, name))).length === 0) await removePath(join10(base, name));
|
|
3315
|
+
}
|
|
3316
|
+
if (lines.length === 0) return ok2("Nothing to migrate.");
|
|
3317
|
+
return ok2(lines.join("\n"));
|
|
3318
|
+
}
|
|
3319
|
+
|
|
3320
|
+
// src/team/brain.ts
|
|
3321
|
+
var WARNING = "Memory is longer than the budget.";
|
|
3322
|
+
async function loadBrain(root, cfg, bot, item, budgetTokens) {
|
|
3323
|
+
const spec = cfg.bots.find((entry) => entry.name === bot);
|
|
3324
|
+
if (!spec) return { error: "Bot not found." };
|
|
3325
|
+
const role = await readRole(root, spec.role);
|
|
3326
|
+
if (!role) return { error: ROLE_TEXT.notFound };
|
|
3327
|
+
const file = roleFilePath(root, spec.role);
|
|
3328
|
+
if (!await pathExists(file)) return { error: ROLE_TEXT.notFound };
|
|
3329
|
+
const roleText = await readText(file);
|
|
3330
|
+
const memoryAll = await readMemory(root, spec.name);
|
|
3331
|
+
const lessonsNewest = (await readLessons(root, spec.name)).slice().reverse();
|
|
3332
|
+
const factsNewest = (await getFacts(root, { query: item.text })).slice().reverse();
|
|
3333
|
+
const packed = pack({
|
|
3334
|
+
roleText,
|
|
3335
|
+
memoryAll,
|
|
3336
|
+
lessons: lessonsNewest,
|
|
3337
|
+
facts: factsNewest,
|
|
3338
|
+
item: item.text,
|
|
3339
|
+
budget: budgetTokens
|
|
3340
|
+
});
|
|
3341
|
+
return {
|
|
3342
|
+
role,
|
|
3343
|
+
bot: spec,
|
|
3344
|
+
memory: packed.memory,
|
|
3345
|
+
lessons: packed.lessons,
|
|
3346
|
+
facts: packed.facts,
|
|
3347
|
+
inputs: [],
|
|
3348
|
+
prompt: packed.prompt,
|
|
3349
|
+
warning: packed.warning
|
|
3350
|
+
};
|
|
3351
|
+
}
|
|
3352
|
+
function pack(input) {
|
|
3353
|
+
let memory = input.memoryAll;
|
|
3354
|
+
let lessons = input.lessons.slice();
|
|
3355
|
+
let facts = input.facts.slice();
|
|
3356
|
+
let warning = "";
|
|
3357
|
+
const itemBlock = `Item:
|
|
3358
|
+
${input.item}`;
|
|
3359
|
+
const render = () => {
|
|
3360
|
+
const parts = [input.roleText.trimEnd()];
|
|
3361
|
+
if (warning) parts.push(warning);
|
|
3362
|
+
if (memory.trim()) parts.push(`Memory:
|
|
3363
|
+
${memory.trimEnd()}`);
|
|
3364
|
+
if (lessons.length > 0) {
|
|
3365
|
+
parts.push(`Lessons:
|
|
3366
|
+
${lessons.map((lesson) => `- ${lesson.text}`).join("\n")}`);
|
|
3367
|
+
}
|
|
3368
|
+
if (facts.length > 0) {
|
|
3369
|
+
parts.push(`Facts:
|
|
3370
|
+
${facts.map((fact) => `${fact.key}: ${fact.value}`).join("\n")}`);
|
|
3371
|
+
}
|
|
3372
|
+
parts.push(itemBlock);
|
|
3373
|
+
return parts.join("\n");
|
|
3374
|
+
};
|
|
3375
|
+
const roleOnly = countTokens(input.roleText) > input.budget;
|
|
3376
|
+
if (roleOnly) {
|
|
3377
|
+
memory = "";
|
|
3378
|
+
lessons = [];
|
|
3379
|
+
facts = [];
|
|
3380
|
+
warning = input.memoryAll.trim() ? WARNING : "";
|
|
3381
|
+
return { memory, lessons, facts, prompt: render(), warning };
|
|
3382
|
+
}
|
|
3383
|
+
while (countTokens(render()) > input.budget && facts.length > 0) facts.pop();
|
|
3384
|
+
while (countTokens(render()) > input.budget && lessons.length > 0) lessons.pop();
|
|
3385
|
+
if (countTokens(render()) > input.budget && memory.trim()) {
|
|
3386
|
+
const lines = memory.split(/\r?\n/);
|
|
3387
|
+
while (lines.length > 0 && countTokens(render()) > input.budget) {
|
|
3388
|
+
lines.shift();
|
|
3389
|
+
memory = lines.join("\n");
|
|
3390
|
+
}
|
|
3391
|
+
}
|
|
3392
|
+
if (input.memoryAll.trim() && memory.trim() !== input.memoryAll.trim()) {
|
|
3393
|
+
warning = WARNING;
|
|
3394
|
+
while (countTokens(render()) > input.budget && facts.length > 0) facts.pop();
|
|
3395
|
+
while (countTokens(render()) > input.budget && lessons.length > 0) lessons.pop();
|
|
3396
|
+
if (countTokens(render()) > input.budget) memory = "";
|
|
3397
|
+
}
|
|
3398
|
+
return { memory, lessons, facts, prompt: render(), warning };
|
|
3399
|
+
}
|
|
3400
|
+
|
|
3401
|
+
// src/team/loop.ts
|
|
3402
|
+
import { appendFile as appendFile4 } from "fs/promises";
|
|
3403
|
+
import { basename, join as join13 } from "pathe";
|
|
3404
|
+
import { z as z5 } from "zod";
|
|
3405
|
+
|
|
3406
|
+
// src/team/room.ts
|
|
3407
|
+
import { appendFile as appendFile3 } from "fs/promises";
|
|
3408
|
+
import { join as join12 } from "pathe";
|
|
3409
|
+
import { z as z4 } from "zod";
|
|
3410
|
+
|
|
3411
|
+
// src/role/prompts.ts
|
|
3412
|
+
function critiquePrompt(role, section, draft) {
|
|
3413
|
+
return [
|
|
3414
|
+
`Job: ${role.job}`,
|
|
3415
|
+
"Check ids:",
|
|
3416
|
+
...section.checks,
|
|
3417
|
+
`Cite: ${section.id}:${section.decision}`,
|
|
3418
|
+
"Draft:",
|
|
3419
|
+
draft
|
|
3420
|
+
].join("\n");
|
|
3421
|
+
}
|
|
3422
|
+
function revisionPrompt(role, draft, gaps) {
|
|
3423
|
+
return [
|
|
3424
|
+
`Job: ${role.job}`,
|
|
3425
|
+
"Draft:",
|
|
3426
|
+
draft,
|
|
3427
|
+
"Gaps:",
|
|
3428
|
+
...gaps.map((gap) => `${gap.cite}: ${gap.text}`),
|
|
3429
|
+
"changed must list every gap cite."
|
|
3430
|
+
].join("\n");
|
|
3431
|
+
}
|
|
3432
|
+
|
|
3433
|
+
// src/team/tools.ts
|
|
3434
|
+
import { spawn as spawn4 } from "child_process";
|
|
3435
|
+
import { mkdtemp, rm } from "fs/promises";
|
|
3436
|
+
import { tmpdir } from "os";
|
|
3437
|
+
import { isAbsolute as isAbsolute3, join as join11 } from "pathe";
|
|
3438
|
+
import { z as z3 } from "zod";
|
|
3439
|
+
var OUTPUT_CAP = 4e3;
|
|
3440
|
+
var SCOPES = /* @__PURE__ */ new Set(["code", "specs", "memory"]);
|
|
3441
|
+
var ToolsSchema = z3.object({
|
|
3442
|
+
allow: z3.array(z3.object({ run: z3.string().trim().min(1) }).strict()).default([])
|
|
3443
|
+
}).strict();
|
|
3444
|
+
var EvidenceSchema = z3.object({
|
|
3445
|
+
sha: z3.string(),
|
|
3446
|
+
runs: z3.array(
|
|
3447
|
+
z3.object({
|
|
3448
|
+
command: z3.string(),
|
|
3449
|
+
exitCode: z3.number().int().nullable(),
|
|
3450
|
+
output: z3.string()
|
|
3451
|
+
}).strict()
|
|
3452
|
+
)
|
|
3453
|
+
}).strict();
|
|
3454
|
+
var BLOCKED_CHECK = /\bsdd\s+(next|complete)\b|\bgit\s+(push|merge)\b|\bgh\s+pr\s+(create|merge)\b/;
|
|
3455
|
+
var NEGATED = /\b(?:not|no|never|cannot|can't|cant|dont|don't|doesnt|doesn't|isnt|isn't)\s+(?:yet\s+)?(?:pass|works|fixed)\b/gi;
|
|
3456
|
+
function claimsPass(text2) {
|
|
3457
|
+
return /\b(?:pass|works|fixed)\b/i.test(text2.replace(NEGATED, " "));
|
|
3458
|
+
}
|
|
3459
|
+
function safePath(rel) {
|
|
3460
|
+
if (typeof rel !== "string" || rel.length === 0 || rel.includes("\0")) return false;
|
|
3461
|
+
if (isAbsolute3(rel) || rel.startsWith("\\")) return false;
|
|
3462
|
+
if (/^[A-Za-z]:/.test(rel)) return false;
|
|
3463
|
+
const parts = rel.split(/[/\\]/);
|
|
3464
|
+
for (const part of parts) {
|
|
3465
|
+
if (part === "" || part === "." || part === ".." || part === ".active" || part === ".sdd") {
|
|
3466
|
+
return false;
|
|
3467
|
+
}
|
|
3468
|
+
}
|
|
3469
|
+
return parts[parts.length - 1] !== "accepted.md";
|
|
3470
|
+
}
|
|
3471
|
+
function clip(text2) {
|
|
3472
|
+
return text2.length > OUTPUT_CAP ? text2.slice(-OUTPUT_CAP) : text2;
|
|
3473
|
+
}
|
|
3474
|
+
function asRecord(value) {
|
|
3475
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return {};
|
|
3476
|
+
return value;
|
|
3477
|
+
}
|
|
3478
|
+
function result(name, ok8, text2) {
|
|
3479
|
+
return { ok: ok8, name, text: clip(text2) };
|
|
3480
|
+
}
|
|
3481
|
+
async function git(cwd, args) {
|
|
3482
|
+
return new Promise((resolve) => {
|
|
3483
|
+
const child = spawn4("git", args, { cwd, env: process.env });
|
|
3484
|
+
let stdout = "";
|
|
3485
|
+
let stderr = "";
|
|
3486
|
+
child.stdout?.on("data", (chunk) => {
|
|
3487
|
+
stdout += chunk.toString();
|
|
3488
|
+
});
|
|
3489
|
+
child.stderr?.on("data", (chunk) => {
|
|
3490
|
+
stderr += chunk.toString();
|
|
3491
|
+
});
|
|
3492
|
+
child.on("error", (error) => resolve({ code: 1, stdout, stderr: String(error) }));
|
|
3493
|
+
child.on("close", (code) => resolve({ code, stdout, stderr }));
|
|
3494
|
+
});
|
|
3495
|
+
}
|
|
3496
|
+
async function shell(command, cwd) {
|
|
3497
|
+
return new Promise((resolve) => {
|
|
3498
|
+
const child = spawn4(command, { cwd, shell: true, env: process.env });
|
|
3499
|
+
let output = "";
|
|
3500
|
+
child.stdout?.on("data", (chunk) => {
|
|
3501
|
+
output += chunk.toString();
|
|
3502
|
+
});
|
|
3503
|
+
child.stderr?.on("data", (chunk) => {
|
|
3504
|
+
output += chunk.toString();
|
|
3505
|
+
});
|
|
3506
|
+
child.on("error", (error) => resolve({ code: 1, output: String(error) }));
|
|
3507
|
+
child.on("close", (code) => resolve({ code, output }));
|
|
3508
|
+
});
|
|
3509
|
+
}
|
|
3510
|
+
async function headOf(root) {
|
|
3511
|
+
try {
|
|
3512
|
+
return await getHeadSha(root);
|
|
3513
|
+
} catch {
|
|
3514
|
+
return null;
|
|
3515
|
+
}
|
|
3516
|
+
}
|
|
3517
|
+
async function activeChange(root) {
|
|
3518
|
+
const config = await loadConfig(root);
|
|
3519
|
+
const id = await getActiveChangeId(root, config);
|
|
3520
|
+
if (!id) return null;
|
|
3521
|
+
return { id, path: changePath(root, config, id) };
|
|
3522
|
+
}
|
|
3523
|
+
function evidenceRel(changesPath, changeId, sha) {
|
|
3524
|
+
return `${changesPath}/${changeId}/evidence/${sha}.json`.replace(/\\/g, "/");
|
|
3525
|
+
}
|
|
3526
|
+
async function evidenceFile(root, changeId, sha) {
|
|
3527
|
+
const config = await loadConfig(root);
|
|
3528
|
+
return join11(changePath(root, config, changeId), "evidence", `${sha}.json`);
|
|
3529
|
+
}
|
|
3530
|
+
async function evidenceSupports(root, changeId, cite) {
|
|
3531
|
+
const sha = await headOf(root);
|
|
3532
|
+
if (!sha) return false;
|
|
3533
|
+
const config = await loadConfig(root);
|
|
3534
|
+
const expected = evidenceRel(config.changes_path, changeId, sha);
|
|
3535
|
+
if (cite.trim().replace(/\\/g, "/").replace(/^\.\//, "") !== expected) return false;
|
|
3536
|
+
const file = join11(root, ...expected.split("/"));
|
|
3537
|
+
if (!await pathExists(file)) return false;
|
|
3538
|
+
let parsed;
|
|
3539
|
+
try {
|
|
3540
|
+
const value = EvidenceSchema.safeParse(JSON.parse(await readText(file)));
|
|
3541
|
+
if (!value.success) return false;
|
|
3542
|
+
parsed = value.data;
|
|
3543
|
+
} catch {
|
|
3544
|
+
return false;
|
|
3545
|
+
}
|
|
3546
|
+
if (parsed.sha !== sha || parsed.runs.length === 0) return false;
|
|
3547
|
+
return parsed.runs[parsed.runs.length - 1]?.exitCode === 0;
|
|
3548
|
+
}
|
|
3549
|
+
async function passClaimRefusal(root, text2, cites) {
|
|
3550
|
+
if (!claimsPass(text2)) return null;
|
|
3551
|
+
const change = await activeChange(root).catch(() => null);
|
|
3552
|
+
if (!change) return "A pass claim needs evidence at this SHA.";
|
|
3553
|
+
for (const cite of cites) {
|
|
3554
|
+
if (await evidenceSupports(root, change.id, cite)) return null;
|
|
3555
|
+
}
|
|
3556
|
+
return "A pass claim needs evidence at this SHA.";
|
|
3557
|
+
}
|
|
3558
|
+
async function allowList(root) {
|
|
3559
|
+
const change = await activeChange(root);
|
|
3560
|
+
const stageRuns = [];
|
|
3561
|
+
if (change) {
|
|
3562
|
+
const config = await loadConfig(root);
|
|
3563
|
+
const ctx = await buildContext(root, config, change.id);
|
|
3564
|
+
const stage = getStage(ctx.workflow, ctx.meta, ctx.meta.stage);
|
|
3565
|
+
for (const command of stage?.verify?.commands ?? []) stageRuns.push(command.run);
|
|
3566
|
+
}
|
|
3567
|
+
const file = join11(root, ".sdd", "tools.yaml");
|
|
3568
|
+
if (!await pathExists(file)) return stageRuns;
|
|
3569
|
+
let raw;
|
|
3570
|
+
try {
|
|
3571
|
+
raw = await readYaml(file);
|
|
3572
|
+
} catch {
|
|
3573
|
+
return { error: "tools.yaml is invalid: tools could not be parsed" };
|
|
3574
|
+
}
|
|
3575
|
+
const parsed = ToolsSchema.safeParse(raw ?? {});
|
|
3576
|
+
if (!parsed.success) {
|
|
3577
|
+
const issue = parsed.error.issues[0];
|
|
3578
|
+
const path = issue?.path.join(".") || "tools";
|
|
3579
|
+
return { error: `tools.yaml is invalid: ${path} ${issue?.message ?? "invalid"}` };
|
|
3580
|
+
}
|
|
3581
|
+
return [...stageRuns, ...parsed.data.allow.map((entry) => entry.run)];
|
|
3582
|
+
}
|
|
3583
|
+
async function readFileTool(root, args) {
|
|
3584
|
+
const rel = typeof args.path === "string" ? args.path : "";
|
|
3585
|
+
if (!safePath(rel)) return result("read_file", false, "That path is outside the project.");
|
|
3586
|
+
if (isSecretPath(rel)) return result("read_file", false, "That path is not readable.");
|
|
3587
|
+
const file = join11(root, ...rel.split(/[/\\]/));
|
|
3588
|
+
if (!await pathExists(file)) return result("read_file", false, "That path is not readable.");
|
|
3589
|
+
try {
|
|
3590
|
+
const text2 = await readText(file);
|
|
3591
|
+
return result("read_file", true, `${rel}
|
|
3592
|
+
${text2}`);
|
|
3593
|
+
} catch {
|
|
3594
|
+
return result("read_file", false, "That path is not readable.");
|
|
3595
|
+
}
|
|
3596
|
+
}
|
|
3597
|
+
async function searchTool(root, args) {
|
|
3598
|
+
const query = typeof args.query === "string" ? args.query.trim() : "";
|
|
3599
|
+
if (!query) return result("search", false, "A search needs a query.");
|
|
3600
|
+
const scope = typeof args.scope === "string" && args.scope ? args.scope : "code";
|
|
3601
|
+
if (!SCOPES.has(scope)) {
|
|
3602
|
+
return result("search", false, "A search scope is code, specs, or memory.");
|
|
3603
|
+
}
|
|
3604
|
+
const found = await searchProject(root, query, {
|
|
3605
|
+
scope,
|
|
3606
|
+
k: 8,
|
|
3607
|
+
budget: 2e3
|
|
3608
|
+
});
|
|
3609
|
+
return result("search", true, found.text || query);
|
|
3610
|
+
}
|
|
3611
|
+
async function contextTool(root, args) {
|
|
3612
|
+
const path = typeof args.path === "string" ? args.path : "";
|
|
3613
|
+
const query = typeof args.query === "string" ? args.query : "";
|
|
3614
|
+
const symbol = typeof args.symbol === "string" ? args.symbol : "";
|
|
3615
|
+
const change = await activeChange(root).catch(() => null);
|
|
3616
|
+
const found = await generateCodeContext({
|
|
3617
|
+
projectRoot: root,
|
|
3618
|
+
changeId: change?.id,
|
|
3619
|
+
paths: path ? [path] : void 0,
|
|
3620
|
+
query: query || void 0,
|
|
3621
|
+
symbols: symbol ? [symbol] : void 0
|
|
3622
|
+
});
|
|
3623
|
+
return result(
|
|
3624
|
+
"context",
|
|
3625
|
+
found.ok,
|
|
3626
|
+
found.markdown || found.gaps.map((gap) => gap.message).join("\n")
|
|
3627
|
+
);
|
|
3628
|
+
}
|
|
3629
|
+
async function appendRun(root, changeId, sha, command, exitCode, output) {
|
|
3630
|
+
const file = await evidenceFile(root, changeId, sha);
|
|
3631
|
+
let body = { sha, runs: [] };
|
|
3632
|
+
if (await pathExists(file)) {
|
|
3633
|
+
try {
|
|
3634
|
+
const parsed = EvidenceSchema.safeParse(JSON.parse(await readText(file)));
|
|
3635
|
+
if (!parsed.success || parsed.data.sha !== sha) {
|
|
3636
|
+
return { error: "Evidence at this commit cannot be read." };
|
|
3637
|
+
}
|
|
3638
|
+
body = parsed.data;
|
|
3639
|
+
} catch {
|
|
3640
|
+
return { error: "Evidence at this commit cannot be read." };
|
|
3641
|
+
}
|
|
3642
|
+
}
|
|
3643
|
+
body.runs.push({ command, exitCode, output: clip(output) });
|
|
3644
|
+
await ensureDir(join11(file, ".."));
|
|
3645
|
+
await writeText(file, `${JSON.stringify(body, null, 2)}
|
|
3646
|
+
`);
|
|
3647
|
+
const config = await loadConfig(root);
|
|
3648
|
+
return evidenceRel(config.changes_path, changeId, sha);
|
|
3649
|
+
}
|
|
3650
|
+
async function runCheck(root, args) {
|
|
3651
|
+
const command = typeof args.run === "string" ? args.run.trim() : "";
|
|
3652
|
+
if (!command) return result("run_check", false, "That check is not allowlisted.");
|
|
3653
|
+
if (BLOCKED_CHECK.test(command)) {
|
|
3654
|
+
return result("run_check", false, "That check cannot run sdd next, sdd complete, or merge.");
|
|
3655
|
+
}
|
|
3656
|
+
const allowed = await allowList(root);
|
|
3657
|
+
if ("error" in allowed) return result("run_check", false, allowed.error);
|
|
3658
|
+
if (!allowed.includes(command))
|
|
3659
|
+
return result("run_check", false, "That check is not allowlisted.");
|
|
3660
|
+
const change = await activeChange(root);
|
|
3661
|
+
if (!change) return result("run_check", false, "No active change.");
|
|
3662
|
+
const sha = await headOf(root);
|
|
3663
|
+
if (!sha) return result("run_check", false, "Git HEAD is unavailable.");
|
|
3664
|
+
const dir = await mkdtemp(join11(tmpdir(), "sdd-check-"));
|
|
3665
|
+
const added = await git(root, ["worktree", "add", "--detach", dir, sha]);
|
|
3666
|
+
if (added.code !== 0) {
|
|
3667
|
+
await rm(dir, { recursive: true, force: true });
|
|
3668
|
+
return result("run_check", false, "Git HEAD is unavailable.");
|
|
3669
|
+
}
|
|
3670
|
+
let ran = { code: 1, output: "" };
|
|
3671
|
+
let removeFailed = false;
|
|
3672
|
+
try {
|
|
3673
|
+
ran = await shell(command, dir);
|
|
3674
|
+
} finally {
|
|
3675
|
+
const removed = await git(root, ["worktree", "remove", "--force", dir]);
|
|
3676
|
+
await rm(dir, { recursive: true, force: true });
|
|
3677
|
+
removeFailed = removed.code !== 0;
|
|
3678
|
+
}
|
|
3679
|
+
if (removeFailed) return result("run_check", false, "The check worktree could not be removed.");
|
|
3680
|
+
const stored = await appendRun(root, change.id, sha, command, ran.code, ran.output);
|
|
3681
|
+
if (typeof stored !== "string") return result("run_check", false, stored.error);
|
|
3682
|
+
const text2 = [`${command}`, `exit ${ran.code ?? "none"}`, stored, clip(ran.output)].join("\n");
|
|
3683
|
+
return result("run_check", ran.code === 0, text2);
|
|
3684
|
+
}
|
|
3685
|
+
async function writeFileTool(root, args) {
|
|
3686
|
+
const rel = typeof args.path === "string" ? args.path : "";
|
|
3687
|
+
const contents = args.contents;
|
|
3688
|
+
if (!safePath(rel) || typeof contents !== "string" || isSecretPath(rel)) {
|
|
3689
|
+
return result("write_file", false, "That file cannot be written.");
|
|
3690
|
+
}
|
|
3691
|
+
await writeText(join11(root, ...rel.split(/[/\\]/)), contents);
|
|
3692
|
+
return result("write_file", true, `Wrote ${rel}.`);
|
|
3693
|
+
}
|
|
3694
|
+
function branchName(team, bot, changeId) {
|
|
3695
|
+
return team.branch.replaceAll("{bot}", bot).replaceAll("{change}", changeId);
|
|
3696
|
+
}
|
|
3697
|
+
async function currentBranch(root) {
|
|
3698
|
+
const found = await git(root, ["rev-parse", "--abbrev-ref", "HEAD"]);
|
|
3699
|
+
return found.code === 0 ? found.stdout.trim() : "";
|
|
3700
|
+
}
|
|
3701
|
+
async function commitTool(root, team, bot, args) {
|
|
3702
|
+
const change = await activeChange(root);
|
|
3703
|
+
if (!change) return result("commit", false, "No active change.");
|
|
3704
|
+
const expected = branchName(team, bot, change.id);
|
|
3705
|
+
const branch = await currentBranch(root);
|
|
3706
|
+
if (branch !== expected) return result("commit", false, "A bot commits only on its own branch.");
|
|
3707
|
+
const message = typeof args.message === "string" ? args.message.trim() : "";
|
|
3708
|
+
const paths = Array.isArray(args.paths) ? args.paths.filter((item) => typeof item === "string") : [];
|
|
3709
|
+
if (!message || message.includes("\n") || paths.length === 0) {
|
|
3710
|
+
return result("commit", false, "A commit needs a message and a path.");
|
|
3711
|
+
}
|
|
3712
|
+
for (const rel of paths) {
|
|
3713
|
+
if (!safePath(rel) || isSecretPath(rel))
|
|
3714
|
+
return result("commit", false, "That path is outside the project.");
|
|
3715
|
+
}
|
|
3716
|
+
const added = await git(root, ["add", "--", ...paths]);
|
|
3717
|
+
if (added.code !== 0) return result("commit", false, clip(added.stderr || added.stdout));
|
|
3718
|
+
const committed = await git(root, ["commit", "-m", message]);
|
|
3719
|
+
if (committed.code !== 0)
|
|
3720
|
+
return result("commit", false, clip(committed.stderr || committed.stdout));
|
|
3721
|
+
return result("commit", true, clip(committed.stdout || message));
|
|
3722
|
+
}
|
|
3723
|
+
async function refusedShip(root, name, text2) {
|
|
3724
|
+
const sha = await headOf(root);
|
|
3725
|
+
return result(name, false, text2.replace("this SHA", sha ?? "this SHA"));
|
|
3726
|
+
}
|
|
3727
|
+
async function runTool(root, team, bot, call) {
|
|
3728
|
+
const args = asRecord(call.args);
|
|
3729
|
+
const name = call.name.trim();
|
|
3730
|
+
if (name === "read_file") return readFileTool(root, args);
|
|
3731
|
+
if (name === "search") return searchTool(root, args);
|
|
3732
|
+
if (name === "context") return contextTool(root, args);
|
|
3733
|
+
if (name === "run_check") return runCheck(root, args);
|
|
3734
|
+
if (name === "write_file") return writeFileTool(root, args);
|
|
3735
|
+
if (name === "commit") return commitTool(root, team, bot, args);
|
|
3736
|
+
if (name === "push") return refusedShip(root, name, "Push needs a person at this SHA.");
|
|
3737
|
+
if (name === "pull_request")
|
|
3738
|
+
return refusedShip(root, name, "A pull request needs a person at this SHA.");
|
|
3739
|
+
if (name === "merge" || name === "tag" || name === "deploy") {
|
|
3740
|
+
return result(name, false, "A bot cannot merge.");
|
|
3741
|
+
}
|
|
3742
|
+
if (name === "sdd_next" || name === "sdd_complete" || name === "accept_spec") {
|
|
3743
|
+
return result(name, false, "A role cannot do that.");
|
|
3744
|
+
}
|
|
3745
|
+
return result(name, false, "That tool is not available.");
|
|
3746
|
+
}
|
|
3747
|
+
async function reviewNeedsEvidence(root, changeId, commands, type, text2, cite) {
|
|
3748
|
+
const claim = claimsPass(text2);
|
|
3749
|
+
const agree = type === "agree" && commands > 0;
|
|
3750
|
+
if (!claim && !agree) return null;
|
|
3751
|
+
if (await evidenceSupports(root, changeId, cite)) return null;
|
|
3752
|
+
return claim ? "A pass claim needs evidence at this SHA." : "A review needs evidence at this SHA.";
|
|
3753
|
+
}
|
|
3754
|
+
|
|
3755
|
+
// src/team/room.ts
|
|
3756
|
+
var ROOM_TYPES = ["post", "objection", "agree", "block", "decision"];
|
|
3757
|
+
var RoomEntrySchema = z4.object({
|
|
3758
|
+
ts: z4.string(),
|
|
3759
|
+
from: z4.string(),
|
|
3760
|
+
type: z4.enum(ROOM_TYPES),
|
|
3761
|
+
text: z4.string(),
|
|
3762
|
+
round: z4.number().int(),
|
|
3763
|
+
cite: z4.string().optional(),
|
|
3764
|
+
sha: z4.string().optional(),
|
|
3765
|
+
evidence: z4.array(z4.string()).optional()
|
|
3766
|
+
}).strict();
|
|
3767
|
+
function ok3(stdout = "") {
|
|
3768
|
+
return { exitCode: 0, stdout };
|
|
3769
|
+
}
|
|
3770
|
+
function fail3(stdout) {
|
|
3771
|
+
return { exitCode: 1, stdout };
|
|
3772
|
+
}
|
|
3773
|
+
function safeId(value) {
|
|
3774
|
+
return /^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(value);
|
|
3775
|
+
}
|
|
3776
|
+
function oneLine(value, empty, broken) {
|
|
3777
|
+
const raw = value ?? "";
|
|
3778
|
+
if (raw.includes("\n") || raw.includes("\r")) return fail3(broken);
|
|
3779
|
+
const words = raw.trim();
|
|
3780
|
+
if (!words) return fail3(empty);
|
|
3781
|
+
return words;
|
|
3782
|
+
}
|
|
3783
|
+
function roomPath(root, change, stage) {
|
|
3784
|
+
return join12(root, ".sdd", "rooms", change, `${stage}.jsonl`);
|
|
3785
|
+
}
|
|
3786
|
+
function roomRounds(team) {
|
|
3787
|
+
return team.limits?.room_rounds ?? 2;
|
|
3788
|
+
}
|
|
3789
|
+
function consensus(entries) {
|
|
3790
|
+
const agreed = [];
|
|
3791
|
+
const open = [];
|
|
3792
|
+
for (const entry of entries) {
|
|
3793
|
+
if (entry.type === "agree" && !agreed.includes(entry.from)) agreed.push(entry.from);
|
|
3794
|
+
if (entry.type === "objection" || entry.type === "block") open.push(entry);
|
|
3795
|
+
}
|
|
3796
|
+
return { agreed, open, blocked: entries.some((entry) => entry.type === "block") };
|
|
3797
|
+
}
|
|
3798
|
+
async function readEntries(file) {
|
|
3799
|
+
if (!await pathExists(file)) return [];
|
|
3800
|
+
const entries = [];
|
|
3801
|
+
for (const line of (await readText(file)).split(/\r?\n/)) {
|
|
3802
|
+
if (!line.trim()) continue;
|
|
3803
|
+
try {
|
|
3804
|
+
const parsed = RoomEntrySchema.safeParse(JSON.parse(line));
|
|
3805
|
+
if (parsed.success) entries.push(parsed.data);
|
|
3806
|
+
} catch {
|
|
3807
|
+
}
|
|
3808
|
+
}
|
|
3809
|
+
return entries;
|
|
3810
|
+
}
|
|
3811
|
+
async function headSha(root) {
|
|
3812
|
+
try {
|
|
3813
|
+
return await getHeadSha(root);
|
|
3814
|
+
} catch {
|
|
3815
|
+
return null;
|
|
3816
|
+
}
|
|
3817
|
+
}
|
|
3818
|
+
async function openChange(root, change) {
|
|
3819
|
+
let config;
|
|
3820
|
+
try {
|
|
3821
|
+
config = await loadConfig(root);
|
|
3822
|
+
} catch (error) {
|
|
3823
|
+
return fail3(error instanceof Error ? error.message : "Config is invalid.");
|
|
3824
|
+
}
|
|
3825
|
+
const id = (change?.trim() || await getActiveChangeId(root, config) || "").trim();
|
|
3826
|
+
if (!id) return fail3("No active change.");
|
|
3827
|
+
if (!safeId(id)) return fail3("A change id is not valid.");
|
|
3828
|
+
try {
|
|
3829
|
+
const ctx = await buildContext(root, config, id);
|
|
3830
|
+
return { id, path: ctx.path, stage: ctx.meta.stage, workflow: ctx.workflow, meta: ctx.meta };
|
|
3831
|
+
} catch (error) {
|
|
3832
|
+
return fail3(error instanceof Error ? error.message : "Change not found.");
|
|
3833
|
+
}
|
|
3834
|
+
}
|
|
3835
|
+
function isFailure(value) {
|
|
3836
|
+
return "exitCode" in value;
|
|
3837
|
+
}
|
|
3838
|
+
function memberBots(team, names) {
|
|
3839
|
+
const bots = [];
|
|
3840
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3841
|
+
for (const name of names) {
|
|
3842
|
+
const exact = team.bots.find((bot) => bot.name === name);
|
|
3843
|
+
const chosen = exact ?? team.bots.find((bot) => bot.role === name);
|
|
3844
|
+
if (!chosen) return { error: "Bot not found." };
|
|
3845
|
+
if (seen.has(chosen.name)) continue;
|
|
3846
|
+
seen.add(chosen.name);
|
|
3847
|
+
bots.push(chosen);
|
|
3848
|
+
}
|
|
3849
|
+
if (bots.length === 0) return { error: "A room needs a member." };
|
|
3850
|
+
return bots;
|
|
3851
|
+
}
|
|
3852
|
+
async function draftOf(changePath2, stage) {
|
|
3853
|
+
const rel = stage?.artifacts[0]?.path;
|
|
3854
|
+
if (!rel || rel.includes("..")) return "";
|
|
3855
|
+
const file = join12(changePath2, rel);
|
|
3856
|
+
if (!await pathExists(file)) return "";
|
|
3857
|
+
return readText(file);
|
|
3858
|
+
}
|
|
3859
|
+
function formatEntry(entry) {
|
|
3860
|
+
const cite = entry.cite ? ` ${entry.cite}` : "";
|
|
3861
|
+
return `${entry.round} ${entry.from} ${entry.type}${cite} ${entry.text}`;
|
|
3862
|
+
}
|
|
3863
|
+
async function appendEntry(file, entry) {
|
|
3864
|
+
await ensureDir(join12(file, ".."));
|
|
3865
|
+
await appendFile3(file, `${JSON.stringify(entry)}
|
|
3866
|
+
`, "utf8");
|
|
3867
|
+
}
|
|
3868
|
+
async function consensusLine(changePath2, stage, sha, entries) {
|
|
3869
|
+
const found = consensus(entries);
|
|
3870
|
+
const agreed = found.agreed.join(",") || "none";
|
|
3871
|
+
const open = found.open.map((entry) => entry.cite || entry.text).join(",") || "none";
|
|
3872
|
+
await appendFile3(
|
|
3873
|
+
join12(changePath2, "decisions.md"),
|
|
3874
|
+
`${nowIso()} room ${stage} ${sha ?? "none"} agreed ${agreed} open ${open}
|
|
3875
|
+
`
|
|
3876
|
+
);
|
|
3877
|
+
}
|
|
3878
|
+
async function showRoom(root, input) {
|
|
3879
|
+
const team = await readTeamConfig(root);
|
|
3880
|
+
if ("error" in team) return fail3(team.error);
|
|
3881
|
+
const opened = await openChange(root, input.change);
|
|
3882
|
+
if (isFailure(opened)) return opened;
|
|
3883
|
+
const stage = (input.stage?.trim() || opened.stage).trim();
|
|
3884
|
+
if (!safeId(stage)) return fail3("A stage id is not valid.");
|
|
3885
|
+
const entries = await readEntries(roomPath(root, opened.id, stage));
|
|
3886
|
+
if (entries.length === 0) return ok3("No room.");
|
|
3887
|
+
return ok3(entries.map(formatEntry).join("\n"));
|
|
3888
|
+
}
|
|
3889
|
+
async function postRoom(root, input) {
|
|
3890
|
+
const team = await readTeamConfig(root);
|
|
3891
|
+
if ("error" in team) return fail3(team.error);
|
|
3892
|
+
const opened = await openChange(root, input.change);
|
|
3893
|
+
if (isFailure(opened)) return opened;
|
|
3894
|
+
const stage = (input.stage?.trim() || opened.stage).trim();
|
|
3895
|
+
if (!safeId(stage)) return fail3("A stage id is not valid.");
|
|
3896
|
+
const text2 = oneLine(input.text, "A room entry needs text.", "A room entry is one line.");
|
|
3897
|
+
if (typeof text2 !== "string") return text2;
|
|
3898
|
+
const type = input.type?.trim() || "post";
|
|
3899
|
+
if (!ROOM_TYPES.includes(type)) return fail3("A room entry is not valid.");
|
|
3900
|
+
const cite = input.cite?.trim() ?? "";
|
|
3901
|
+
if (cite && (cite.includes("\n") || cite.includes("\r"))) return fail3("A cite is one line.");
|
|
3902
|
+
if (type === "objection" && !cite) return fail3("An objection needs a cite.");
|
|
3903
|
+
const stageDef = getStage(opened.workflow, opened.meta, stage);
|
|
3904
|
+
const needed = await reviewNeedsEvidence(
|
|
3905
|
+
root,
|
|
3906
|
+
opened.id,
|
|
3907
|
+
stageDef?.verify?.commands?.length ?? 0,
|
|
3908
|
+
type,
|
|
3909
|
+
text2,
|
|
3910
|
+
cite
|
|
3911
|
+
);
|
|
3912
|
+
if (needed) return fail3(needed);
|
|
3913
|
+
const evidence = [];
|
|
3914
|
+
for (const item of input.evidence ?? []) {
|
|
3915
|
+
const line = oneLine(item, "Evidence needs text.", "Evidence is one line.");
|
|
3916
|
+
if (typeof line !== "string") return line;
|
|
3917
|
+
evidence.push(line);
|
|
3918
|
+
}
|
|
3919
|
+
const file = roomPath(root, opened.id, stage);
|
|
3920
|
+
const prior = await readEntries(file);
|
|
3921
|
+
const round = prior.reduce((max, entry2) => Math.max(max, entry2.round), 1);
|
|
3922
|
+
const sha = await headSha(root);
|
|
3923
|
+
const entry = {
|
|
3924
|
+
ts: nowIso(),
|
|
3925
|
+
from: input.from?.trim() || gitUserName(root) || "person",
|
|
3926
|
+
type,
|
|
3927
|
+
text: text2,
|
|
3928
|
+
round: prior.length === 0 ? 1 : round
|
|
3929
|
+
};
|
|
3930
|
+
if (cite) entry.cite = cite;
|
|
3931
|
+
if (sha) entry.sha = sha;
|
|
3932
|
+
if (evidence.length > 0) entry.evidence = evidence;
|
|
3933
|
+
await appendEntry(file, entry);
|
|
3934
|
+
return ok3("Posted.");
|
|
3935
|
+
}
|
|
3936
|
+
async function classifyReply(root, text2, env, runCommand2) {
|
|
3937
|
+
const loaded = await loadModels(root);
|
|
3938
|
+
if (loaded === null) return "post";
|
|
3939
|
+
if ("error" in loaded) return fail3(loaded.error);
|
|
3940
|
+
const decision = await decide({
|
|
3941
|
+
models: loaded.config,
|
|
3942
|
+
ctx: { workType: "classify" },
|
|
3943
|
+
state: text2,
|
|
3944
|
+
question: "What kind of room entry is this?",
|
|
3945
|
+
options: ROOM_TYPES,
|
|
3946
|
+
env,
|
|
3947
|
+
runCommand: runCommand2
|
|
3948
|
+
});
|
|
3949
|
+
if (decision.pick === "human") return "post";
|
|
3950
|
+
return decision.pick;
|
|
3951
|
+
}
|
|
3952
|
+
async function reviewRoom(root, input) {
|
|
3953
|
+
if (!input.model) return fail3(ROLE_TEXT.noModel);
|
|
3954
|
+
const loaded = await readTeamConfig(root);
|
|
3955
|
+
if ("error" in loaded) return fail3(loaded.error);
|
|
3956
|
+
const opened = await openChange(root, input.change);
|
|
3957
|
+
if (isFailure(opened)) return opened;
|
|
3958
|
+
const stageId = (input.stage?.trim() || opened.stage).trim();
|
|
3959
|
+
if (!safeId(stageId)) return fail3("A stage id is not valid.");
|
|
3960
|
+
const stage = getStage(opened.workflow, opened.meta, stageId);
|
|
3961
|
+
if (!stage) return fail3("Stage not found.");
|
|
3962
|
+
const names = input.roles && input.roles.length > 0 ? input.roles : [stage.owner, ...stage.collaborates ?? []].filter((name) => !!name);
|
|
3963
|
+
const bots = memberBots(loaded.team, names);
|
|
3964
|
+
if ("error" in bots) return fail3(bots.error);
|
|
3965
|
+
const file = roomPath(root, opened.id, stageId);
|
|
3966
|
+
const prior = await readEntries(file);
|
|
3967
|
+
const highest = prior.reduce((max, entry) => Math.max(max, entry.round), 0);
|
|
3968
|
+
const round = highest + 1;
|
|
3969
|
+
const cap = roomRounds(loaded.team);
|
|
3970
|
+
if (round > cap) return ok3("Round limit reached.");
|
|
3971
|
+
const draft = await draftOf(opened.path, stage);
|
|
3972
|
+
const before = consensus(prior.filter((entry) => entry.round === highest));
|
|
3973
|
+
const sha = await headSha(root);
|
|
3974
|
+
const written = [];
|
|
3975
|
+
for (const bot of bots) {
|
|
3976
|
+
const role = await readRole(root, bot.role);
|
|
3977
|
+
if (!role) return fail3(ROLE_TEXT.notFound);
|
|
3978
|
+
const section = {
|
|
3979
|
+
id: stageId,
|
|
3980
|
+
decision: "room",
|
|
3981
|
+
checks: before.open.map((entry2) => entry2.cite || "room"),
|
|
3982
|
+
body: draft
|
|
3983
|
+
};
|
|
3984
|
+
if (section.checks.length === 0) section.checks = ["room"];
|
|
3985
|
+
const rows = [critiquePrompt(role, section, draft || "(none)"), "Room:"];
|
|
3986
|
+
for (const entry2 of prior) rows.push(formatEntry(entry2));
|
|
3987
|
+
const owner = stage.owner === bot.role || stage.owner === bot.name;
|
|
3988
|
+
if (owner && before.open.length > 0) {
|
|
3989
|
+
rows.push(
|
|
3990
|
+
revisionPrompt(
|
|
3991
|
+
role,
|
|
3992
|
+
draft || "(none)",
|
|
3993
|
+
before.open.map((entry2) => ({ cite: entry2.cite || "room", text: entry2.text }))
|
|
3994
|
+
)
|
|
3995
|
+
);
|
|
3996
|
+
}
|
|
3997
|
+
rows.push('Reply with JSON {"type":"post","text":"...","cite":"..."}');
|
|
3998
|
+
rows.push("An objection needs a cite.");
|
|
3999
|
+
let raw = "";
|
|
4000
|
+
try {
|
|
4001
|
+
raw = await input.model.complete({ roleId: role.name, prompt: rows.join("\n") });
|
|
4002
|
+
} catch {
|
|
4003
|
+
return fail3("A room entry is not valid.");
|
|
4004
|
+
}
|
|
4005
|
+
const stored = await entryFromReply(root, raw, input.env, input.runCommand);
|
|
4006
|
+
if (isFailure(stored)) return stored;
|
|
4007
|
+
const needed = await reviewNeedsEvidence(
|
|
4008
|
+
root,
|
|
4009
|
+
opened.id,
|
|
4010
|
+
stage.verify?.commands?.length ?? 0,
|
|
4011
|
+
stored.type,
|
|
4012
|
+
stored.text,
|
|
4013
|
+
stored.cite ?? ""
|
|
4014
|
+
);
|
|
4015
|
+
if (needed) return fail3(needed);
|
|
4016
|
+
const entry = {
|
|
4017
|
+
ts: nowIso(),
|
|
4018
|
+
from: bot.name,
|
|
4019
|
+
type: stored.type,
|
|
4020
|
+
text: stored.text,
|
|
4021
|
+
round
|
|
4022
|
+
};
|
|
4023
|
+
if (stored.cite) entry.cite = stored.cite;
|
|
4024
|
+
if (sha) entry.sha = sha;
|
|
4025
|
+
await appendEntry(file, entry);
|
|
4026
|
+
written.push(entry);
|
|
4027
|
+
}
|
|
4028
|
+
const latest = written;
|
|
4029
|
+
await consensusLine(opened.path, stageId, sha, latest);
|
|
4030
|
+
if (round >= cap && consensus(latest).open.length > 0) {
|
|
4031
|
+
const asked = await recordDecisionRequest(root, {
|
|
4032
|
+
change: opened.id,
|
|
4033
|
+
note: `Room ${stageId} still open after ${round} rounds.`
|
|
4034
|
+
});
|
|
4035
|
+
if (asked.exitCode !== 0) return asked;
|
|
4036
|
+
return ok3("Decision needed.");
|
|
4037
|
+
}
|
|
4038
|
+
return ok3(`Round ${round}.`);
|
|
4039
|
+
}
|
|
4040
|
+
async function entryFromReply(root, raw, env = process.env, runCommand2) {
|
|
4041
|
+
let value;
|
|
4042
|
+
try {
|
|
4043
|
+
value = JSON.parse(raw);
|
|
4044
|
+
} catch {
|
|
4045
|
+
value = null;
|
|
4046
|
+
}
|
|
4047
|
+
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
4048
|
+
const rec = value;
|
|
4049
|
+
if (typeof rec.type === "string" && typeof rec.text === "string") {
|
|
4050
|
+
return checked(rec.type, rec.text, typeof rec.cite === "string" ? rec.cite : "");
|
|
4051
|
+
}
|
|
4052
|
+
}
|
|
4053
|
+
const text2 = raw.trim();
|
|
4054
|
+
if (!text2 || text2.includes("\n") || text2.includes("\r"))
|
|
4055
|
+
return fail3("A room entry is not valid.");
|
|
4056
|
+
const typed = await classifyReply(root, text2, env, runCommand2);
|
|
4057
|
+
if (typeof typed !== "string") return typed;
|
|
4058
|
+
return checked(typed, text2, "");
|
|
4059
|
+
}
|
|
4060
|
+
function checked(type, text2, cite) {
|
|
4061
|
+
if (!ROOM_TYPES.includes(type)) return fail3("A room entry is not valid.");
|
|
4062
|
+
const words = oneLine(text2, "A room entry needs text.", "A room entry is one line.");
|
|
4063
|
+
if (typeof words !== "string") return words;
|
|
4064
|
+
const citeWords = cite.trim();
|
|
4065
|
+
if (citeWords && (cite.includes("\n") || cite.includes("\r"))) return fail3("A cite is one line.");
|
|
4066
|
+
if (type === "objection" && !citeWords) return fail3("An objection needs a cite.");
|
|
4067
|
+
return citeWords ? { type, text: words, cite: citeWords } : { type, text: words };
|
|
4068
|
+
}
|
|
4069
|
+
async function activeRoom(root) {
|
|
4070
|
+
try {
|
|
4071
|
+
const config = await loadConfig(root);
|
|
4072
|
+
const id = await getActiveChangeId(root, config);
|
|
4073
|
+
if (!id || !safeId(id)) return null;
|
|
4074
|
+
const ctx = await buildContext(root, config, id);
|
|
4075
|
+
const stage = ctx.meta.stage;
|
|
4076
|
+
if (!safeId(stage)) return null;
|
|
4077
|
+
const file = roomPath(root, id, stage);
|
|
4078
|
+
if (!await pathExists(file)) return null;
|
|
4079
|
+
return { file, text: await readText(file) };
|
|
4080
|
+
} catch {
|
|
4081
|
+
return null;
|
|
4082
|
+
}
|
|
4083
|
+
}
|
|
4084
|
+
async function roomForPrompt(root) {
|
|
4085
|
+
const room = await activeRoom(root);
|
|
4086
|
+
return room?.text.trim() ?? "";
|
|
4087
|
+
}
|
|
4088
|
+
async function postLoopDone(root, bot, text2, cite) {
|
|
4089
|
+
const words = text2.trim();
|
|
4090
|
+
if (!words || words.includes("\n") || words.includes("\r")) return;
|
|
4091
|
+
const room = await activeRoom(root);
|
|
4092
|
+
if (!room) return;
|
|
4093
|
+
let storedCite = "";
|
|
4094
|
+
if (claimsPass(words)) {
|
|
4095
|
+
try {
|
|
4096
|
+
const config = await loadConfig(root);
|
|
4097
|
+
const id = await getActiveChangeId(root, config);
|
|
4098
|
+
if (!id || !cite || !await evidenceSupports(root, id, cite)) return;
|
|
4099
|
+
storedCite = cite;
|
|
4100
|
+
} catch {
|
|
4101
|
+
return;
|
|
4102
|
+
}
|
|
4103
|
+
}
|
|
4104
|
+
const prior = await readEntries(room.file);
|
|
4105
|
+
const round = prior.reduce((max, entry2) => Math.max(max, entry2.round), 1);
|
|
4106
|
+
const sha = await headSha(root);
|
|
4107
|
+
const entry = {
|
|
4108
|
+
ts: nowIso(),
|
|
4109
|
+
from: bot,
|
|
4110
|
+
type: "post",
|
|
4111
|
+
text: words,
|
|
4112
|
+
round: prior.length === 0 ? 1 : round
|
|
4113
|
+
};
|
|
4114
|
+
if (storedCite) entry.cite = storedCite;
|
|
4115
|
+
if (sha) entry.sha = sha;
|
|
4116
|
+
await appendEntry(room.file, entry);
|
|
4117
|
+
}
|
|
4118
|
+
|
|
4119
|
+
// src/team/loop.ts
|
|
4120
|
+
var planStep = z5.object({ step: z5.literal("plan"), items: z5.array(z5.string()) }).strict();
|
|
4121
|
+
var toolStep = z5.object({ step: z5.literal("tool"), name: z5.string().min(1), args: z5.unknown() }).strict();
|
|
4122
|
+
var checkStep = z5.object({
|
|
4123
|
+
step: z5.literal("check"),
|
|
4124
|
+
claim: z5.string().min(1),
|
|
4125
|
+
evidence: z5.array(z5.string()).optional()
|
|
4126
|
+
}).strict();
|
|
4127
|
+
var askStep = z5.object({
|
|
4128
|
+
step: z5.literal("ask"),
|
|
4129
|
+
question: z5.string().min(1),
|
|
4130
|
+
options: z5.array(z5.string()),
|
|
4131
|
+
suggest: z5.string().optional()
|
|
4132
|
+
}).strict();
|
|
4133
|
+
var doneStep = z5.object({
|
|
4134
|
+
step: z5.literal("done"),
|
|
4135
|
+
summary: z5.string(),
|
|
4136
|
+
outputs: z5.array(z5.string()),
|
|
4137
|
+
evidence: z5.array(z5.string()).optional()
|
|
4138
|
+
}).strict();
|
|
4139
|
+
var LoopStepSchema = z5.discriminatedUnion("step", [
|
|
4140
|
+
planStep,
|
|
4141
|
+
toolStep,
|
|
4142
|
+
checkStep,
|
|
4143
|
+
askStep,
|
|
4144
|
+
doneStep
|
|
4145
|
+
]);
|
|
4146
|
+
function ok4(stdout = "") {
|
|
4147
|
+
return { exitCode: 0, stdout };
|
|
4148
|
+
}
|
|
4149
|
+
function fail4(stdout) {
|
|
4150
|
+
return { exitCode: 1, stdout };
|
|
4151
|
+
}
|
|
4152
|
+
async function appendLine(file, value) {
|
|
4153
|
+
await ensureDir(join13(file, ".."));
|
|
4154
|
+
await appendFile4(file, `${JSON.stringify(value)}
|
|
4155
|
+
`, "utf8");
|
|
4156
|
+
}
|
|
4157
|
+
async function runLoop(input) {
|
|
4158
|
+
const max = input.cfg.limits?.loop_max_steps ?? 8;
|
|
4159
|
+
const room = await roomForPrompt(input.root);
|
|
4160
|
+
const base = room ? `${input.brain.prompt}
|
|
4161
|
+
Room:
|
|
4162
|
+
${room}` : input.brain.prompt;
|
|
4163
|
+
let tools = "";
|
|
4164
|
+
const promptFor = () => tools ? `${base}
|
|
4165
|
+
Tools:
|
|
4166
|
+
${tools}` : base;
|
|
4167
|
+
const remember = (text2) => {
|
|
4168
|
+
const piece = text2.trim().slice(-4e3);
|
|
4169
|
+
tools = `${tools}
|
|
4170
|
+
${piece}`.trim().slice(-8e3);
|
|
4171
|
+
};
|
|
4172
|
+
let taken = 0;
|
|
4173
|
+
let copied = false;
|
|
4174
|
+
const copyReviewer = async () => {
|
|
4175
|
+
if (copied) return;
|
|
4176
|
+
copied = true;
|
|
4177
|
+
const lesson = await priorLesson(input.root, input.logName, input.file, input.roleName);
|
|
4178
|
+
if (!lesson) return;
|
|
4179
|
+
await addReviewerLesson(input.root, input.brain.bot.name, lesson);
|
|
4180
|
+
};
|
|
4181
|
+
while (taken < max) {
|
|
4182
|
+
let raw;
|
|
4183
|
+
try {
|
|
4184
|
+
raw = await input.model.complete({ roleId: input.roleName, prompt: promptFor() });
|
|
4185
|
+
} catch {
|
|
4186
|
+
await appendLine(input.file, { type: "bad_call", roleId: input.roleName });
|
|
4187
|
+
return fail4("A loop step is not valid.");
|
|
4188
|
+
}
|
|
4189
|
+
taken += 1;
|
|
4190
|
+
const step = parseStep(raw);
|
|
4191
|
+
if (!step) {
|
|
4192
|
+
await appendLine(input.file, { type: "bad_call", roleId: input.roleName });
|
|
4193
|
+
return fail4("A loop step is not valid.");
|
|
4194
|
+
}
|
|
4195
|
+
if (step.kind === "understand") {
|
|
4196
|
+
await appendLine(input.file, { type: "step", step: "understand" });
|
|
4197
|
+
continue;
|
|
4198
|
+
}
|
|
4199
|
+
if (step.kind === "reflect") {
|
|
4200
|
+
await appendLine(input.file, { type: "step", step: "reflect" });
|
|
4201
|
+
await copyReviewer();
|
|
4202
|
+
continue;
|
|
4203
|
+
}
|
|
4204
|
+
if (step.kind === "schema") {
|
|
4205
|
+
const body = step.step;
|
|
4206
|
+
if (body.step === "plan") {
|
|
4207
|
+
await appendLine(input.file, { type: "step", step: "plan", items: body.items });
|
|
4208
|
+
continue;
|
|
4209
|
+
}
|
|
4210
|
+
if (body.step === "tool") {
|
|
4211
|
+
const ran = await runTool(input.root, input.cfg, input.brain.bot.name, {
|
|
4212
|
+
name: body.name,
|
|
4213
|
+
args: body.args
|
|
4214
|
+
});
|
|
4215
|
+
remember(ran.text);
|
|
4216
|
+
await appendLine(input.file, { type: "step", step: "tool", name: body.name, ok: ran.ok });
|
|
4217
|
+
continue;
|
|
4218
|
+
}
|
|
4219
|
+
if (body.step === "check") {
|
|
4220
|
+
const refusal2 = await passClaimRefusal(input.root, body.claim, body.evidence ?? []);
|
|
4221
|
+
if (refusal2) {
|
|
4222
|
+
remember(refusal2);
|
|
4223
|
+
await appendLine(input.file, { type: "step", step: "refused", text: refusal2 });
|
|
4224
|
+
continue;
|
|
4225
|
+
}
|
|
4226
|
+
await appendLine(input.file, {
|
|
4227
|
+
type: "step",
|
|
4228
|
+
step: "check",
|
|
4229
|
+
claim: body.claim,
|
|
4230
|
+
evidence: body.evidence ?? []
|
|
4231
|
+
});
|
|
4232
|
+
continue;
|
|
4233
|
+
}
|
|
4234
|
+
if (body.step === "ask") return ask(input.root, input.file, body.question, body.options);
|
|
4235
|
+
const cites = body.evidence ?? [];
|
|
4236
|
+
const refusal = await passClaimRefusal(input.root, body.summary, cites);
|
|
4237
|
+
if (refusal) {
|
|
4238
|
+
remember(refusal);
|
|
4239
|
+
await appendLine(input.file, { type: "step", step: "refused", text: refusal });
|
|
4240
|
+
continue;
|
|
4241
|
+
}
|
|
4242
|
+
await appendLine(input.file, { type: "step", step: "done", summary: body.summary });
|
|
4243
|
+
await copyReviewer();
|
|
4244
|
+
await postLoopDone(input.root, input.brain.bot.name, body.summary, cites[0]);
|
|
4245
|
+
return ok4(body.summary);
|
|
4246
|
+
}
|
|
4247
|
+
}
|
|
4248
|
+
return ask(input.root, input.file, `Stopped after ${max} steps.`, ["continue", "stop"]);
|
|
4249
|
+
}
|
|
4250
|
+
async function ask(root, file, question, options) {
|
|
4251
|
+
await appendLine(file, { type: "step", step: "ask", question, options });
|
|
4252
|
+
const note = question.trim();
|
|
4253
|
+
if (note && !note.includes("\n") && !note.includes("\r")) {
|
|
4254
|
+
await recordDecisionRequest(root, { note });
|
|
4255
|
+
}
|
|
4256
|
+
return ok4(question);
|
|
4257
|
+
}
|
|
4258
|
+
function parseStep(raw) {
|
|
4259
|
+
let value;
|
|
4260
|
+
try {
|
|
4261
|
+
value = JSON.parse(raw);
|
|
4262
|
+
} catch {
|
|
4263
|
+
return null;
|
|
4264
|
+
}
|
|
4265
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
4266
|
+
const rec = { ...value };
|
|
4267
|
+
if (rec.step === "understand") return { kind: "understand" };
|
|
4268
|
+
if (rec.step === "reflect") return { kind: "reflect" };
|
|
4269
|
+
if (rec.step === "stop") {
|
|
4270
|
+
rec.step = "done";
|
|
4271
|
+
if (typeof rec.summary !== "string") rec.summary = typeof rec.text === "string" ? rec.text : "";
|
|
4272
|
+
if (!Array.isArray(rec.outputs)) rec.outputs = [];
|
|
4273
|
+
}
|
|
4274
|
+
if (rec.step === "act") {
|
|
4275
|
+
rec.step = "tool";
|
|
4276
|
+
if (typeof rec.name !== "string" || !rec.name) rec.name = "act";
|
|
4277
|
+
if (rec.args === void 0) rec.args = {};
|
|
4278
|
+
}
|
|
4279
|
+
if (rec.step === void 0 && typeof rec.text === "string") {
|
|
4280
|
+
const summary = rec.text;
|
|
4281
|
+
const { text: _ignored, ...rest } = rec;
|
|
4282
|
+
const done = LoopStepSchema.safeParse({ ...rest, step: "done", summary, outputs: [] });
|
|
4283
|
+
if (!done.success) return null;
|
|
4284
|
+
return { kind: "schema", step: done.data };
|
|
4285
|
+
}
|
|
4286
|
+
const parsed = LoopStepSchema.safeParse(rec);
|
|
4287
|
+
if (!parsed.success) return null;
|
|
4288
|
+
return { kind: "schema", step: parsed.data };
|
|
4289
|
+
}
|
|
4290
|
+
async function priorLesson(root, logName, file, roleName) {
|
|
4291
|
+
const dir = conversationDir(root, logName);
|
|
4292
|
+
const names = (await listFiles(dir)).filter((item) => item.endsWith(".jsonl")).sort();
|
|
4293
|
+
const index = names.indexOf(basename(file));
|
|
4294
|
+
if (index <= 0) return null;
|
|
4295
|
+
const prior = join13(dir, names[index - 1]);
|
|
4296
|
+
if (!await pathExists(prior)) return null;
|
|
4297
|
+
let fromChange = null;
|
|
4298
|
+
let fromOther = null;
|
|
4299
|
+
for (const line of (await readText(prior)).split(/\r?\n/)) {
|
|
4300
|
+
if (!line.trim()) continue;
|
|
4301
|
+
let value;
|
|
4302
|
+
try {
|
|
4303
|
+
value = JSON.parse(line);
|
|
4304
|
+
} catch {
|
|
4305
|
+
continue;
|
|
4306
|
+
}
|
|
4307
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) continue;
|
|
4308
|
+
const rec = value;
|
|
4309
|
+
if (rec.type !== "model") continue;
|
|
4310
|
+
const roleId = typeof rec.roleId === "string" ? rec.roleId : "reviewer";
|
|
4311
|
+
const changed = Array.isArray(rec.changed) ? rec.changed.filter((cite) => typeof cite === "string" && cite.trim() !== "") : [];
|
|
4312
|
+
if (changed.length > 0) fromChange = { by: roleId, text: changed.join(", ") };
|
|
4313
|
+
else if (roleId !== roleName && typeof rec.text === "string" && !rec.text.includes("\n")) {
|
|
4314
|
+
fromOther = { by: roleId, text: rec.text };
|
|
4315
|
+
}
|
|
4316
|
+
}
|
|
4317
|
+
return fromChange ?? fromOther;
|
|
4318
|
+
}
|
|
4319
|
+
|
|
4320
|
+
// src/role/run.ts
|
|
4321
|
+
var DRAFT_KINDS = /* @__PURE__ */ new Set(["code", "scenario", "release"]);
|
|
4322
|
+
var FORBIDDEN = /* @__PURE__ */ new Set([
|
|
4323
|
+
"accept_spec",
|
|
4324
|
+
"replace_decision",
|
|
4325
|
+
"write_qa",
|
|
4326
|
+
"generate_release",
|
|
4327
|
+
"sdd_next",
|
|
4328
|
+
"sdd_complete",
|
|
4329
|
+
"create_change"
|
|
4330
|
+
]);
|
|
4331
|
+
var UNAVAILABLE = /* @__PURE__ */ new Set(["commit", "push", "tag", "pull_request", "merge", "deploy"]);
|
|
4332
|
+
var CLOSED = /* @__PURE__ */ new Set(["applied", "denied", "rejected", "unavailable", "stopped", "expired"]);
|
|
4333
|
+
function ok5(stdout = "", runId) {
|
|
4334
|
+
return runId ? { exitCode: 0, stdout, runId } : { exitCode: 0, stdout };
|
|
4335
|
+
}
|
|
4336
|
+
function fail5(stdout, runId) {
|
|
4337
|
+
return runId ? { exitCode: 1, stdout, runId } : { exitCode: 1, stdout };
|
|
4338
|
+
}
|
|
4339
|
+
function linesOf(...parts) {
|
|
4340
|
+
return parts.filter((part) => part.length > 0).join("\n");
|
|
4341
|
+
}
|
|
4342
|
+
function runIdFrom(date) {
|
|
4343
|
+
const iso = date.toISOString();
|
|
4344
|
+
const match = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})/.exec(iso);
|
|
4345
|
+
if (!match) return "unknown";
|
|
4346
|
+
return `${match[1]}${match[2]}${match[3]}T${match[4]}${match[5]}${match[6]}Z`;
|
|
4347
|
+
}
|
|
4348
|
+
function mentionsX(task) {
|
|
4349
|
+
return /@X|x\.com|twitter\.com/.test(task);
|
|
4350
|
+
}
|
|
4351
|
+
function safeChangeId(id) {
|
|
4352
|
+
if (!id || id.length > 200) return false;
|
|
4353
|
+
if (id === "." || id === "..") return false;
|
|
4354
|
+
return !id.includes("/") && !id.includes("\\") && !id.includes("\0");
|
|
4355
|
+
}
|
|
4356
|
+
function safeRunId(id) {
|
|
4357
|
+
if (!id || id.length > 80) return false;
|
|
4358
|
+
if (id === "." || id === "..") return false;
|
|
4359
|
+
return !id.includes("/") && !id.includes("\\") && !id.includes("\0");
|
|
4360
|
+
}
|
|
4361
|
+
function isSafeProductPath(rel) {
|
|
4362
|
+
if (typeof rel !== "string" || rel.length === 0 || rel.includes("\0")) return false;
|
|
4363
|
+
if (isAbsolute4(rel) || rel.startsWith("\\")) return false;
|
|
4364
|
+
if (/^[A-Za-z]:/.test(rel)) return false;
|
|
4365
|
+
const parts = rel.split(/[/\\]/);
|
|
4366
|
+
for (const part of parts) {
|
|
4367
|
+
if (part === "" || part === "." || part === ".." || part === ".active" || part === ".sdd") {
|
|
4368
|
+
return false;
|
|
4369
|
+
}
|
|
4370
|
+
}
|
|
4371
|
+
return parts[parts.length - 1] !== "accepted.md";
|
|
4372
|
+
}
|
|
4373
|
+
function canonicalJson(value) {
|
|
4374
|
+
return JSON.stringify(sortKeys(value));
|
|
4375
|
+
}
|
|
4376
|
+
function sortKeys(value) {
|
|
4377
|
+
if (Array.isArray(value)) return value.map(sortKeys);
|
|
2719
4378
|
if (value && typeof value === "object") {
|
|
2720
4379
|
const rec = value;
|
|
2721
4380
|
const out = {};
|
|
@@ -2811,8 +4470,8 @@ function parseReply(raw) {
|
|
|
2811
4470
|
return { text: rec.text, gaps, changed, action };
|
|
2812
4471
|
}
|
|
2813
4472
|
async function appendJson(file, value) {
|
|
2814
|
-
await ensureDir(
|
|
2815
|
-
await
|
|
4473
|
+
await ensureDir(join14(file, ".."));
|
|
4474
|
+
await appendFile5(file, `${JSON.stringify(value)}
|
|
2816
4475
|
`, "utf8");
|
|
2817
4476
|
}
|
|
2818
4477
|
async function readJsonLines(file) {
|
|
@@ -2828,7 +4487,7 @@ async function readJsonLines(file) {
|
|
|
2828
4487
|
return lines;
|
|
2829
4488
|
}
|
|
2830
4489
|
async function timeoutMinutes(root) {
|
|
2831
|
-
const path =
|
|
4490
|
+
const path = join14(root, ".sdd", "role.yaml");
|
|
2832
4491
|
if (!await pathExists(path)) return 10;
|
|
2833
4492
|
let data;
|
|
2834
4493
|
try {
|
|
@@ -2865,30 +4524,10 @@ function authorPrompt(role, task, kind, section) {
|
|
|
2865
4524
|
}
|
|
2866
4525
|
return rows.join("\n");
|
|
2867
4526
|
}
|
|
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
4527
|
async function acceptedFile(root, changeId) {
|
|
2889
|
-
if (!await pathExists(configPath(root))) return
|
|
4528
|
+
if (!await pathExists(configPath(root))) return join14(root, "changes", changeId, "accepted.md");
|
|
2890
4529
|
const config = await loadConfig(root);
|
|
2891
|
-
return
|
|
4530
|
+
return join14(changesDir(root, config), changeId, "accepted.md");
|
|
2892
4531
|
}
|
|
2893
4532
|
async function callModel(model, file, roleId, prompt, pass) {
|
|
2894
4533
|
let raw;
|
|
@@ -2915,12 +4554,16 @@ async function readRule(root, roleName) {
|
|
|
2915
4554
|
const name = typeof rec.tool === "string" ? rec.tool : typeof rec.name === "string" ? rec.name : "";
|
|
2916
4555
|
if (!name) return null;
|
|
2917
4556
|
if (!rec.inputs || typeof rec.inputs !== "object" || Array.isArray(rec.inputs)) return null;
|
|
4557
|
+
const stored = typeof rec.sha === "string" ? rec.sha : "";
|
|
4558
|
+
const head = await getHeadSha(root);
|
|
4559
|
+
if (head && stored !== head) return null;
|
|
2918
4560
|
return { name, inputs: rec.inputs };
|
|
2919
4561
|
}
|
|
2920
4562
|
async function saveRule(root, roleName, rule) {
|
|
2921
4563
|
const path = allowFilePath(root);
|
|
2922
4564
|
const existing = await readAllowFile(root) ?? {};
|
|
2923
|
-
|
|
4565
|
+
const head = await getHeadSha(root);
|
|
4566
|
+
existing[roleName] = head ? { tool: rule.name, inputs: rule.inputs, sha: head } : { tool: rule.name, inputs: rule.inputs };
|
|
2924
4567
|
await writeText(path, `${JSON.stringify(existing, null, 2)}
|
|
2925
4568
|
`);
|
|
2926
4569
|
}
|
|
@@ -2939,7 +4582,7 @@ async function applyAction(root, file, roleName, action, mode) {
|
|
|
2939
4582
|
await appendJson(file, { type: "rejected" });
|
|
2940
4583
|
return { exitCode: 1, stdout: ROLE_TEXT.cannot, applied: false };
|
|
2941
4584
|
}
|
|
2942
|
-
await writeText(
|
|
4585
|
+
await writeText(join14(root, ...path.split(/[/\\]/)), contents);
|
|
2943
4586
|
if (mode === "always")
|
|
2944
4587
|
await saveRule(root, roleName, { name: action.name, inputs: action.inputs });
|
|
2945
4588
|
await appendJson(file, { type: "applied", name: "write_file" });
|
|
@@ -2947,16 +4590,16 @@ async function applyAction(root, file, roleName, action, mode) {
|
|
|
2947
4590
|
}
|
|
2948
4591
|
async function finishRun(root, file, role, final, minutes, now, runId) {
|
|
2949
4592
|
const action = final.action;
|
|
2950
|
-
if (!action) return
|
|
4593
|
+
if (!action) return ok5(final.text, runId);
|
|
2951
4594
|
if (FORBIDDEN.has(action.name) || !UNAVAILABLE.has(action.name) && action.name !== "write_file") {
|
|
2952
4595
|
await appendJson(file, { type: "rejected" });
|
|
2953
|
-
return
|
|
4596
|
+
return fail5(linesOf(final.text, ROLE_TEXT.cannot), runId);
|
|
2954
4597
|
}
|
|
2955
4598
|
const rule = await readRule(root, role.name);
|
|
2956
4599
|
if (!role.askFirst && rule && rule.name === action.name && sameInputs(rule.inputs, action.inputs)) {
|
|
2957
4600
|
const applied = await applyAction(root, file, role.name, action, "always");
|
|
2958
4601
|
const stdout = applied.applied ? final.text : linesOf(final.text, applied.stdout);
|
|
2959
|
-
return applied.exitCode === 0 ?
|
|
4602
|
+
return applied.exitCode === 0 ? ok5(stdout, runId) : fail5(stdout, runId);
|
|
2960
4603
|
}
|
|
2961
4604
|
const expiresAt = new Date(now.getTime() + minutes * 6e4).toISOString();
|
|
2962
4605
|
await appendJson(file, {
|
|
@@ -2964,12 +4607,12 @@ async function finishRun(root, file, role, final, minutes, now, runId) {
|
|
|
2964
4607
|
action: { name: action.name, words: action.words, inputs: action.inputs },
|
|
2965
4608
|
expiresAt
|
|
2966
4609
|
});
|
|
2967
|
-
if (role.askFirst) return
|
|
2968
|
-
return
|
|
4610
|
+
if (role.askFirst) return ok5(linesOf(action.words, ROLE_TEXT.askFirst), runId);
|
|
4611
|
+
return ok5(linesOf(action.words, ROLE_TEXT.choice), runId);
|
|
2969
4612
|
}
|
|
2970
4613
|
async function settleRoleReply(root, file, role, reply, now, runId) {
|
|
2971
4614
|
const minutes = await timeoutMinutes(root);
|
|
2972
|
-
if (minutes === "invalid") return
|
|
4615
|
+
if (minutes === "invalid") return fail5(ROLE_TEXT.badYaml, runId);
|
|
2973
4616
|
return finishRun(
|
|
2974
4617
|
root,
|
|
2975
4618
|
file,
|
|
@@ -2984,54 +4627,69 @@ async function latestRunFile(root, name) {
|
|
|
2984
4627
|
const dir = conversationDir(root, name);
|
|
2985
4628
|
const files = (await listFiles(dir)).filter((file) => file.endsWith(".jsonl")).sort();
|
|
2986
4629
|
const last = files[files.length - 1];
|
|
2987
|
-
return last ?
|
|
4630
|
+
return last ? join14(dir, last) : null;
|
|
2988
4631
|
}
|
|
2989
4632
|
async function runRole(root, input) {
|
|
2990
4633
|
const name = canonicalRoleName(input.name);
|
|
2991
|
-
if (!name) return
|
|
4634
|
+
if (!name) return fail5(ROLE_TEXT.badName);
|
|
2992
4635
|
if (input.stop) {
|
|
2993
|
-
if (!await readRole(root, name)) return
|
|
4636
|
+
if (!await readRole(root, name)) return fail5(ROLE_TEXT.notFound);
|
|
2994
4637
|
const latest = await latestRunFile(root, name);
|
|
2995
|
-
if (!latest) return
|
|
4638
|
+
if (!latest) return fail5(ROLE_TEXT.runNotFound);
|
|
2996
4639
|
await appendJson(latest, { type: "stopped" });
|
|
2997
|
-
return
|
|
4640
|
+
return ok5();
|
|
4641
|
+
}
|
|
4642
|
+
let roleName = name;
|
|
4643
|
+
let team = null;
|
|
4644
|
+
let botName = "";
|
|
4645
|
+
if (await pathExists(join14(root, ".sdd", "team.yaml"))) {
|
|
4646
|
+
const loaded = await readTeamConfig(root);
|
|
4647
|
+
if ("error" in loaded) return fail5(loaded.error);
|
|
4648
|
+
const resolved = resolveBot(loaded.team, name);
|
|
4649
|
+
if ("error" in resolved) {
|
|
4650
|
+
if (resolved.error === "Name the bot.") return fail5(resolved.error);
|
|
4651
|
+
} else {
|
|
4652
|
+
team = loaded.team;
|
|
4653
|
+
botName = resolved.name;
|
|
4654
|
+
roleName = resolved.role;
|
|
4655
|
+
}
|
|
2998
4656
|
}
|
|
2999
|
-
const role = await readRole(root,
|
|
3000
|
-
if (!role) return
|
|
3001
|
-
if (!role.job.trim()) return
|
|
4657
|
+
const role = await readRole(root, roleName);
|
|
4658
|
+
if (!role) return fail5(ROLE_TEXT.notFound);
|
|
4659
|
+
if (!role.job.trim()) return fail5(ROLE_TEXT.needsJob);
|
|
3002
4660
|
const task = input.task ?? "";
|
|
3003
4661
|
let withName = "";
|
|
3004
4662
|
if (input.withRole !== void 0 && input.withRole.trim() !== "") {
|
|
3005
4663
|
const canonical = canonicalRoleName(input.withRole);
|
|
3006
|
-
if (!canonical) return
|
|
3007
|
-
if (canonical === name) return
|
|
4664
|
+
if (!canonical) return fail5(ROLE_TEXT.badName);
|
|
4665
|
+
if (canonical === name) return fail5(ROLE_TEXT.self);
|
|
3008
4666
|
withName = canonical;
|
|
3009
4667
|
}
|
|
3010
|
-
if (mentionsX(task)) return
|
|
4668
|
+
if (mentionsX(task)) return fail5(ROLE_TEXT.x);
|
|
3011
4669
|
const kind = input.kind?.trim() || "reading";
|
|
3012
4670
|
let section;
|
|
3013
4671
|
if (kind !== "reading") {
|
|
3014
|
-
if (!DRAFT_KINDS.has(kind)) return
|
|
4672
|
+
if (!DRAFT_KINDS.has(kind)) return fail5(ROLE_TEXT.needsSection);
|
|
3015
4673
|
const changeId = input.change?.trim() ?? "";
|
|
3016
4674
|
const sectionId = input.section?.trim() ?? "";
|
|
3017
|
-
if (!safeChangeId(changeId) || !sectionId) return
|
|
4675
|
+
if (!safeChangeId(changeId) || !sectionId) return fail5(ROLE_TEXT.needsSection);
|
|
3018
4676
|
const acceptedPath = await acceptedFile(root, changeId);
|
|
3019
|
-
if (!await pathExists(acceptedPath)) return
|
|
4677
|
+
if (!await pathExists(acceptedPath)) return fail5(ROLE_TEXT.needsSection);
|
|
3020
4678
|
const parsed = parseSection(await readText(acceptedPath), sectionId);
|
|
3021
|
-
if (!parsed) return
|
|
4679
|
+
if (!parsed) return fail5(ROLE_TEXT.needsSection);
|
|
3022
4680
|
section = parsed;
|
|
3023
4681
|
}
|
|
3024
4682
|
let critic = null;
|
|
3025
4683
|
if (withName && section) {
|
|
3026
4684
|
critic = await readRole(root, withName);
|
|
3027
|
-
if (!critic) return
|
|
4685
|
+
if (!critic) return fail5(ROLE_TEXT.notFound);
|
|
3028
4686
|
}
|
|
3029
4687
|
const minutes = await timeoutMinutes(root);
|
|
3030
|
-
if (minutes === "invalid") return
|
|
3031
|
-
if (!input.model) return
|
|
4688
|
+
if (minutes === "invalid") return fail5(ROLE_TEXT.badYaml);
|
|
4689
|
+
if (!input.model) return fail5(ROLE_TEXT.noModel);
|
|
3032
4690
|
const now = (input.now ?? (() => /* @__PURE__ */ new Date()))();
|
|
3033
4691
|
const runId = runIdFrom(now);
|
|
3034
|
-
const file =
|
|
4692
|
+
const file = join14(conversationDir(root, name), `${runId}.jsonl`);
|
|
3035
4693
|
if (section) {
|
|
3036
4694
|
await appendJson(file, {
|
|
3037
4695
|
type: "checks",
|
|
@@ -3040,6 +4698,27 @@ async function runRole(root, input) {
|
|
|
3040
4698
|
decision: section.decision
|
|
3041
4699
|
});
|
|
3042
4700
|
}
|
|
4701
|
+
if (team && botName) {
|
|
4702
|
+
let budget = 12e3;
|
|
4703
|
+
try {
|
|
4704
|
+
const search = await loadSearchConfig(root);
|
|
4705
|
+
budget = budgetForStage(search, void 0);
|
|
4706
|
+
} catch (error) {
|
|
4707
|
+
if (error instanceof SearchConfigError) return fail5("search.yaml is invalid", runId);
|
|
4708
|
+
throw error;
|
|
4709
|
+
}
|
|
4710
|
+
const brain = await loadBrain(root, team, botName, { text: task }, budget);
|
|
4711
|
+
if ("error" in brain) return fail5(brain.error, runId);
|
|
4712
|
+
return runLoop({
|
|
4713
|
+
root,
|
|
4714
|
+
cfg: team,
|
|
4715
|
+
brain,
|
|
4716
|
+
model: input.model,
|
|
4717
|
+
file,
|
|
4718
|
+
roleName: role.name,
|
|
4719
|
+
logName: name
|
|
4720
|
+
});
|
|
4721
|
+
}
|
|
3043
4722
|
const draft = await callModel(
|
|
3044
4723
|
input.model,
|
|
3045
4724
|
file,
|
|
@@ -3047,7 +4726,7 @@ async function runRole(root, input) {
|
|
|
3047
4726
|
authorPrompt(role, task, kind, section),
|
|
3048
4727
|
1
|
|
3049
4728
|
);
|
|
3050
|
-
if (!draft) return
|
|
4729
|
+
if (!draft) return fail5("", runId);
|
|
3051
4730
|
let final = draft;
|
|
3052
4731
|
if (critic && section) {
|
|
3053
4732
|
const accepted = section;
|
|
@@ -3058,10 +4737,10 @@ async function runRole(root, input) {
|
|
|
3058
4737
|
critiquePrompt(critic, accepted, draft.text),
|
|
3059
4738
|
2
|
|
3060
4739
|
);
|
|
3061
|
-
if (!critique) return
|
|
4740
|
+
if (!critique) return fail5(draft.text, runId);
|
|
3062
4741
|
if (critique.gaps.some((gap) => !citeOk(gap.cite, accepted))) {
|
|
3063
4742
|
await appendJson(file, { type: "bad_call", roleId: critic.name });
|
|
3064
|
-
return
|
|
4743
|
+
return fail5(draft.text, runId);
|
|
3065
4744
|
}
|
|
3066
4745
|
if (critique.gaps.length > 0) {
|
|
3067
4746
|
const revision = await callModel(
|
|
@@ -3071,11 +4750,11 @@ async function runRole(root, input) {
|
|
|
3071
4750
|
revisionPrompt(role, draft.text, critique.gaps),
|
|
3072
4751
|
3
|
|
3073
4752
|
);
|
|
3074
|
-
if (!revision) return
|
|
4753
|
+
if (!revision) return fail5(draft.text, runId);
|
|
3075
4754
|
const cites = critique.gaps.map((gap) => gap.cite);
|
|
3076
4755
|
if (revision.changed === null || !sameCiteSet(revision.changed, cites)) {
|
|
3077
4756
|
await appendJson(file, { type: "bad_call", roleId: role.name });
|
|
3078
|
-
return
|
|
4757
|
+
return fail5(draft.text, runId);
|
|
3079
4758
|
}
|
|
3080
4759
|
final = revision;
|
|
3081
4760
|
}
|
|
@@ -3114,7 +4793,7 @@ async function findRun(root, runId) {
|
|
|
3114
4793
|
if (!safeRunId(runId)) return null;
|
|
3115
4794
|
const hits = [];
|
|
3116
4795
|
for (const roleName of await listConversationRoles(root)) {
|
|
3117
|
-
const file =
|
|
4796
|
+
const file = join14(conversationDir(root, roleName), `${runId}.jsonl`);
|
|
3118
4797
|
if (await pathExists(file)) hits.push({ file, roleName });
|
|
3119
4798
|
}
|
|
3120
4799
|
if (hits.length === 1) return hits[0];
|
|
@@ -3126,33 +4805,482 @@ async function findRun(root, runId) {
|
|
|
3126
4805
|
}
|
|
3127
4806
|
async function allowRole(root, input) {
|
|
3128
4807
|
const found = await findRun(root, input.runId);
|
|
3129
|
-
if (!found) return
|
|
4808
|
+
if (!found) return fail5(ROLE_TEXT.runNotFound);
|
|
3130
4809
|
const pending = openPending(await readJsonLines(found.file));
|
|
3131
|
-
if (!pending) return
|
|
4810
|
+
if (!pending) return fail5(ROLE_TEXT.runNotFound);
|
|
3132
4811
|
const now = (input.now ?? (() => /* @__PURE__ */ new Date()))();
|
|
3133
4812
|
const expires = Date.parse(pending.expiresAt);
|
|
3134
4813
|
if (!Number.isFinite(expires) || now.getTime() >= expires) {
|
|
3135
4814
|
await appendJson(found.file, { type: "expired" });
|
|
3136
|
-
return
|
|
4815
|
+
return fail5(ROLE_TEXT.expired);
|
|
3137
4816
|
}
|
|
3138
4817
|
if (input.mode === "deny") {
|
|
3139
4818
|
await appendJson(found.file, { type: "denied" });
|
|
3140
|
-
return
|
|
4819
|
+
return ok5("", input.runId);
|
|
3141
4820
|
}
|
|
3142
4821
|
const role = await readRole(root, found.roleName);
|
|
3143
|
-
if (!role) return
|
|
3144
|
-
if (role.askFirst) return
|
|
4822
|
+
if (!role) return fail5(ROLE_TEXT.notFound);
|
|
4823
|
+
if (role.askFirst) return ok5(ROLE_TEXT.askFirst, input.runId);
|
|
3145
4824
|
const applied = await applyAction(root, found.file, found.roleName, pending, input.mode);
|
|
3146
|
-
return applied.exitCode === 0 ?
|
|
4825
|
+
return applied.exitCode === 0 ? ok5(applied.stdout, input.runId) : fail5(applied.stdout, input.runId);
|
|
4826
|
+
}
|
|
4827
|
+
|
|
4828
|
+
// src/role/routing.ts
|
|
4829
|
+
var matcher = z6.string().trim().min(1);
|
|
4830
|
+
var RoutingRuleSchema = z6.object({
|
|
4831
|
+
id: matcher,
|
|
4832
|
+
role: matcher,
|
|
4833
|
+
stage: matcher.optional(),
|
|
4834
|
+
artifact: matcher.optional(),
|
|
4835
|
+
owns: matcher.optional(),
|
|
4836
|
+
skill: matcher.optional()
|
|
4837
|
+
}).strict().superRefine((rule, ctx) => {
|
|
4838
|
+
if (!rule.stage && !rule.artifact && !rule.owns && !rule.skill) {
|
|
4839
|
+
ctx.addIssue({
|
|
4840
|
+
code: "custom",
|
|
4841
|
+
message: "needs stage, artifact, owns, or skill"
|
|
4842
|
+
});
|
|
4843
|
+
}
|
|
4844
|
+
});
|
|
4845
|
+
var RoutingSchema = z6.object({
|
|
4846
|
+
rules: z6.array(RoutingRuleSchema)
|
|
4847
|
+
}).strict().superRefine((file, ctx) => {
|
|
4848
|
+
const seen = /* @__PURE__ */ new Set();
|
|
4849
|
+
for (const [index, rule] of file.rules.entries()) {
|
|
4850
|
+
if (seen.has(rule.id)) {
|
|
4851
|
+
ctx.addIssue({
|
|
4852
|
+
code: "custom",
|
|
4853
|
+
path: ["rules", index, "id"],
|
|
4854
|
+
message: "is repeated"
|
|
4855
|
+
});
|
|
4856
|
+
}
|
|
4857
|
+
seen.add(rule.id);
|
|
4858
|
+
}
|
|
4859
|
+
});
|
|
4860
|
+
function ok6(stdout = "") {
|
|
4861
|
+
return { exitCode: 0, stdout };
|
|
4862
|
+
}
|
|
4863
|
+
function fail6(stdout) {
|
|
4864
|
+
return { exitCode: 1, stdout };
|
|
4865
|
+
}
|
|
4866
|
+
function formatRoutingError(error) {
|
|
4867
|
+
const issue = error.issues[0];
|
|
4868
|
+
const path = issue?.path.join(".") || "routing";
|
|
4869
|
+
const message = issue?.message ?? "invalid";
|
|
4870
|
+
return `routing.yaml is invalid: ${path} ${message}`;
|
|
4871
|
+
}
|
|
4872
|
+
function text(value) {
|
|
4873
|
+
return typeof value === "string" && value.trim() ? value.trim() : void 0;
|
|
4874
|
+
}
|
|
4875
|
+
function cleanItem(input) {
|
|
4876
|
+
const item = {};
|
|
4877
|
+
const id = input.id?.trim();
|
|
4878
|
+
const stage = input.stage?.trim();
|
|
4879
|
+
const artifact = input.artifact?.trim();
|
|
4880
|
+
const skill = input.skill?.trim();
|
|
4881
|
+
const bot = input.bot?.trim();
|
|
4882
|
+
if (id) item.id = id;
|
|
4883
|
+
if (stage) item.stage = stage;
|
|
4884
|
+
if (artifact) item.artifact = artifact;
|
|
4885
|
+
if (skill) item.skill = skill;
|
|
4886
|
+
if (bot) item.bot = bot;
|
|
4887
|
+
return item;
|
|
4888
|
+
}
|
|
4889
|
+
function inboxFile(root, name) {
|
|
4890
|
+
const canonical = canonicalRoleName(name);
|
|
4891
|
+
if (!canonical) return null;
|
|
4892
|
+
return join15(root, ".sdd", "agents", canonical, "inbox.jsonl");
|
|
4893
|
+
}
|
|
4894
|
+
async function writeHandoffInboxes(root, record) {
|
|
4895
|
+
const seen = /* @__PURE__ */ new Set();
|
|
4896
|
+
for (const name of [record.from, record.to]) {
|
|
4897
|
+
if (!name || seen.has(name)) continue;
|
|
4898
|
+
seen.add(name);
|
|
4899
|
+
const file = inboxFile(root, name);
|
|
4900
|
+
if (!file) continue;
|
|
4901
|
+
await appendJson(file, record);
|
|
4902
|
+
}
|
|
4903
|
+
}
|
|
4904
|
+
async function readRouting(root) {
|
|
4905
|
+
const path = join15(root, ".sdd", "routing.yaml");
|
|
4906
|
+
if (!await pathExists(path)) return { rules: [] };
|
|
4907
|
+
let raw;
|
|
4908
|
+
try {
|
|
4909
|
+
raw = parseYaml2(await readText(path));
|
|
4910
|
+
} catch {
|
|
4911
|
+
return { error: "routing.yaml is invalid: routing could not be parsed" };
|
|
4912
|
+
}
|
|
4913
|
+
const parsed = RoutingSchema.safeParse(raw);
|
|
4914
|
+
if (!parsed.success) return { error: formatRoutingError(parsed.error) };
|
|
4915
|
+
return parsed.data;
|
|
4916
|
+
}
|
|
4917
|
+
async function loadRoles(root) {
|
|
4918
|
+
const dir = join15(root, ".sdd", "roles");
|
|
4919
|
+
const files = (await listFiles(dir)).filter((file) => file.endsWith(".md")).sort();
|
|
4920
|
+
const roles = [];
|
|
4921
|
+
for (const file of files) {
|
|
4922
|
+
const name = file.slice(0, -".md".length);
|
|
4923
|
+
let parsed;
|
|
4924
|
+
try {
|
|
4925
|
+
parsed = parseRoleDocument(name, await readText(join15(dir, file)));
|
|
4926
|
+
} catch {
|
|
4927
|
+
return { error: `Role file is invalid: ${name}.` };
|
|
4928
|
+
}
|
|
4929
|
+
if (!parsed) return { error: `Role file is invalid: ${name}.` };
|
|
4930
|
+
roles.push(parsed);
|
|
4931
|
+
}
|
|
4932
|
+
return roles;
|
|
4933
|
+
}
|
|
4934
|
+
function ruleMatches(rule, item) {
|
|
4935
|
+
if (rule.stage && rule.stage !== item.stage) return false;
|
|
4936
|
+
if (rule.artifact && rule.artifact !== item.artifact) return false;
|
|
4937
|
+
if (rule.owns && rule.owns !== item.artifact) return false;
|
|
4938
|
+
if (rule.skill && rule.skill !== item.skill) return false;
|
|
4939
|
+
return true;
|
|
4940
|
+
}
|
|
4941
|
+
function listedOwners(roles, item) {
|
|
4942
|
+
if (item.artifact) {
|
|
4943
|
+
const names = roles.filter((role) => role.owns.includes(item.artifact ?? "")).map((role) => role.name);
|
|
4944
|
+
if (names.length > 0) return { rule: "owns", names };
|
|
4945
|
+
}
|
|
4946
|
+
if (item.skill) {
|
|
4947
|
+
const names = roles.filter((role) => role.skills.includes(item.skill ?? "")).map((role) => role.name);
|
|
4948
|
+
if (names.length > 0) return { rule: "skill", names };
|
|
4949
|
+
}
|
|
4950
|
+
return null;
|
|
4951
|
+
}
|
|
4952
|
+
async function stageOwner(root, stageId) {
|
|
4953
|
+
let config;
|
|
4954
|
+
try {
|
|
4955
|
+
config = await loadConfig(root);
|
|
4956
|
+
} catch {
|
|
4957
|
+
return void 0;
|
|
4958
|
+
}
|
|
4959
|
+
const changeId = await getActiveChangeId(root, config);
|
|
4960
|
+
if (!changeId) return void 0;
|
|
4961
|
+
try {
|
|
4962
|
+
const context = await buildContext(root, config, changeId);
|
|
4963
|
+
return context.workflow.stages.find((stage) => stage.id === stageId)?.owner;
|
|
4964
|
+
} catch {
|
|
4965
|
+
return { error: "Change is unreadable." };
|
|
4966
|
+
}
|
|
4967
|
+
}
|
|
4968
|
+
async function matchRoute(root, input) {
|
|
4969
|
+
const item = cleanItem(input);
|
|
4970
|
+
const routing = await readRouting(root);
|
|
4971
|
+
if ("error" in routing) return { ok: false, message: routing.error };
|
|
4972
|
+
const rule = routing.rules.find((candidate) => ruleMatches(candidate, item));
|
|
4973
|
+
if (rule) {
|
|
4974
|
+
return {
|
|
4975
|
+
ok: true,
|
|
4976
|
+
match: { kind: "role", role: rule.role, reason: `Rule ${rule.id} matches.`, rule: rule.id }
|
|
4977
|
+
};
|
|
4978
|
+
}
|
|
4979
|
+
const roles = await loadRoles(root);
|
|
4980
|
+
if ("error" in roles) return { ok: false, message: roles.error };
|
|
4981
|
+
const listed = listedOwners(roles, item);
|
|
4982
|
+
if (listed && listed.names.length > 1) {
|
|
4983
|
+
return { ok: true, match: { kind: "ask", reason: "Two roles own this." } };
|
|
4984
|
+
}
|
|
4985
|
+
if (listed && listed.names.length === 1) {
|
|
4986
|
+
const role = listed.names[0] ?? "";
|
|
4987
|
+
if (listed.rule === "owns") {
|
|
4988
|
+
return {
|
|
4989
|
+
ok: true,
|
|
4990
|
+
match: {
|
|
4991
|
+
kind: "role",
|
|
4992
|
+
role,
|
|
4993
|
+
reason: `One role owns ${item.artifact}.`,
|
|
4994
|
+
rule: "owns"
|
|
4995
|
+
}
|
|
4996
|
+
};
|
|
4997
|
+
}
|
|
4998
|
+
return {
|
|
4999
|
+
ok: true,
|
|
5000
|
+
match: {
|
|
5001
|
+
kind: "role",
|
|
5002
|
+
role,
|
|
5003
|
+
reason: `One role has skill ${item.skill}.`,
|
|
5004
|
+
rule: "skill"
|
|
5005
|
+
}
|
|
5006
|
+
};
|
|
5007
|
+
}
|
|
5008
|
+
if (item.stage) {
|
|
5009
|
+
const owner = await stageOwner(root, item.stage);
|
|
5010
|
+
if (owner && typeof owner === "object") return { ok: false, message: owner.error };
|
|
5011
|
+
if (typeof owner === "string" && roles.some((role) => role.name === owner)) {
|
|
5012
|
+
return {
|
|
5013
|
+
ok: true,
|
|
5014
|
+
match: {
|
|
5015
|
+
kind: "role",
|
|
5016
|
+
role: owner,
|
|
5017
|
+
reason: `Stage ${item.stage} is owned by ${owner}.`,
|
|
5018
|
+
rule: "stage-owner"
|
|
5019
|
+
}
|
|
5020
|
+
};
|
|
5021
|
+
}
|
|
5022
|
+
}
|
|
5023
|
+
return { ok: true, match: { kind: "ask", reason: "No rule matched." } };
|
|
5024
|
+
}
|
|
5025
|
+
async function activeStageItem(root) {
|
|
5026
|
+
let config;
|
|
5027
|
+
try {
|
|
5028
|
+
config = await loadConfig(root);
|
|
5029
|
+
} catch {
|
|
5030
|
+
return { error: "No active change." };
|
|
5031
|
+
}
|
|
5032
|
+
const changeId = await getActiveChangeId(root, config);
|
|
5033
|
+
if (!changeId) return { error: "No active change." };
|
|
5034
|
+
try {
|
|
5035
|
+
const context = await buildContext(root, config, changeId);
|
|
5036
|
+
const stage = context.workflow.stages.find((item2) => item2.id === context.meta.stage) ?? context.active[0];
|
|
5037
|
+
if (!stage) return { error: "No active change." };
|
|
5038
|
+
const artifact = stage.artifacts.find((item2) => !item2.path.endsWith("/"))?.path;
|
|
5039
|
+
const item = { stage: stage.id };
|
|
5040
|
+
if (artifact) item.artifact = artifact;
|
|
5041
|
+
return item;
|
|
5042
|
+
} catch {
|
|
5043
|
+
return { error: "Change is unreadable." };
|
|
5044
|
+
}
|
|
5045
|
+
}
|
|
5046
|
+
async function prepareItem(root, input) {
|
|
5047
|
+
const item = cleanItem(input);
|
|
5048
|
+
if (item.stage || item.artifact || item.skill) return item;
|
|
5049
|
+
const active = await activeStageItem(root);
|
|
5050
|
+
if ("error" in active) return active;
|
|
5051
|
+
const merged = { ...active };
|
|
5052
|
+
if (item.id) merged.id = item.id;
|
|
5053
|
+
if (item.bot) merged.bot = item.bot;
|
|
5054
|
+
return merged;
|
|
5055
|
+
}
|
|
5056
|
+
function formatHit(match) {
|
|
5057
|
+
if (match.kind === "ask") return `Ask the human.
|
|
5058
|
+
${match.reason}`;
|
|
5059
|
+
return `role ${match.role}
|
|
5060
|
+
reason ${match.reason}
|
|
5061
|
+
rule ${match.rule}`;
|
|
5062
|
+
}
|
|
5063
|
+
function routeState(item) {
|
|
5064
|
+
return JSON.stringify({
|
|
5065
|
+
stage: item.stage ?? null,
|
|
5066
|
+
artifact: item.artifact ?? null,
|
|
5067
|
+
skill: item.skill ?? null,
|
|
5068
|
+
id: item.id ?? null
|
|
5069
|
+
});
|
|
5070
|
+
}
|
|
5071
|
+
async function routeMetrics(root) {
|
|
5072
|
+
try {
|
|
5073
|
+
const config = await loadConfig(root);
|
|
5074
|
+
const changeId = await getActiveChangeId(root, config);
|
|
5075
|
+
if (!changeId) return void 0;
|
|
5076
|
+
return { projectRoot: root, config, changeId };
|
|
5077
|
+
} catch {
|
|
5078
|
+
return void 0;
|
|
5079
|
+
}
|
|
5080
|
+
}
|
|
5081
|
+
async function routeRole(root, input, deps) {
|
|
5082
|
+
const item = await prepareItem(root, input);
|
|
5083
|
+
if ("error" in item) return fail6(item.error);
|
|
5084
|
+
const matched = await matchRoute(root, item);
|
|
5085
|
+
if (!matched.ok) return fail6(matched.message);
|
|
5086
|
+
if (matched.match.kind !== "ask" || matched.match.reason !== "No rule matched.") {
|
|
5087
|
+
return ok6(formatHit(matched.match));
|
|
5088
|
+
}
|
|
5089
|
+
const loaded = await loadModels(root);
|
|
5090
|
+
if (loaded === null) return ok6(formatHit(matched.match));
|
|
5091
|
+
if ("error" in loaded) return fail6(loaded.error);
|
|
5092
|
+
const roles = await loadRoles(root);
|
|
5093
|
+
if ("error" in roles) return fail6(roles.error);
|
|
5094
|
+
const names = roles.map((role) => role.name).sort((left, right) => left.localeCompare(right));
|
|
5095
|
+
const options = names.includes("human") ? names : [...names, "human"];
|
|
5096
|
+
const decision = await decide({
|
|
5097
|
+
models: loaded.config,
|
|
5098
|
+
ctx: { workType: "route", stage: item.stage, bot: item.bot },
|
|
5099
|
+
state: routeState(item),
|
|
5100
|
+
question: "No rule matched.",
|
|
5101
|
+
options,
|
|
5102
|
+
env: deps?.env ?? {},
|
|
5103
|
+
fetch: deps?.fetch,
|
|
5104
|
+
runCommand: deps?.runCommand,
|
|
5105
|
+
now: deps?.now,
|
|
5106
|
+
metrics: await routeMetrics(root)
|
|
5107
|
+
});
|
|
5108
|
+
if (decision.pick !== "human" && names.includes(decision.pick)) {
|
|
5109
|
+
const id = decision.choice?.id ?? "model";
|
|
5110
|
+
return ok6(`role ${decision.pick}
|
|
5111
|
+
reason Model ${id} picked ${decision.pick}.`);
|
|
5112
|
+
}
|
|
5113
|
+
return ok6("Ask the human.\nNo rule matched.");
|
|
5114
|
+
}
|
|
5115
|
+
async function readInbox(root, name) {
|
|
5116
|
+
const file = inboxFile(root, name);
|
|
5117
|
+
if (!file) return { error: ROLE_TEXT.badName };
|
|
5118
|
+
if (!await pathExists(file)) return [];
|
|
5119
|
+
let raw;
|
|
5120
|
+
try {
|
|
5121
|
+
raw = await readText(file);
|
|
5122
|
+
} catch {
|
|
5123
|
+
return { error: "inbox.jsonl is invalid" };
|
|
5124
|
+
}
|
|
5125
|
+
const entries = [];
|
|
5126
|
+
for (const line of raw.split(/\r?\n/)) {
|
|
5127
|
+
if (!line.trim()) continue;
|
|
5128
|
+
let value;
|
|
5129
|
+
try {
|
|
5130
|
+
value = JSON.parse(line);
|
|
5131
|
+
} catch {
|
|
5132
|
+
return { error: "inbox.jsonl is invalid" };
|
|
5133
|
+
}
|
|
5134
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
5135
|
+
return { error: "inbox.jsonl is invalid" };
|
|
5136
|
+
}
|
|
5137
|
+
const record = value;
|
|
5138
|
+
if (record.type === "item") {
|
|
5139
|
+
const id = text(record.id);
|
|
5140
|
+
if (!id || record.status !== "active" && record.status !== "done") {
|
|
5141
|
+
return { error: "inbox.jsonl is invalid" };
|
|
5142
|
+
}
|
|
5143
|
+
entries.push({
|
|
5144
|
+
type: "item",
|
|
5145
|
+
id,
|
|
5146
|
+
status: record.status,
|
|
5147
|
+
stage: text(record.stage),
|
|
5148
|
+
artifact: text(record.artifact)
|
|
5149
|
+
});
|
|
5150
|
+
continue;
|
|
5151
|
+
}
|
|
5152
|
+
if (record.type === "handoff") {
|
|
5153
|
+
const from = text(record.from);
|
|
5154
|
+
const to = text(record.to);
|
|
5155
|
+
if (!from || !to) return { error: "inbox.jsonl is invalid" };
|
|
5156
|
+
entries.push({ type: "handoff", from, to });
|
|
5157
|
+
continue;
|
|
5158
|
+
}
|
|
5159
|
+
if (typeof record.type === "string" && record.type.trim()) {
|
|
5160
|
+
entries.push({ type: "other", label: record.type.trim() });
|
|
5161
|
+
continue;
|
|
5162
|
+
}
|
|
5163
|
+
return { error: "inbox.jsonl is invalid" };
|
|
5164
|
+
}
|
|
5165
|
+
return entries;
|
|
5166
|
+
}
|
|
5167
|
+
function formatInbox(name, entries) {
|
|
5168
|
+
if (entries.length === 0) return `${name}
|
|
5169
|
+
- (empty)`;
|
|
5170
|
+
const lines = entries.map((entry) => {
|
|
5171
|
+
if (entry.type === "item") {
|
|
5172
|
+
const stage = entry.stage ? ` ${entry.stage}` : "";
|
|
5173
|
+
const artifact = entry.artifact ? ` ${entry.artifact}` : "";
|
|
5174
|
+
return `- ${entry.id} ${entry.status}${stage}${artifact}`;
|
|
5175
|
+
}
|
|
5176
|
+
if (entry.type === "handoff") return `- handoff from ${entry.from} to ${entry.to}`;
|
|
5177
|
+
return `- ${entry.label}`;
|
|
5178
|
+
});
|
|
5179
|
+
return `${name}
|
|
5180
|
+
${lines.join("\n")}`;
|
|
5181
|
+
}
|
|
5182
|
+
async function queueNames(root, bot) {
|
|
5183
|
+
if (bot) {
|
|
5184
|
+
if (!canonicalRoleName(bot)) return { error: ROLE_TEXT.badName };
|
|
5185
|
+
return [bot];
|
|
5186
|
+
}
|
|
5187
|
+
const team = await readTeamConfig(root);
|
|
5188
|
+
if ("team" in team) return team.team.bots.map((item) => item.name);
|
|
5189
|
+
const dirs = (await listDirs(join15(root, ".sdd", "agents"))).sort();
|
|
5190
|
+
if (dirs.length === 0) return { error: team.error };
|
|
5191
|
+
return dirs;
|
|
5192
|
+
}
|
|
5193
|
+
async function queueRole(root, bot) {
|
|
5194
|
+
const names = await queueNames(root, bot?.trim());
|
|
5195
|
+
if ("error" in names) return fail6(names.error);
|
|
5196
|
+
const blocks = [];
|
|
5197
|
+
for (const name of names) {
|
|
5198
|
+
const entries = await readInbox(root, name);
|
|
5199
|
+
if ("error" in entries) return fail6(entries.error);
|
|
5200
|
+
blocks.push(formatInbox(name, entries));
|
|
5201
|
+
}
|
|
5202
|
+
return ok6(blocks.join("\n\n"));
|
|
5203
|
+
}
|
|
5204
|
+
function itemId(item) {
|
|
5205
|
+
if (item.id) return item.id;
|
|
5206
|
+
if (item.artifact) {
|
|
5207
|
+
const base = item.artifact.split("/").pop() ?? "";
|
|
5208
|
+
const stem = base.replace(/\.[A-Za-z0-9]+$/, "");
|
|
5209
|
+
if (stem) return stem;
|
|
5210
|
+
}
|
|
5211
|
+
return item.stage ?? null;
|
|
5212
|
+
}
|
|
5213
|
+
async function takeRole(root, input) {
|
|
5214
|
+
const item = await prepareItem(root, input);
|
|
5215
|
+
if ("error" in item) return fail6(item.error);
|
|
5216
|
+
const matched = await matchRoute(root, item);
|
|
5217
|
+
if (!matched.ok) return fail6(matched.message);
|
|
5218
|
+
const match = matched.match;
|
|
5219
|
+
if (match.kind === "ask") return fail6(formatHit(match));
|
|
5220
|
+
const team = await readTeamConfig(root);
|
|
5221
|
+
if ("error" in team) return fail6(team.error);
|
|
5222
|
+
const bots = team.team.bots.filter((bot) => bot.role === match.role);
|
|
5223
|
+
if (bots.length === 0) return fail6(`No bot has role ${match.role}.`);
|
|
5224
|
+
const requested = input.bot?.trim();
|
|
5225
|
+
if (requested && !canonicalRoleName(requested)) return fail6(ROLE_TEXT.badName);
|
|
5226
|
+
if (requested && !bots.some((bot) => bot.name === requested)) {
|
|
5227
|
+
const known = team.team.bots.some((bot) => bot.name === requested);
|
|
5228
|
+
if (!known) return fail6("Bot not found.");
|
|
5229
|
+
return fail6(`${requested} is not a ${match.role}.`);
|
|
5230
|
+
}
|
|
5231
|
+
const counts = /* @__PURE__ */ new Map();
|
|
5232
|
+
for (const bot of team.team.bots) {
|
|
5233
|
+
const entries = await readInbox(root, bot.name);
|
|
5234
|
+
if ("error" in entries) return fail6(entries.error);
|
|
5235
|
+
counts.set(
|
|
5236
|
+
bot.name,
|
|
5237
|
+
entries.filter((entry) => entry.type === "item" && entry.status === "active").length
|
|
5238
|
+
);
|
|
5239
|
+
}
|
|
5240
|
+
const roleCount = bots.reduce((sum, bot) => sum + (counts.get(bot.name) ?? 0), 0);
|
|
5241
|
+
const roleCap = team.team.role_wip?.[match.role];
|
|
5242
|
+
if (roleCap !== void 0 && roleCount >= roleCap) {
|
|
5243
|
+
return fail6(`${match.role} is at the limit.`);
|
|
5244
|
+
}
|
|
5245
|
+
const chosen = requested ? bots.find((bot) => bot.name === requested) : bots.find((bot) => (counts.get(bot.name) ?? 0) < bot.wip);
|
|
5246
|
+
if (!chosen || (counts.get(chosen.name) ?? 0) >= chosen.wip) {
|
|
5247
|
+
return fail6(`${requested || bots[0]?.name || match.role} is at the limit.`);
|
|
5248
|
+
}
|
|
5249
|
+
const id = itemId(item);
|
|
5250
|
+
if (!id) return fail6("An item needs an id.");
|
|
5251
|
+
for (const bot of team.team.bots) {
|
|
5252
|
+
const entries = await readInbox(root, bot.name);
|
|
5253
|
+
if ("error" in entries) return fail6(entries.error);
|
|
5254
|
+
if (entries.some((entry) => entry.type === "item" && entry.status === "active" && entry.id === id)) {
|
|
5255
|
+
return fail6(`${id} is already active.`);
|
|
5256
|
+
}
|
|
5257
|
+
}
|
|
5258
|
+
const record = {
|
|
5259
|
+
type: "item",
|
|
5260
|
+
id,
|
|
5261
|
+
status: "active",
|
|
5262
|
+
role: match.role,
|
|
5263
|
+
rule: match.rule,
|
|
5264
|
+
reason: match.reason
|
|
5265
|
+
};
|
|
5266
|
+
if (item.stage) record.stage = item.stage;
|
|
5267
|
+
if (item.artifact) record.artifact = item.artifact;
|
|
5268
|
+
if (item.skill) record.skill = item.skill;
|
|
5269
|
+
const file = inboxFile(root, chosen.name);
|
|
5270
|
+
if (!file) return fail6(ROLE_TEXT.badName);
|
|
5271
|
+
await appendJson(file, record);
|
|
5272
|
+
return ok6(`${chosen.name} took ${id}
|
|
5273
|
+
role ${match.role}
|
|
5274
|
+
rule ${match.rule}`);
|
|
3147
5275
|
}
|
|
3148
5276
|
|
|
3149
5277
|
// src/role/handoff.ts
|
|
3150
5278
|
var ID = /^[a-z][a-z0-9-]{0,39}$/;
|
|
3151
5279
|
var ROLE_YAML = ["role.yaml"];
|
|
3152
|
-
function
|
|
5280
|
+
function ok7(stdout = "", runId) {
|
|
3153
5281
|
return runId ? { exitCode: 0, stdout, runId } : { exitCode: 0, stdout };
|
|
3154
5282
|
}
|
|
3155
|
-
function
|
|
5283
|
+
function fail7(stdout, runId) {
|
|
3156
5284
|
return runId ? { exitCode: 1, stdout, runId } : { exitCode: 1, stdout };
|
|
3157
5285
|
}
|
|
3158
5286
|
function mentionsX2(task) {
|
|
@@ -3163,10 +5291,10 @@ function safeChangeId2(id) {
|
|
|
3163
5291
|
return !id.includes("/") && !id.includes("\\") && !id.includes("\0");
|
|
3164
5292
|
}
|
|
3165
5293
|
function yamlPath(root) {
|
|
3166
|
-
return
|
|
5294
|
+
return join16(root, ".sdd", ...ROLE_YAML);
|
|
3167
5295
|
}
|
|
3168
5296
|
function planPath(root, changeId, sectionId) {
|
|
3169
|
-
return
|
|
5297
|
+
return join16(root, ".sdd", "role-runs", "plans", changeId, `${sectionId}.json`);
|
|
3170
5298
|
}
|
|
3171
5299
|
async function loadYaml(root) {
|
|
3172
5300
|
const path = yamlPath(root);
|
|
@@ -3197,11 +5325,11 @@ async function messagingOn(root) {
|
|
|
3197
5325
|
}
|
|
3198
5326
|
async function setRoleMessaging(root, enabled) {
|
|
3199
5327
|
const loaded = await loadYaml(root);
|
|
3200
|
-
if (loaded === "invalid") return
|
|
5328
|
+
if (loaded === "invalid") return fail7(ROLE_TEXT.badYaml);
|
|
3201
5329
|
const raw = loaded === "missing" ? {} : loaded;
|
|
3202
5330
|
raw.role_messaging = enabled;
|
|
3203
|
-
await writeText(yamlPath(root),
|
|
3204
|
-
return
|
|
5331
|
+
await writeText(yamlPath(root), stringifyYaml(raw));
|
|
5332
|
+
return ok7(enabled ? "Messaging between roles is on." : "Messaging between roles is off.");
|
|
3205
5333
|
}
|
|
3206
5334
|
function field(body, prefix) {
|
|
3207
5335
|
const found = [];
|
|
@@ -3300,29 +5428,30 @@ function receiverPrompt(role, task, section) {
|
|
|
3300
5428
|
async function handoffRole(root, input) {
|
|
3301
5429
|
const from = canonicalRoleName(input.from);
|
|
3302
5430
|
const to = canonicalRoleName(input.to);
|
|
3303
|
-
if (!from || !to) return
|
|
3304
|
-
if (from === to) return
|
|
5431
|
+
if (!from || !to) return fail7(ROLE_TEXT.badName);
|
|
5432
|
+
if (from === to) return fail7("A role cannot hand off to itself.");
|
|
3305
5433
|
const sender = await readRole(root, from);
|
|
3306
5434
|
const receiver = await readRole(root, to);
|
|
3307
|
-
if (!sender || !receiver) return
|
|
5435
|
+
if (!sender || !receiver) return fail7(ROLE_TEXT.notFound);
|
|
3308
5436
|
const task = input.task ?? "";
|
|
3309
|
-
if (mentionsX2(task)) return
|
|
5437
|
+
if (mentionsX2(task)) return fail7(ROLE_TEXT.x);
|
|
3310
5438
|
const loaded = await acceptedFacts(root, input.change?.trim() ?? "", input.section?.trim() ?? "");
|
|
3311
|
-
if (!loaded) return
|
|
5439
|
+
if (!loaded) return fail7(ROLE_TEXT.needsSection);
|
|
3312
5440
|
if (loaded.current.owners.length > 0 && !loaded.current.owners.includes(to)) {
|
|
3313
|
-
return
|
|
5441
|
+
return fail7("One owner handles this section.");
|
|
3314
5442
|
}
|
|
3315
5443
|
const messaging = await messagingOn(root);
|
|
3316
|
-
if (messaging === false) return
|
|
3317
|
-
if (!input.model) return
|
|
3318
|
-
if (messaging === "invalid") return
|
|
5444
|
+
if (messaging === false) return ok7("Messaging between roles is off.");
|
|
5445
|
+
if (!input.model) return fail7(ROLE_TEXT.noModel);
|
|
5446
|
+
if (messaging === "invalid") return fail7(ROLE_TEXT.badYaml);
|
|
3319
5447
|
const now = (input.now ?? (() => /* @__PURE__ */ new Date()))();
|
|
3320
5448
|
const runId = runIdFrom(now);
|
|
3321
|
-
const senderFile =
|
|
3322
|
-
const receiverFile =
|
|
5449
|
+
const senderFile = join16(conversationDir(root, from), `${runId}.jsonl`);
|
|
5450
|
+
const receiverFile = join16(conversationDir(root, to), `${runId}.jsonl`);
|
|
3323
5451
|
const record = handoffRecord(from, to, input.change?.trim() ?? "", loaded.current);
|
|
3324
5452
|
await appendJson(senderFile, record);
|
|
3325
5453
|
await appendJson(receiverFile, record);
|
|
5454
|
+
await writeHandoffInboxes(root, record);
|
|
3326
5455
|
let raw;
|
|
3327
5456
|
try {
|
|
3328
5457
|
raw = await input.model.complete({
|
|
@@ -3331,12 +5460,12 @@ async function handoffRole(root, input) {
|
|
|
3331
5460
|
});
|
|
3332
5461
|
} catch {
|
|
3333
5462
|
await appendJson(receiverFile, { type: "bad_call", roleId: to });
|
|
3334
|
-
return
|
|
5463
|
+
return fail7("", runId);
|
|
3335
5464
|
}
|
|
3336
5465
|
const parsed = parseReply(raw);
|
|
3337
5466
|
if (!parsed) {
|
|
3338
5467
|
await appendJson(receiverFile, { type: "bad_call", roleId: to });
|
|
3339
|
-
return
|
|
5468
|
+
return fail7("", runId);
|
|
3340
5469
|
}
|
|
3341
5470
|
await appendJson(receiverFile, { type: "model", pass: 1, roleId: to, text: parsed.text });
|
|
3342
5471
|
const settled = await settleRoleReply(root, receiverFile, receiver, parsed, now, runId);
|
|
@@ -3353,7 +5482,7 @@ async function handoffRole(root, input) {
|
|
|
3353
5482
|
}
|
|
3354
5483
|
async function showRoleGroup(root, changeId) {
|
|
3355
5484
|
const path = safeChangeId2(changeId) ? await acceptedFile(root, changeId) : "";
|
|
3356
|
-
if (!path || !await pathExists(path)) return
|
|
5485
|
+
if (!path || !await pathExists(path)) return ok7("No roles in this group.");
|
|
3357
5486
|
const seen = /* @__PURE__ */ new Set();
|
|
3358
5487
|
const owners = [];
|
|
3359
5488
|
for (const section of factsFrom(await readText(path))) {
|
|
@@ -3363,7 +5492,7 @@ async function showRoleGroup(root, changeId) {
|
|
|
3363
5492
|
owners.push(owner);
|
|
3364
5493
|
}
|
|
3365
5494
|
}
|
|
3366
|
-
return
|
|
5495
|
+
return ok7(owners.length > 0 ? owners.join("\n") : "No roles in this group.");
|
|
3367
5496
|
}
|
|
3368
5497
|
function distinctServices(all) {
|
|
3369
5498
|
const services = /* @__PURE__ */ new Map();
|
|
@@ -3491,19 +5620,20 @@ function leadPrompt(task, services) {
|
|
|
3491
5620
|
}
|
|
3492
5621
|
async function connectionResult(root, changeId, section, now) {
|
|
3493
5622
|
const messaging = await messagingOn(root);
|
|
3494
|
-
if (messaging === "invalid") return
|
|
3495
|
-
if (!messaging) return
|
|
5623
|
+
if (messaging === "invalid") return fail7(ROLE_TEXT.badYaml);
|
|
5624
|
+
if (!messaging) return ok7("Messaging between roles is off.");
|
|
3496
5625
|
const owners = unique(section.owners);
|
|
3497
5626
|
const runId = runIdFrom(now);
|
|
3498
5627
|
for (let i = 0; i < owners.length - 1; i++) {
|
|
3499
5628
|
const from = owners[i] ?? "";
|
|
3500
5629
|
const to = owners[i + 1] ?? "";
|
|
3501
5630
|
const record = handoffRecord(from, to, changeId, section);
|
|
3502
|
-
await appendJson(
|
|
3503
|
-
await appendJson(
|
|
5631
|
+
await appendJson(join16(conversationDir(root, from), `${runId}.jsonl`), record);
|
|
5632
|
+
await appendJson(join16(conversationDir(root, to), `${runId}.jsonl`), record);
|
|
5633
|
+
await writeHandoffInboxes(root, record);
|
|
3504
5634
|
}
|
|
3505
5635
|
const lines = ["Connection hands off. Lead is absent.", ...owners];
|
|
3506
|
-
return
|
|
5636
|
+
return ok7(lines.join("\n"));
|
|
3507
5637
|
}
|
|
3508
5638
|
function freshPlan(task, nodes) {
|
|
3509
5639
|
return { status: "pending", rebuilds: 0, stalled: false, task, nodes, results: [] };
|
|
@@ -3512,7 +5642,7 @@ async function planNetwork(root, input) {
|
|
|
3512
5642
|
const changeId = input.change?.trim() ?? "";
|
|
3513
5643
|
const sectionId = input.section?.trim() ?? "";
|
|
3514
5644
|
const loaded = await acceptedFacts(root, changeId, sectionId);
|
|
3515
|
-
if (!loaded) return
|
|
5645
|
+
if (!loaded) return fail7(ROLE_TEXT.needsSection);
|
|
3516
5646
|
const task = input.task ?? "";
|
|
3517
5647
|
const now = (input.now ?? (() => /* @__PURE__ */ new Date()))();
|
|
3518
5648
|
const connection = loaded.all.find(
|
|
@@ -3520,23 +5650,23 @@ async function planNetwork(root, input) {
|
|
|
3520
5650
|
);
|
|
3521
5651
|
if (connection?.connection) return connectionResult(root, changeId, connection, now);
|
|
3522
5652
|
const services = distinctServices(loaded.all);
|
|
3523
|
-
if (services.size === 0) return
|
|
5653
|
+
if (services.size === 0) return ok7("One scope. No lead.");
|
|
3524
5654
|
if (services.size === 1) {
|
|
3525
5655
|
const entry = [...services.entries()][0];
|
|
3526
|
-
if (!entry) return
|
|
5656
|
+
if (!entry) return ok7("One scope. No lead.");
|
|
3527
5657
|
const [name, section] = entry;
|
|
3528
5658
|
const owner = section.owners[0];
|
|
3529
|
-
if (!owner) return
|
|
3530
|
-
if (!await readRole(root, owner)) return
|
|
3531
|
-
return
|
|
5659
|
+
if (!owner) return fail7("This service needs an owner.");
|
|
5660
|
+
if (!await readRole(root, owner)) return fail7(ROLE_TEXT.notFound);
|
|
5661
|
+
return ok7(`Service ${name} runs ${owner}.`);
|
|
3532
5662
|
}
|
|
3533
|
-
const picked = pickLead(loaded.all, services);
|
|
3534
|
-
if (picked.kind === "none") return
|
|
3535
|
-
if (picked.kind === "missing") return
|
|
5663
|
+
const picked = await pickLead(root, loaded.all, services);
|
|
5664
|
+
if (picked.kind === "none") return ok7("One scope. No lead.");
|
|
5665
|
+
if (picked.kind === "missing") return fail7(picked.sentence);
|
|
3536
5666
|
const lead = await readRole(root, picked.name);
|
|
3537
|
-
if (!lead) return
|
|
3538
|
-
if (await messagingOn(root) === "invalid") return
|
|
3539
|
-
if (!input.model) return
|
|
5667
|
+
if (!lead) return fail7(ROLE_TEXT.notFound);
|
|
5668
|
+
if (await messagingOn(root) === "invalid") return fail7(ROLE_TEXT.badYaml);
|
|
5669
|
+
if (!input.model) return fail7(ROLE_TEXT.noModel);
|
|
3540
5670
|
return storeLeadPlan(
|
|
3541
5671
|
root,
|
|
3542
5672
|
changeId,
|
|
@@ -3554,17 +5684,17 @@ async function storeLeadPlan(root, changeId, sectionId, task, lead, services, al
|
|
|
3554
5684
|
try {
|
|
3555
5685
|
raw = await model.complete({ roleId: lead.name, prompt: leadPrompt(task, services) });
|
|
3556
5686
|
} catch {
|
|
3557
|
-
return
|
|
5687
|
+
return fail7("");
|
|
3558
5688
|
}
|
|
3559
5689
|
const parsed = await parsePlan(root, raw, contextOf(all));
|
|
3560
|
-
if (previous && (parsed === "invalid" || parsed === "deep")) return
|
|
3561
|
-
if (parsed === "invalid") return
|
|
3562
|
-
if (parsed === "deep") return
|
|
5690
|
+
if (previous && (parsed === "invalid" || parsed === "deep")) return fail7(planFailure(parsed));
|
|
5691
|
+
if (parsed === "invalid") return fail7("The plan is not valid.");
|
|
5692
|
+
if (parsed === "deep") return fail7("A plan is deeper than 2.");
|
|
3563
5693
|
if (previous && sameArtifacts(previous.nodes, parsed)) {
|
|
3564
5694
|
previous.stalled = true;
|
|
3565
5695
|
previous.rebuilds += 1;
|
|
3566
5696
|
await writePlan(root, changeId, sectionId, previous);
|
|
3567
|
-
return
|
|
5697
|
+
return ok7("Stopped. The plan made no progress.");
|
|
3568
5698
|
}
|
|
3569
5699
|
const next = previous ? {
|
|
3570
5700
|
...previous,
|
|
@@ -3575,7 +5705,7 @@ async function storeLeadPlan(root, changeId, sectionId, task, lead, services, al
|
|
|
3575
5705
|
results: []
|
|
3576
5706
|
} : freshPlan(task, parsed);
|
|
3577
5707
|
await writePlan(root, changeId, sectionId, next);
|
|
3578
|
-
return
|
|
5708
|
+
return ok7(["Accept this plan before anyone writes.", ...planLines(parsed)].join("\n"));
|
|
3579
5709
|
}
|
|
3580
5710
|
function planFailure(parsed) {
|
|
3581
5711
|
return parsed === "deep" ? "A plan is deeper than 2." : "The plan is not valid.";
|
|
@@ -3588,13 +5718,13 @@ function sameArtifacts(left, right) {
|
|
|
3588
5718
|
async function acceptNetwork(root, input) {
|
|
3589
5719
|
const changeId = input.change?.trim() ?? "";
|
|
3590
5720
|
const sectionId = input.section?.trim() ?? "";
|
|
3591
|
-
if (!safeChangeId2(changeId) || !sectionId) return
|
|
5721
|
+
if (!safeChangeId2(changeId) || !sectionId) return fail7("No plan is stored.");
|
|
3592
5722
|
const plan = await readPlan(root, changeId, sectionId);
|
|
3593
|
-
if (!plan) return
|
|
3594
|
-
if (plan.status !== "pending") return
|
|
5723
|
+
if (!plan) return fail7("No plan is stored.");
|
|
5724
|
+
if (plan.status !== "pending") return fail7("The plan is not waiting for acceptance.");
|
|
3595
5725
|
plan.status = "accepted";
|
|
3596
5726
|
await writePlan(root, changeId, sectionId, plan);
|
|
3597
|
-
return
|
|
5727
|
+
return ok7("Plan accepted.");
|
|
3598
5728
|
}
|
|
3599
5729
|
function walk(nodes) {
|
|
3600
5730
|
const flat = [];
|
|
@@ -3615,16 +5745,16 @@ function ready(node, results) {
|
|
|
3615
5745
|
async function runNetwork(root, input) {
|
|
3616
5746
|
const changeId = input.change?.trim() ?? "";
|
|
3617
5747
|
const sectionId = input.section?.trim() ?? "";
|
|
3618
|
-
if (!safeChangeId2(changeId) || !sectionId) return
|
|
5748
|
+
if (!safeChangeId2(changeId) || !sectionId) return fail7("No plan is stored.");
|
|
3619
5749
|
const plan = await readPlan(root, changeId, sectionId);
|
|
3620
|
-
if (!plan) return
|
|
3621
|
-
if (plan.status === "pending") return
|
|
3622
|
-
if (plan.status === "done") return
|
|
3623
|
-
if (plan.status !== "accepted" && plan.status !== "open") return
|
|
3624
|
-
if (await messagingOn(root) === "invalid") return
|
|
5750
|
+
if (!plan) return fail7("No plan is stored.");
|
|
5751
|
+
if (plan.status === "pending") return ok7("Accept this plan before anyone writes.");
|
|
5752
|
+
if (plan.status === "done") return ok7("The parent is done.");
|
|
5753
|
+
if (plan.status !== "accepted" && plan.status !== "open") return fail7("No plan is stored.");
|
|
5754
|
+
if (await messagingOn(root) === "invalid") return fail7(ROLE_TEXT.badYaml);
|
|
3625
5755
|
const minutes = await timeoutMinutes(root);
|
|
3626
|
-
if (minutes === "invalid") return
|
|
3627
|
-
if (!input.model) return
|
|
5756
|
+
if (minutes === "invalid") return fail7(ROLE_TEXT.badYaml);
|
|
5757
|
+
if (!input.model) return fail7(ROLE_TEXT.noModel);
|
|
3628
5758
|
const now = (input.now ?? (() => /* @__PURE__ */ new Date()))();
|
|
3629
5759
|
const summaries = [];
|
|
3630
5760
|
const called = /* @__PURE__ */ new Set();
|
|
@@ -3644,7 +5774,7 @@ async function runNetwork(root, input) {
|
|
|
3644
5774
|
const when = new Date(now.getTime() + index * 1e3);
|
|
3645
5775
|
index += 1;
|
|
3646
5776
|
const runId = runIdFrom(when);
|
|
3647
|
-
const file =
|
|
5777
|
+
const file = join16(conversationDir(root, owner.name), `${runId}.jsonl`);
|
|
3648
5778
|
let raw;
|
|
3649
5779
|
try {
|
|
3650
5780
|
raw = await input.model.complete({
|
|
@@ -3688,7 +5818,7 @@ async function runNetwork(root, input) {
|
|
|
3688
5818
|
const waiting = all.some((node) => !plan.results.some((item) => item.decision === node.decision));
|
|
3689
5819
|
plan.status = waiting ? "open" : "done";
|
|
3690
5820
|
await writePlan(root, changeId, sectionId, plan);
|
|
3691
|
-
if (missingOwner) return
|
|
5821
|
+
if (missingOwner) return fail7(ROLE_TEXT.notFound);
|
|
3692
5822
|
const lines = [...summaries];
|
|
3693
5823
|
if (waiting) {
|
|
3694
5824
|
lines.push("A branch is waiting for its prerequisites.");
|
|
@@ -3696,27 +5826,27 @@ async function runNetwork(root, input) {
|
|
|
3696
5826
|
} else {
|
|
3697
5827
|
lines.push("The parent is done.");
|
|
3698
5828
|
}
|
|
3699
|
-
return
|
|
5829
|
+
return ok7(lines.join("\n"));
|
|
3700
5830
|
}
|
|
3701
5831
|
async function rebuildNetwork(root, input) {
|
|
3702
5832
|
const changeId = input.change?.trim() ?? "";
|
|
3703
5833
|
const sectionId = input.section?.trim() ?? "";
|
|
3704
|
-
if (!safeChangeId2(changeId) || !sectionId) return
|
|
5834
|
+
if (!safeChangeId2(changeId) || !sectionId) return fail7("No plan is stored.");
|
|
3705
5835
|
const plan = await readPlan(root, changeId, sectionId);
|
|
3706
|
-
if (!plan) return
|
|
5836
|
+
if (!plan) return fail7("No plan is stored.");
|
|
3707
5837
|
const allowed = plan.rebuilds === 0 || plan.rebuilds === 1 && !plan.stalled && plan.status === "accepted";
|
|
3708
|
-
if (!allowed) return
|
|
5838
|
+
if (!allowed) return fail7("A second rebuild does not start.");
|
|
3709
5839
|
const loaded = await acceptedFacts(root, changeId, sectionId);
|
|
3710
|
-
if (!loaded) return
|
|
3711
|
-
if (!input.model) return
|
|
5840
|
+
if (!loaded) return fail7(ROLE_TEXT.needsSection);
|
|
5841
|
+
if (!input.model) return fail7(ROLE_TEXT.noModel);
|
|
3712
5842
|
const services = distinctServices(loaded.all);
|
|
3713
|
-
const picked = pickLead(loaded.all, services);
|
|
3714
|
-
if (picked.kind === "missing") return
|
|
3715
|
-
if (picked.kind !== "lead") return
|
|
5843
|
+
const picked = await pickLead(root, loaded.all, services);
|
|
5844
|
+
if (picked.kind === "missing") return fail7(picked.sentence);
|
|
5845
|
+
if (picked.kind !== "lead") return fail7("This module needs a lead.");
|
|
3716
5846
|
const lead = await readRole(root, picked.name);
|
|
3717
|
-
if (!lead) return
|
|
5847
|
+
if (!lead) return fail7(ROLE_TEXT.notFound);
|
|
3718
5848
|
const before = JSON.stringify(plan);
|
|
3719
|
-
const
|
|
5849
|
+
const result2 = await storeLeadPlan(
|
|
3720
5850
|
root,
|
|
3721
5851
|
changeId,
|
|
3722
5852
|
sectionId,
|
|
@@ -3727,15 +5857,35 @@ async function rebuildNetwork(root, input) {
|
|
|
3727
5857
|
input.model,
|
|
3728
5858
|
plan
|
|
3729
5859
|
);
|
|
3730
|
-
if (
|
|
5860
|
+
if (result2.exitCode !== 0) {
|
|
3731
5861
|
const current = await readPlan(root, changeId, sectionId);
|
|
3732
5862
|
if (current && JSON.stringify(current) !== before) {
|
|
3733
5863
|
await writePlan(root, changeId, sectionId, JSON.parse(before));
|
|
3734
5864
|
}
|
|
3735
5865
|
}
|
|
3736
|
-
return
|
|
5866
|
+
return result2;
|
|
3737
5867
|
}
|
|
3738
|
-
function
|
|
5868
|
+
async function chooseLead(root, sections, missing) {
|
|
5869
|
+
const routed = await matchRoute(root, { stage: "network", skill: "name the next owner" });
|
|
5870
|
+
if (!routed.ok) return { kind: "missing", sentence: routed.message };
|
|
5871
|
+
if (routed.match.kind === "role") return { kind: "lead", name: routed.match.role };
|
|
5872
|
+
if (routed.match.reason !== "No rule matched.") {
|
|
5873
|
+
return { kind: "missing", sentence: "Ask the human." };
|
|
5874
|
+
}
|
|
5875
|
+
const names = [];
|
|
5876
|
+
for (const section of sections) {
|
|
5877
|
+
for (const lead of section.leads) {
|
|
5878
|
+
if (!names.includes(lead)) names.push(lead);
|
|
5879
|
+
}
|
|
5880
|
+
}
|
|
5881
|
+
if (names.length === 1) {
|
|
5882
|
+
const name = names[0];
|
|
5883
|
+
if (name) return { kind: "lead", name };
|
|
5884
|
+
}
|
|
5885
|
+
if (names.length === 0) return { kind: "missing", sentence: missing };
|
|
5886
|
+
return { kind: "missing", sentence: "Ask the human." };
|
|
5887
|
+
}
|
|
5888
|
+
async function pickLead(root, all, services) {
|
|
3739
5889
|
const modules = /* @__PURE__ */ new Set();
|
|
3740
5890
|
let missingModule = false;
|
|
3741
5891
|
for (const section of services.values()) {
|
|
@@ -3745,20 +5895,61 @@ function pickLead(all, services) {
|
|
|
3745
5895
|
}
|
|
3746
5896
|
const moduleId = [...modules][0];
|
|
3747
5897
|
if (!missingModule && modules.size === 1 && moduleId && services.size > 1) {
|
|
3748
|
-
|
|
3749
|
-
|
|
3750
|
-
|
|
5898
|
+
return chooseLead(
|
|
5899
|
+
root,
|
|
5900
|
+
all.filter((item) => item.modules.includes(moduleId)),
|
|
5901
|
+
"This module needs a lead."
|
|
5902
|
+
);
|
|
3751
5903
|
}
|
|
3752
5904
|
if (modules.size > 1) {
|
|
3753
|
-
|
|
3754
|
-
|
|
3755
|
-
|
|
5905
|
+
return chooseLead(
|
|
5906
|
+
root,
|
|
5907
|
+
all.filter((item) => item.product),
|
|
5908
|
+
"This product needs a lead."
|
|
5909
|
+
);
|
|
3756
5910
|
}
|
|
3757
5911
|
return { kind: "none" };
|
|
3758
5912
|
}
|
|
3759
5913
|
|
|
5914
|
+
// src/role/models/show.ts
|
|
5915
|
+
function formatModelChoice(workType, choice) {
|
|
5916
|
+
return `${workType} ${choice.id} ${choice.kind} ${choice.provider} ${choice.why}`;
|
|
5917
|
+
}
|
|
5918
|
+
function confidence(model) {
|
|
5919
|
+
return "min_confidence" in model && model.min_confidence !== void 0 ? String(model.min_confidence) : "n/a";
|
|
5920
|
+
}
|
|
5921
|
+
function formatEntry2(id, model) {
|
|
5922
|
+
if (model.provider === "rules") return `${id} decide rules`;
|
|
5923
|
+
if (model.provider === "command") {
|
|
5924
|
+
return `${id} ${model.kind} command command_env ${model.command_env} min_confidence ${confidence(model)}`;
|
|
5925
|
+
}
|
|
5926
|
+
return `${id} decide ollama model ${model.model} base_url_env ${model.base_url_env} min_confidence ${confidence(model)}`;
|
|
5927
|
+
}
|
|
5928
|
+
function formatModels(config) {
|
|
5929
|
+
const lines = Object.keys(config.models).sort().map((id) => formatEntry2(id, config.models[id]));
|
|
5930
|
+
for (const workType of WORK_TYPES) {
|
|
5931
|
+
lines.push(formatModelChoice(workType, selectModel(config, { workType })));
|
|
5932
|
+
}
|
|
5933
|
+
const reason = config.models[config.defaults.fallback_reason];
|
|
5934
|
+
const decide2 = config.models[config.defaults.fallback_decide];
|
|
5935
|
+
if (reason) {
|
|
5936
|
+
lines.push(
|
|
5937
|
+
`fallback_reason ${config.defaults.fallback_reason} ${reason.kind} ${reason.provider} fallback`
|
|
5938
|
+
);
|
|
5939
|
+
}
|
|
5940
|
+
if (decide2) {
|
|
5941
|
+
lines.push(
|
|
5942
|
+
`fallback_decide ${config.defaults.fallback_decide} ${decide2.kind} ${decide2.provider} fallback`
|
|
5943
|
+
);
|
|
5944
|
+
}
|
|
5945
|
+
return lines.join("\n");
|
|
5946
|
+
}
|
|
5947
|
+
function explainModel(config, workType, ctx) {
|
|
5948
|
+
return formatModelChoice(workType, selectModel(config, { workType, ...ctx }));
|
|
5949
|
+
}
|
|
5950
|
+
|
|
3760
5951
|
// src/stories.ts
|
|
3761
|
-
import { join as
|
|
5952
|
+
import { join as join17 } from "pathe";
|
|
3762
5953
|
var STORY_ID = /^S-\d+$/i;
|
|
3763
5954
|
var EPIC_KEY = /^[A-Za-z0-9][A-Za-z0-9-]{0,39}$/;
|
|
3764
5955
|
function validBranchName(name) {
|
|
@@ -3800,7 +5991,7 @@ function parseStoryDraft(input) {
|
|
|
3800
5991
|
async function metasIn(dir) {
|
|
3801
5992
|
const out = [];
|
|
3802
5993
|
for (const id of await listDirs(dir)) {
|
|
3803
|
-
const path =
|
|
5994
|
+
const path = join17(dir, id, "meta.yaml");
|
|
3804
5995
|
if (!await pathExists(path)) continue;
|
|
3805
5996
|
try {
|
|
3806
5997
|
out.push(ChangeMetaSchema.parse(await readYaml(path)));
|
|
@@ -3933,6 +6124,7 @@ export {
|
|
|
3933
6124
|
ConfigSchema,
|
|
3934
6125
|
DEFAULT_CODE_CONTEXT_CAPS,
|
|
3935
6126
|
DEFAULT_INIT_INTEGRATION,
|
|
6127
|
+
DecisionMetricSchema,
|
|
3936
6128
|
EMBEDDINGS_UNAVAILABLE,
|
|
3937
6129
|
EMBEDDING_MODEL,
|
|
3938
6130
|
GateSchema,
|
|
@@ -3940,6 +6132,7 @@ export {
|
|
|
3940
6132
|
McpInvokeSchema,
|
|
3941
6133
|
McpSourceSchema,
|
|
3942
6134
|
McpWhenSchema,
|
|
6135
|
+
ModelsConfigSchema,
|
|
3943
6136
|
PROTOCOL_MD,
|
|
3944
6137
|
PhaseMetricSchema,
|
|
3945
6138
|
RRF_K,
|
|
@@ -3950,12 +6143,15 @@ export {
|
|
|
3950
6143
|
SddError,
|
|
3951
6144
|
SearchConfigError,
|
|
3952
6145
|
StageSchema,
|
|
6146
|
+
TeamSchema,
|
|
3953
6147
|
TokenUsageMetricSchema,
|
|
3954
6148
|
VerifyCommandSchema,
|
|
6149
|
+
WORK_TYPES,
|
|
3955
6150
|
WorkflowSchema,
|
|
3956
6151
|
acceptNetwork,
|
|
3957
6152
|
activePointerPath,
|
|
3958
6153
|
activeStages,
|
|
6154
|
+
addLesson,
|
|
3959
6155
|
addMcpSource,
|
|
3960
6156
|
advanceStage,
|
|
3961
6157
|
agentHostPaths,
|
|
@@ -3978,6 +6174,8 @@ export {
|
|
|
3978
6174
|
changePath,
|
|
3979
6175
|
changesDir,
|
|
3980
6176
|
changesPathRel,
|
|
6177
|
+
checkModels,
|
|
6178
|
+
checkTeam,
|
|
3981
6179
|
chunkCode,
|
|
3982
6180
|
chunkMarkdown,
|
|
3983
6181
|
classifyPath,
|
|
@@ -3995,12 +6193,15 @@ export {
|
|
|
3995
6193
|
createChange,
|
|
3996
6194
|
createFastembedEmbedder,
|
|
3997
6195
|
createRole,
|
|
6196
|
+
createRoleFromTemplate,
|
|
6197
|
+
decide,
|
|
3998
6198
|
defaultConfig,
|
|
3999
6199
|
defaultMcpConfig,
|
|
4000
6200
|
defaultMcpYamlPath,
|
|
4001
6201
|
defaultMemoryDir,
|
|
4002
6202
|
defaultPricesYamlPath,
|
|
4003
6203
|
defaultSearchYamlPath,
|
|
6204
|
+
defaultTeamsDir,
|
|
4004
6205
|
defaultTemplatesDir,
|
|
4005
6206
|
defaultWorkflowsDir,
|
|
4006
6207
|
defaultsRoot,
|
|
@@ -4019,6 +6220,7 @@ export {
|
|
|
4019
6220
|
errorMessage,
|
|
4020
6221
|
executeSearch,
|
|
4021
6222
|
explainMatch,
|
|
6223
|
+
explainModel,
|
|
4022
6224
|
extractiveSummary,
|
|
4023
6225
|
fallbackAdapter,
|
|
4024
6226
|
fallbackIncompleteStageId,
|
|
@@ -4030,7 +6232,10 @@ export {
|
|
|
4030
6232
|
formatEpicGraph,
|
|
4031
6233
|
formatJsonSummary,
|
|
4032
6234
|
formatMcpContextForHandoff,
|
|
6235
|
+
formatModels,
|
|
4033
6236
|
formatStatus,
|
|
6237
|
+
formatTeam,
|
|
6238
|
+
formatTokenInventory,
|
|
4034
6239
|
ftsTokens,
|
|
4035
6240
|
fuseByRank,
|
|
4036
6241
|
fuseHits,
|
|
@@ -4039,9 +6244,11 @@ export {
|
|
|
4039
6244
|
generateCodeContext,
|
|
4040
6245
|
generatedState,
|
|
4041
6246
|
getActiveChangeId,
|
|
6247
|
+
getFacts,
|
|
4042
6248
|
getHeadSha,
|
|
4043
6249
|
getIntegration,
|
|
4044
6250
|
getStage,
|
|
6251
|
+
gitUserName,
|
|
4045
6252
|
guidancePaths,
|
|
4046
6253
|
handoffRole,
|
|
4047
6254
|
hideRole,
|
|
@@ -4050,6 +6257,7 @@ export {
|
|
|
4050
6257
|
indexDbPath,
|
|
4051
6258
|
inferComplexity,
|
|
4052
6259
|
initProject,
|
|
6260
|
+
initTeam,
|
|
4053
6261
|
installAgentIntegration,
|
|
4054
6262
|
installAgentIntegrations,
|
|
4055
6263
|
integrationKeyFor,
|
|
@@ -4063,15 +6271,21 @@ export {
|
|
|
4063
6271
|
isSecretPath,
|
|
4064
6272
|
isStageSkipped,
|
|
4065
6273
|
isSubstantiveArtifactContent,
|
|
6274
|
+
isWorkType,
|
|
4066
6275
|
keywordHits,
|
|
4067
6276
|
listBacklogFeatures,
|
|
6277
|
+
listBundledWorkflowNames,
|
|
4068
6278
|
listChanges,
|
|
4069
6279
|
listDirs,
|
|
6280
|
+
listFacts,
|
|
4070
6281
|
listFiles,
|
|
4071
6282
|
listMatchingSourceIds,
|
|
4072
6283
|
listMcpSourceTools,
|
|
6284
|
+
listRoleTemplates,
|
|
4073
6285
|
listScopeHits,
|
|
6286
|
+
listTeamTemplates,
|
|
4074
6287
|
listWorkflowNames,
|
|
6288
|
+
loadBrain,
|
|
4075
6289
|
loadChangeMeta,
|
|
4076
6290
|
loadConfig,
|
|
4077
6291
|
loadEpicStories,
|
|
@@ -4079,7 +6293,9 @@ export {
|
|
|
4079
6293
|
loadGeneratedManifest,
|
|
4080
6294
|
loadInstalledAgent,
|
|
4081
6295
|
loadMcpConfig,
|
|
6296
|
+
loadModels,
|
|
4082
6297
|
loadPrices,
|
|
6298
|
+
loadProductTeam,
|
|
4083
6299
|
loadSearchConfig,
|
|
4084
6300
|
loadWorkflow,
|
|
4085
6301
|
markGenerated,
|
|
@@ -4091,6 +6307,9 @@ export {
|
|
|
4091
6307
|
mergeCaps,
|
|
4092
6308
|
mergeConfigDefaults,
|
|
4093
6309
|
mergeRankedHits,
|
|
6310
|
+
migrateTeamMemory,
|
|
6311
|
+
missingBundledWorkflows,
|
|
6312
|
+
modelFromEnv,
|
|
4094
6313
|
nextStageId,
|
|
4095
6314
|
nowIso,
|
|
4096
6315
|
optionForTarget,
|
|
@@ -4104,15 +6323,22 @@ export {
|
|
|
4104
6323
|
pinRole,
|
|
4105
6324
|
planAgentIntegration,
|
|
4106
6325
|
planNetwork,
|
|
6326
|
+
postRoom,
|
|
6327
|
+
productTeamHome,
|
|
4107
6328
|
promoteGreenfieldToMemory,
|
|
6329
|
+
queueRole,
|
|
4108
6330
|
rankScope,
|
|
4109
6331
|
readChangeMetrics,
|
|
4110
6332
|
readHandoffCodePoints,
|
|
6333
|
+
readTeamConfig,
|
|
4111
6334
|
readText,
|
|
4112
6335
|
readYaml,
|
|
4113
6336
|
rebuildNetwork,
|
|
4114
6337
|
recommendWorkflow,
|
|
4115
6338
|
reconcileGeneratedTree,
|
|
6339
|
+
recordDecisionMetric,
|
|
6340
|
+
recordDecisionRequest,
|
|
6341
|
+
recordSignoff,
|
|
4116
6342
|
refreshActiveAgentContext,
|
|
4117
6343
|
refreshConfiguredGuidance,
|
|
4118
6344
|
registerSearchProvider,
|
|
@@ -4124,6 +6350,7 @@ export {
|
|
|
4124
6350
|
renderBlock,
|
|
4125
6351
|
renderChangeReport,
|
|
4126
6352
|
renderMemoryIndex,
|
|
6353
|
+
renderProductTeamSection,
|
|
4127
6354
|
renderProtocolMd,
|
|
4128
6355
|
renderSddReadme,
|
|
4129
6356
|
renderThinAgent,
|
|
@@ -4131,13 +6358,17 @@ export {
|
|
|
4131
6358
|
reportAllChanges,
|
|
4132
6359
|
reportChange,
|
|
4133
6360
|
resolveActiveDirectory,
|
|
6361
|
+
resolveBot,
|
|
4134
6362
|
resolveChangeId,
|
|
4135
6363
|
resolveEmbedder,
|
|
4136
6364
|
resolveFocus,
|
|
4137
6365
|
resolveSearchProvider,
|
|
4138
6366
|
resolveStages,
|
|
6367
|
+
reviewRoom,
|
|
6368
|
+
routeRole,
|
|
4139
6369
|
rrf,
|
|
4140
6370
|
runLocalVerify,
|
|
6371
|
+
runLoop,
|
|
4141
6372
|
runNetwork,
|
|
4142
6373
|
runRole,
|
|
4143
6374
|
saveChangeMeta,
|
|
@@ -4148,19 +6379,27 @@ export {
|
|
|
4148
6379
|
sddRoot,
|
|
4149
6380
|
searchConfigPath,
|
|
4150
6381
|
searchPreamble,
|
|
6382
|
+
selectModel,
|
|
4151
6383
|
setActiveChange,
|
|
6384
|
+
setFact,
|
|
4152
6385
|
setRoleMessaging,
|
|
4153
6386
|
shouldAutoSkip,
|
|
6387
|
+
showFact,
|
|
6388
|
+
showMemory,
|
|
4154
6389
|
showRoleGroup,
|
|
4155
6390
|
showRoles,
|
|
6391
|
+
showRoom,
|
|
6392
|
+
showTeam,
|
|
4156
6393
|
skipStage,
|
|
4157
6394
|
slugify,
|
|
6395
|
+
spawnModelCommand,
|
|
4158
6396
|
startFeatureFromBacklog,
|
|
4159
6397
|
startGreenfield,
|
|
4160
6398
|
startPhase,
|
|
4161
6399
|
storyIdTaken,
|
|
4162
6400
|
switchWorkflow,
|
|
4163
6401
|
syncChangeHead,
|
|
6402
|
+
takeRole,
|
|
4164
6403
|
templatesDir,
|
|
4165
6404
|
toProcessEnv,
|
|
4166
6405
|
todayIsoDate,
|
|
@@ -4171,6 +6410,8 @@ export {
|
|
|
4171
6410
|
updateFeatureStatus,
|
|
4172
6411
|
validBranchName,
|
|
4173
6412
|
withMcpSource,
|
|
6413
|
+
workKind,
|
|
6414
|
+
workflowUnavailableMessage,
|
|
4174
6415
|
workflowsDir,
|
|
4175
6416
|
writeAgentHandoff,
|
|
4176
6417
|
writeRefineBrief,
|