@structured-vibe-coding/core 0.19.0 → 0.20.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/software/routing.yaml +0 -24
- package/defaults/templates/qa-testing.md +11 -0
- package/defaults/templates/signoff.md +7 -0
- package/defaults/workflows/team-software.yaml +188 -48
- package/dist/{agents-PSIOQEYH.js → agents-YN3Z3VOH.js} +3 -3
- package/dist/{change-context-EZJMSGSP.js → change-context-CZT3U7MZ.js} +1 -1
- package/dist/{chunk-H55RFAEI.js → chunk-EDXT3RTN.js} +1 -1
- package/dist/{chunk-TGRV6WGS.js → chunk-HUSXETPS.js} +23 -2
- package/dist/{chunk-2FJBFYVJ.js → chunk-NY5FFWXG.js} +7 -7
- package/dist/{chunk-26YG6A4M.js → chunk-O4OQWVC7.js} +2 -2
- package/dist/{chunk-HG7TPW5V.js → chunk-TNWFMRIA.js} +46 -2
- package/dist/{gather-7TSVKQO2.js → gather-ZKVMJW5L.js} +1 -1
- package/dist/{greenfield-QFA5WVHE.js → greenfield-O2DI27HY.js} +3 -3
- package/dist/index.d.ts +30 -1
- package/dist/index.js +881 -276
- package/package.json +1 -1
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-EDXT3RTN.js";
|
|
15
15
|
import {
|
|
16
16
|
McpConfigSchema,
|
|
17
17
|
McpInvokeSchema,
|
|
@@ -68,7 +68,7 @@ import {
|
|
|
68
68
|
renderThinAgent,
|
|
69
69
|
saveGeneratedManifest,
|
|
70
70
|
writeAgentHandoff
|
|
71
|
-
} from "./chunk-
|
|
71
|
+
} from "./chunk-O4OQWVC7.js";
|
|
72
72
|
import {
|
|
73
73
|
ChangeMetricsSchema,
|
|
74
74
|
DecisionMetricSchema,
|
|
@@ -100,7 +100,7 @@ import {
|
|
|
100
100
|
switchWorkflow,
|
|
101
101
|
transitionPhase,
|
|
102
102
|
updateFeatureStatus
|
|
103
|
-
} from "./chunk-
|
|
103
|
+
} from "./chunk-NY5FFWXG.js";
|
|
104
104
|
import {
|
|
105
105
|
EMBEDDINGS_UNAVAILABLE,
|
|
106
106
|
EMBEDDING_MODEL,
|
|
@@ -178,7 +178,9 @@ import {
|
|
|
178
178
|
resolveBot,
|
|
179
179
|
resolveEmbedder,
|
|
180
180
|
resolveSearchProvider,
|
|
181
|
+
roleContextDir,
|
|
181
182
|
roleFilePath,
|
|
183
|
+
rolesListingStage,
|
|
182
184
|
rrf,
|
|
183
185
|
saveConfig,
|
|
184
186
|
scopeForPath,
|
|
@@ -188,7 +190,7 @@ import {
|
|
|
188
190
|
showTeam,
|
|
189
191
|
workKind,
|
|
190
192
|
writePackedCache
|
|
191
|
-
} from "./chunk-
|
|
193
|
+
} from "./chunk-HUSXETPS.js";
|
|
192
194
|
import {
|
|
193
195
|
ArtifactDefSchema,
|
|
194
196
|
ChangeMetaSchema,
|
|
@@ -232,7 +234,7 @@ import {
|
|
|
232
234
|
todayIsoDate,
|
|
233
235
|
uniqueChangeDirName,
|
|
234
236
|
workflowUnavailableMessage
|
|
235
|
-
} from "./chunk-
|
|
237
|
+
} from "./chunk-TNWFMRIA.js";
|
|
236
238
|
import {
|
|
237
239
|
SddError,
|
|
238
240
|
errorMessage,
|
|
@@ -1393,7 +1395,7 @@ ${output}`);
|
|
|
1393
1395
|
status = failed.length ? "fail" : "pass";
|
|
1394
1396
|
reason = failed.length ? `failed: ${failed.join(", ")}` : void 0;
|
|
1395
1397
|
}
|
|
1396
|
-
const
|
|
1398
|
+
const ok10 = status === "pass";
|
|
1397
1399
|
const checklist = stage.gate?.checklist ?? [];
|
|
1398
1400
|
const lines = buildSummaryLines(
|
|
1399
1401
|
ctx.meta.title,
|
|
@@ -1444,7 +1446,7 @@ ${output}`);
|
|
|
1444
1446
|
ctx.meta.verify_results = {
|
|
1445
1447
|
...ctx.meta.verify_results,
|
|
1446
1448
|
[stage.id]: {
|
|
1447
|
-
ok:
|
|
1449
|
+
ok: ok10,
|
|
1448
1450
|
status,
|
|
1449
1451
|
...results.length && headSha2 ? { head_sha: headSha2 } : {},
|
|
1450
1452
|
at: nowIso(),
|
|
@@ -1460,7 +1462,7 @@ ${output}`);
|
|
|
1460
1462
|
checklist,
|
|
1461
1463
|
evidencePath: evidenceDir,
|
|
1462
1464
|
resultsPath: summaryPath,
|
|
1463
|
-
ok:
|
|
1465
|
+
ok: ok10
|
|
1464
1466
|
};
|
|
1465
1467
|
}
|
|
1466
1468
|
|
|
@@ -1873,10 +1875,10 @@ function formatMarkdown(input) {
|
|
|
1873
1875
|
`;
|
|
1874
1876
|
return { markdown: packed ? frame + packed : frame, truncated: input.summary.truncated };
|
|
1875
1877
|
}
|
|
1876
|
-
function formatJsonSummary(summary, gaps, slices,
|
|
1878
|
+
function formatJsonSummary(summary, gaps, slices, ok10) {
|
|
1877
1879
|
return JSON.stringify(
|
|
1878
1880
|
{
|
|
1879
|
-
ok:
|
|
1881
|
+
ok: ok10,
|
|
1880
1882
|
summary,
|
|
1881
1883
|
gaps,
|
|
1882
1884
|
slices: slices.map((s) => ({
|
|
@@ -2641,11 +2643,11 @@ Run: \`sdd refine${plan.focusStageId ? ` ${plan.focusStageId}` : ""}\`
|
|
|
2641
2643
|
}
|
|
2642
2644
|
|
|
2643
2645
|
// src/role/handoff.ts
|
|
2644
|
-
import { join as
|
|
2646
|
+
import { join as join17 } from "pathe";
|
|
2645
2647
|
import { parse as parseYaml3, stringify as stringifyYaml } from "yaml";
|
|
2646
2648
|
|
|
2647
2649
|
// src/role/routing.ts
|
|
2648
|
-
import { join as
|
|
2650
|
+
import { join as join16 } from "pathe";
|
|
2649
2651
|
import { parse as parseYaml2 } from "yaml";
|
|
2650
2652
|
import { z as z6 } from "zod";
|
|
2651
2653
|
|
|
@@ -3064,19 +3066,143 @@ async function decide(input) {
|
|
|
3064
3066
|
|
|
3065
3067
|
// src/role/run.ts
|
|
3066
3068
|
import { appendFile as appendFile5 } from "fs/promises";
|
|
3067
|
-
import { isAbsolute as isAbsolute4, join as
|
|
3069
|
+
import { isAbsolute as isAbsolute4, join as join15 } from "pathe";
|
|
3068
3070
|
|
|
3069
|
-
// src/
|
|
3070
|
-
import { appendFile } from "fs/promises";
|
|
3071
|
+
// src/role/context.ts
|
|
3071
3072
|
import { join as join9 } from "pathe";
|
|
3073
|
+
var NO_SLICE = "No repo slice matched this role.";
|
|
3072
3074
|
function ok(stdout = "") {
|
|
3073
3075
|
return { exitCode: 0, stdout };
|
|
3074
3076
|
}
|
|
3075
3077
|
function fail(stdout) {
|
|
3076
3078
|
return { exitCode: 1, stdout };
|
|
3077
3079
|
}
|
|
3080
|
+
function contextPath(root, name) {
|
|
3081
|
+
return join9(roleContextDir(root, name), "context.md");
|
|
3082
|
+
}
|
|
3083
|
+
function knowledgePath(root, name) {
|
|
3084
|
+
return join9(roleContextDir(root, name), "knowledge.md");
|
|
3085
|
+
}
|
|
3086
|
+
async function readStoredContext(root, name) {
|
|
3087
|
+
const path = contextPath(root, name);
|
|
3088
|
+
if (!await pathExists(path)) return null;
|
|
3089
|
+
const raw = await readText(path);
|
|
3090
|
+
const match = /^---\r?\nsource: (provided|generated)\r?\n---\r?\n?/.exec(raw);
|
|
3091
|
+
if (!match) return { source: "provided", body: raw.replace(/\s*$/, "") };
|
|
3092
|
+
const source = match[1] === "generated" ? "generated" : "provided";
|
|
3093
|
+
return { source, body: raw.slice(match[0].length).replace(/\s*$/, "") };
|
|
3094
|
+
}
|
|
3095
|
+
async function roleContextBody(root, name) {
|
|
3096
|
+
const stored = await readStoredContext(root, name);
|
|
3097
|
+
return stored?.body.trim() ?? "";
|
|
3098
|
+
}
|
|
3099
|
+
async function readKnowledge(root, name) {
|
|
3100
|
+
const path = knowledgePath(root, name);
|
|
3101
|
+
if (!await pathExists(path)) return "";
|
|
3102
|
+
return (await readText(path)).trim();
|
|
3103
|
+
}
|
|
3104
|
+
async function writeKnowledge(root, name, text2) {
|
|
3105
|
+
await ensureDir(roleContextDir(root, name));
|
|
3106
|
+
const body = text2.trim();
|
|
3107
|
+
if (!body) {
|
|
3108
|
+
await removePath(knowledgePath(root, name));
|
|
3109
|
+
return;
|
|
3110
|
+
}
|
|
3111
|
+
await writeText(knowledgePath(root, name), `${body}
|
|
3112
|
+
`);
|
|
3113
|
+
}
|
|
3114
|
+
function renderStored(source, body) {
|
|
3115
|
+
return `---
|
|
3116
|
+
source: ${source}
|
|
3117
|
+
---
|
|
3118
|
+
${body.trim()}
|
|
3119
|
+
`;
|
|
3120
|
+
}
|
|
3121
|
+
function buildGeneratedContext(input) {
|
|
3122
|
+
const rows = [`Role: ${input.name}`, `Job: ${input.job}`];
|
|
3123
|
+
if (input.knowledge.trim()) rows.push("", "Knowledge:", input.knowledge.trim());
|
|
3124
|
+
rows.push("", "Repo:", input.repo.trim() || NO_SLICE);
|
|
3125
|
+
return rows.join("\n");
|
|
3126
|
+
}
|
|
3127
|
+
async function repoSlice(root, name, job, knowledge) {
|
|
3128
|
+
const query = [name, job, knowledge].filter((part) => part.trim()).join("\n");
|
|
3129
|
+
let seeds = [];
|
|
3130
|
+
try {
|
|
3131
|
+
const focused = await resolveFocus({ projectRoot: root, query });
|
|
3132
|
+
seeds = focused.plan.seedPaths.filter((rel) => !isSecretPath(rel)).slice(0, 12);
|
|
3133
|
+
} catch {
|
|
3134
|
+
seeds = [];
|
|
3135
|
+
}
|
|
3136
|
+
try {
|
|
3137
|
+
const found = await generateCodeContext({
|
|
3138
|
+
projectRoot: root,
|
|
3139
|
+
query,
|
|
3140
|
+
paths: seeds.length > 0 ? seeds : void 0,
|
|
3141
|
+
caps: { maxSlices: 8, maxTokensApprox: 2e3, maxLinesPerSlice: 40, maxFiles: 12 }
|
|
3142
|
+
});
|
|
3143
|
+
if (found.ok && found.slices.length > 0 && found.markdown.trim()) return found.markdown.trim();
|
|
3144
|
+
} catch {
|
|
3145
|
+
}
|
|
3146
|
+
const blocks = [];
|
|
3147
|
+
for (const rel of seeds) {
|
|
3148
|
+
const abs = join9(root, rel);
|
|
3149
|
+
if (!await pathExists(abs)) continue;
|
|
3150
|
+
let text2 = "";
|
|
3151
|
+
try {
|
|
3152
|
+
text2 = await readText(abs);
|
|
3153
|
+
} catch {
|
|
3154
|
+
continue;
|
|
3155
|
+
}
|
|
3156
|
+
const body = text2.split(/\r?\n/).slice(0, 40).join("\n").trim();
|
|
3157
|
+
if (!body) continue;
|
|
3158
|
+
blocks.push(`${rel}
|
|
3159
|
+
${body}`);
|
|
3160
|
+
if (blocks.length >= 8) break;
|
|
3161
|
+
}
|
|
3162
|
+
return blocks.join("\n\n") || NO_SLICE;
|
|
3163
|
+
}
|
|
3164
|
+
async function setRoleContext(root, input) {
|
|
3165
|
+
const name = canonicalRoleName(input.name);
|
|
3166
|
+
if (!name) return fail(ROLE_TEXT.badName);
|
|
3167
|
+
const role = await readRole(root, name);
|
|
3168
|
+
if (!role) return fail(ROLE_TEXT.notFound);
|
|
3169
|
+
if (input.knowledge !== void 0) await writeKnowledge(root, name, input.knowledge);
|
|
3170
|
+
let knowledge = await readKnowledge(root, name);
|
|
3171
|
+
const extra = input.generate && input.text !== void 0 ? input.text.trim() : "";
|
|
3172
|
+
if (extra && input.knowledge === void 0) {
|
|
3173
|
+
await writeKnowledge(root, name, extra);
|
|
3174
|
+
knowledge = extra;
|
|
3175
|
+
} else if (extra) {
|
|
3176
|
+
knowledge = [knowledge, extra].filter((part) => part.length > 0).join("\n");
|
|
3177
|
+
}
|
|
3178
|
+
if (input.text !== void 0 && !input.generate) {
|
|
3179
|
+
const text2 = input.text.trim();
|
|
3180
|
+
if (!text2) return fail("A context needs text.");
|
|
3181
|
+
await ensureDir(roleContextDir(root, name));
|
|
3182
|
+
await writeText(contextPath(root, name), renderStored("provided", text2));
|
|
3183
|
+
return ok(text2);
|
|
3184
|
+
}
|
|
3185
|
+
const saved = await readStoredContext(root, name);
|
|
3186
|
+
if (!input.generate && input.knowledge !== void 0) return ok(saved?.body || knowledge);
|
|
3187
|
+
if (!input.generate && saved) return ok(saved.body);
|
|
3188
|
+
const repo = await repoSlice(root, name, role.job, knowledge);
|
|
3189
|
+
const body = buildGeneratedContext({ name: role.name, job: role.job, knowledge, repo });
|
|
3190
|
+
await ensureDir(roleContextDir(root, name));
|
|
3191
|
+
await writeText(contextPath(root, name), renderStored("generated", body));
|
|
3192
|
+
return ok(body);
|
|
3193
|
+
}
|
|
3194
|
+
|
|
3195
|
+
// src/team/facts.ts
|
|
3196
|
+
import { appendFile } from "fs/promises";
|
|
3197
|
+
import { join as join10 } from "pathe";
|
|
3198
|
+
function ok2(stdout = "") {
|
|
3199
|
+
return { exitCode: 0, stdout };
|
|
3200
|
+
}
|
|
3201
|
+
function fail2(stdout) {
|
|
3202
|
+
return { exitCode: 1, stdout };
|
|
3203
|
+
}
|
|
3078
3204
|
function factsPath(root) {
|
|
3079
|
-
return
|
|
3205
|
+
return join10(root, ".sdd", "facts.jsonl");
|
|
3080
3206
|
}
|
|
3081
3207
|
function sharesWords(left, right) {
|
|
3082
3208
|
const bag = wordsOf(left);
|
|
@@ -3135,13 +3261,13 @@ async function setFact(root, input) {
|
|
|
3135
3261
|
const rawValue = input.value ?? "";
|
|
3136
3262
|
const rawSource = input.source ?? "";
|
|
3137
3263
|
if (rawKey.includes("\n") || rawKey.includes("\r") || rawValue.includes("\n") || rawValue.includes("\r") || rawSource.includes("\n") || rawSource.includes("\r")) {
|
|
3138
|
-
return
|
|
3264
|
+
return fail2("A fact is one line.");
|
|
3139
3265
|
}
|
|
3140
3266
|
const key = rawKey.trim();
|
|
3141
3267
|
const value = rawValue.trim();
|
|
3142
|
-
if (!key || !value) return
|
|
3268
|
+
if (!key || !value) return fail2("A fact needs a key and a value.");
|
|
3143
3269
|
if (key.includes("/") || key.includes("\\") || key.includes("\0")) {
|
|
3144
|
-
return
|
|
3270
|
+
return fail2("A fact key is not valid.");
|
|
3145
3271
|
}
|
|
3146
3272
|
if (await contradicts(root, value)) return escalate(root, key);
|
|
3147
3273
|
const fact = {
|
|
@@ -3152,33 +3278,33 @@ async function setFact(root, input) {
|
|
|
3152
3278
|
source: rawSource.trim() || "person"
|
|
3153
3279
|
};
|
|
3154
3280
|
const file = factsPath(root);
|
|
3155
|
-
await ensureDir(
|
|
3281
|
+
await ensureDir(join10(file, ".."));
|
|
3156
3282
|
await appendFile(file, `${JSON.stringify(fact)}
|
|
3157
3283
|
`, "utf8");
|
|
3158
|
-
return
|
|
3284
|
+
return ok2("Fact set.");
|
|
3159
3285
|
}
|
|
3160
3286
|
async function showFact(root, key) {
|
|
3161
3287
|
const name = (key ?? "").trim();
|
|
3162
|
-
if (!name) return
|
|
3288
|
+
if (!name) return fail2("A fact needs a key and a value.");
|
|
3163
3289
|
const facts = await getFacts(root, { keys: [name] });
|
|
3164
3290
|
const hit = facts[0];
|
|
3165
|
-
if (!hit) return
|
|
3166
|
-
return
|
|
3291
|
+
if (!hit) return fail2("Fact not set.");
|
|
3292
|
+
return ok2(hit.value);
|
|
3167
3293
|
}
|
|
3168
3294
|
async function listFacts(root, query) {
|
|
3169
3295
|
const facts = await getFacts(root, query ? { query } : void 0);
|
|
3170
|
-
if (facts.length === 0) return
|
|
3171
|
-
return
|
|
3296
|
+
if (facts.length === 0) return ok2("No facts.");
|
|
3297
|
+
return ok2(facts.map((fact) => `${fact.key}: ${fact.value}`).join("\n"));
|
|
3172
3298
|
}
|
|
3173
3299
|
async function contradicts(root, value) {
|
|
3174
3300
|
if (!/\b(ignore|skip|override)\b/i.test(value)) return false;
|
|
3175
3301
|
let text2 = "";
|
|
3176
3302
|
try {
|
|
3177
3303
|
const config = await loadConfig(root);
|
|
3178
|
-
const file =
|
|
3304
|
+
const file = join10(root, guidancePaths(config).constitution);
|
|
3179
3305
|
if (await pathExists(file)) text2 = await readText(file);
|
|
3180
3306
|
} catch {
|
|
3181
|
-
const file =
|
|
3307
|
+
const file = join10(root, "memory", "constitution.md");
|
|
3182
3308
|
if (await pathExists(file)) text2 = await readText(file);
|
|
3183
3309
|
}
|
|
3184
3310
|
for (const line of text2.split(/\r?\n/)) {
|
|
@@ -3197,33 +3323,33 @@ async function escalate(root, key) {
|
|
|
3197
3323
|
} catch {
|
|
3198
3324
|
changeId = null;
|
|
3199
3325
|
}
|
|
3200
|
-
if (!changeId) return
|
|
3326
|
+
if (!changeId) return fail2("A fact contradicts the constitution.");
|
|
3201
3327
|
return recordDecisionRequest(root, { note, change: changeId });
|
|
3202
3328
|
}
|
|
3203
3329
|
|
|
3204
3330
|
// src/team/memory.ts
|
|
3205
3331
|
import { appendFile as appendFile2 } from "fs/promises";
|
|
3206
|
-
import { join as
|
|
3332
|
+
import { join as join11 } from "pathe";
|
|
3207
3333
|
var BRAIN_FILES = ["memory.md", "lessons.jsonl"];
|
|
3208
|
-
function
|
|
3334
|
+
function ok3(stdout = "") {
|
|
3209
3335
|
return { exitCode: 0, stdout };
|
|
3210
3336
|
}
|
|
3211
|
-
function
|
|
3337
|
+
function fail3(stdout) {
|
|
3212
3338
|
return { exitCode: 1, stdout };
|
|
3213
3339
|
}
|
|
3214
3340
|
async function knownBot(root, name) {
|
|
3215
3341
|
const team = await readTeamConfig(root);
|
|
3216
|
-
if ("error" in team) return
|
|
3217
|
-
if (!team.team.bots.some((bot) => bot.name === name)) return
|
|
3342
|
+
if ("error" in team) return fail3(team.error);
|
|
3343
|
+
if (!team.team.bots.some((bot) => bot.name === name)) return fail3("Bot not found.");
|
|
3218
3344
|
return null;
|
|
3219
3345
|
}
|
|
3220
3346
|
async function readMemory(root, bot) {
|
|
3221
|
-
const file =
|
|
3347
|
+
const file = join11(memoryDir2(root, bot, true), "memory.md");
|
|
3222
3348
|
if (!await pathExists(file)) return "";
|
|
3223
3349
|
return readText(file);
|
|
3224
3350
|
}
|
|
3225
3351
|
async function readLessons(root, bot) {
|
|
3226
|
-
const file =
|
|
3352
|
+
const file = join11(memoryDir2(root, bot, true), "lessons.jsonl");
|
|
3227
3353
|
if (!await pathExists(file)) return [];
|
|
3228
3354
|
const lessons = [];
|
|
3229
3355
|
for (const line of (await readText(file)).split(/\r?\n/)) {
|
|
@@ -3247,35 +3373,35 @@ async function readLessons(root, bot) {
|
|
|
3247
3373
|
return lessons;
|
|
3248
3374
|
}
|
|
3249
3375
|
async function appendLesson(root, bot, lesson) {
|
|
3250
|
-
const file =
|
|
3251
|
-
await ensureDir(
|
|
3376
|
+
const file = join11(memoryDir2(root, bot, true), "lessons.jsonl");
|
|
3377
|
+
await ensureDir(join11(file, ".."));
|
|
3252
3378
|
await appendFile2(file, `${JSON.stringify(lesson)}
|
|
3253
3379
|
`, "utf8");
|
|
3254
3380
|
}
|
|
3255
3381
|
async function showMemory(root, bot) {
|
|
3256
3382
|
const name = canonicalRoleName(bot);
|
|
3257
|
-
if (!name) return
|
|
3383
|
+
if (!name) return fail3(ROLE_TEXT.badName);
|
|
3258
3384
|
const missing = await knownBot(root, name);
|
|
3259
3385
|
if (missing) return missing;
|
|
3260
3386
|
const text2 = await readMemory(root, name);
|
|
3261
|
-
if (!text2) return
|
|
3262
|
-
return
|
|
3387
|
+
if (!text2) return ok3("No memory.");
|
|
3388
|
+
return ok3(text2.endsWith("\n") ? text2.slice(0, -1) : text2);
|
|
3263
3389
|
}
|
|
3264
3390
|
async function addLesson(root, bot, text2) {
|
|
3265
3391
|
const name = canonicalRoleName(bot);
|
|
3266
|
-
if (!name) return
|
|
3392
|
+
if (!name) return fail3(ROLE_TEXT.badName);
|
|
3267
3393
|
const missing = await knownBot(root, name);
|
|
3268
3394
|
if (missing) return missing;
|
|
3269
|
-
if (text2.includes("\n") || text2.includes("\r")) return
|
|
3395
|
+
if (text2.includes("\n") || text2.includes("\r")) return fail3("A lesson is one line.");
|
|
3270
3396
|
const words = text2.trim();
|
|
3271
|
-
if (!words) return
|
|
3397
|
+
if (!words) return fail3("A lesson needs text.");
|
|
3272
3398
|
await appendLesson(root, name, {
|
|
3273
3399
|
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3274
3400
|
by: gitUserName(root) ?? "unknown",
|
|
3275
3401
|
text: words,
|
|
3276
3402
|
source: "person"
|
|
3277
3403
|
});
|
|
3278
|
-
return
|
|
3404
|
+
return ok3("Lesson added.");
|
|
3279
3405
|
}
|
|
3280
3406
|
async function addReviewerLesson(root, bot, lesson) {
|
|
3281
3407
|
const text2 = lesson.text.trim();
|
|
@@ -3291,30 +3417,30 @@ async function addReviewerLesson(root, bot, lesson) {
|
|
|
3291
3417
|
}
|
|
3292
3418
|
async function migrateTeamMemory(root) {
|
|
3293
3419
|
const team = await readTeamConfig(root);
|
|
3294
|
-
if ("error" in team) return
|
|
3295
|
-
const base =
|
|
3420
|
+
if ("error" in team) return fail3(team.error);
|
|
3421
|
+
const base = join11(root, ".sdd", "role-runs", "memory");
|
|
3296
3422
|
const lines = [];
|
|
3297
3423
|
for (const name of (await listDirs(base)).sort()) {
|
|
3298
3424
|
if (!canonicalRoleName(name)) continue;
|
|
3299
3425
|
for (const file of BRAIN_FILES) {
|
|
3300
|
-
const from =
|
|
3426
|
+
const from = join11(base, name, file);
|
|
3301
3427
|
if (!await pathExists(from)) continue;
|
|
3302
3428
|
const fromRel = `.sdd/role-runs/memory/${name}/${file}`;
|
|
3303
3429
|
const toRel = `.sdd/agents/${name}/${file}`;
|
|
3304
|
-
const to =
|
|
3430
|
+
const to = join11(memoryDir2(root, name, true), file);
|
|
3305
3431
|
if (await pathExists(to)) {
|
|
3306
3432
|
lines.push(`left ${fromRel}`);
|
|
3307
3433
|
continue;
|
|
3308
3434
|
}
|
|
3309
|
-
await ensureDir(
|
|
3435
|
+
await ensureDir(join11(to, ".."));
|
|
3310
3436
|
await writeText(to, await readText(from));
|
|
3311
3437
|
await removePath(from);
|
|
3312
3438
|
lines.push(`moved ${fromRel} -> ${toRel}`);
|
|
3313
3439
|
}
|
|
3314
|
-
if ((await listFiles(
|
|
3440
|
+
if ((await listFiles(join11(base, name))).length === 0) await removePath(join11(base, name));
|
|
3315
3441
|
}
|
|
3316
|
-
if (lines.length === 0) return
|
|
3317
|
-
return
|
|
3442
|
+
if (lines.length === 0) return ok3("Nothing to migrate.");
|
|
3443
|
+
return ok3(lines.join("\n"));
|
|
3318
3444
|
}
|
|
3319
3445
|
|
|
3320
3446
|
// src/team/brain.ts
|
|
@@ -3327,11 +3453,13 @@ async function loadBrain(root, cfg, bot, item, budgetTokens) {
|
|
|
3327
3453
|
const file = roleFilePath(root, spec.role);
|
|
3328
3454
|
if (!await pathExists(file)) return { error: ROLE_TEXT.notFound };
|
|
3329
3455
|
const roleText = await readText(file);
|
|
3456
|
+
const context = await roleContextBody(root, spec.role);
|
|
3330
3457
|
const memoryAll = await readMemory(root, spec.name);
|
|
3331
3458
|
const lessonsNewest = (await readLessons(root, spec.name)).slice().reverse();
|
|
3332
3459
|
const factsNewest = (await getFacts(root, { query: item.text })).slice().reverse();
|
|
3333
3460
|
const packed = pack({
|
|
3334
3461
|
roleText,
|
|
3462
|
+
context,
|
|
3335
3463
|
memoryAll,
|
|
3336
3464
|
lessons: lessonsNewest,
|
|
3337
3465
|
facts: factsNewest,
|
|
@@ -3353,12 +3481,15 @@ function pack(input) {
|
|
|
3353
3481
|
let memory = input.memoryAll;
|
|
3354
3482
|
let lessons = input.lessons.slice();
|
|
3355
3483
|
let facts = input.facts.slice();
|
|
3484
|
+
let context = input.context;
|
|
3356
3485
|
let warning = "";
|
|
3357
3486
|
const itemBlock = `Item:
|
|
3358
3487
|
${input.item}`;
|
|
3359
3488
|
const render = () => {
|
|
3360
3489
|
const parts = [input.roleText.trimEnd()];
|
|
3361
3490
|
if (warning) parts.push(warning);
|
|
3491
|
+
if (context.trim()) parts.push(`Context:
|
|
3492
|
+
${context.trimEnd()}`);
|
|
3362
3493
|
if (memory.trim()) parts.push(`Memory:
|
|
3363
3494
|
${memory.trimEnd()}`);
|
|
3364
3495
|
if (lessons.length > 0) {
|
|
@@ -3377,9 +3508,11 @@ ${facts.map((fact) => `${fact.key}: ${fact.value}`).join("\n")}`);
|
|
|
3377
3508
|
memory = "";
|
|
3378
3509
|
lessons = [];
|
|
3379
3510
|
facts = [];
|
|
3511
|
+
context = "";
|
|
3380
3512
|
warning = input.memoryAll.trim() ? WARNING : "";
|
|
3381
3513
|
return { memory, lessons, facts, prompt: render(), warning };
|
|
3382
3514
|
}
|
|
3515
|
+
if (countTokens(render()) > input.budget && context.trim()) context = "";
|
|
3383
3516
|
while (countTokens(render()) > input.budget && facts.length > 0) facts.pop();
|
|
3384
3517
|
while (countTokens(render()) > input.budget && lessons.length > 0) lessons.pop();
|
|
3385
3518
|
if (countTokens(render()) > input.budget && memory.trim()) {
|
|
@@ -3400,12 +3533,12 @@ ${facts.map((fact) => `${fact.key}: ${fact.value}`).join("\n")}`);
|
|
|
3400
3533
|
|
|
3401
3534
|
// src/team/loop.ts
|
|
3402
3535
|
import { appendFile as appendFile4 } from "fs/promises";
|
|
3403
|
-
import { basename, join as
|
|
3536
|
+
import { basename, join as join14 } from "pathe";
|
|
3404
3537
|
import { z as z5 } from "zod";
|
|
3405
3538
|
|
|
3406
3539
|
// src/team/room.ts
|
|
3407
3540
|
import { appendFile as appendFile3 } from "fs/promises";
|
|
3408
|
-
import { join as
|
|
3541
|
+
import { join as join13 } from "pathe";
|
|
3409
3542
|
import { z as z4 } from "zod";
|
|
3410
3543
|
|
|
3411
3544
|
// src/role/prompts.ts
|
|
@@ -3434,7 +3567,7 @@ function revisionPrompt(role, draft, gaps) {
|
|
|
3434
3567
|
import { spawn as spawn4 } from "child_process";
|
|
3435
3568
|
import { mkdtemp, rm } from "fs/promises";
|
|
3436
3569
|
import { tmpdir } from "os";
|
|
3437
|
-
import { isAbsolute as isAbsolute3, join as
|
|
3570
|
+
import { isAbsolute as isAbsolute3, join as join12 } from "pathe";
|
|
3438
3571
|
import { z as z3 } from "zod";
|
|
3439
3572
|
var OUTPUT_CAP = 4e3;
|
|
3440
3573
|
var SCOPES = /* @__PURE__ */ new Set(["code", "specs", "memory"]);
|
|
@@ -3475,8 +3608,8 @@ function asRecord(value) {
|
|
|
3475
3608
|
if (!value || typeof value !== "object" || Array.isArray(value)) return {};
|
|
3476
3609
|
return value;
|
|
3477
3610
|
}
|
|
3478
|
-
function result(name,
|
|
3479
|
-
return { ok:
|
|
3611
|
+
function result(name, ok10, text2) {
|
|
3612
|
+
return { ok: ok10, name, text: clip(text2) };
|
|
3480
3613
|
}
|
|
3481
3614
|
async function git(cwd, args) {
|
|
3482
3615
|
return new Promise((resolve) => {
|
|
@@ -3525,7 +3658,7 @@ function evidenceRel(changesPath, changeId, sha) {
|
|
|
3525
3658
|
}
|
|
3526
3659
|
async function evidenceFile(root, changeId, sha) {
|
|
3527
3660
|
const config = await loadConfig(root);
|
|
3528
|
-
return
|
|
3661
|
+
return join12(changePath(root, config, changeId), "evidence", `${sha}.json`);
|
|
3529
3662
|
}
|
|
3530
3663
|
async function evidenceSupports(root, changeId, cite) {
|
|
3531
3664
|
const sha = await headOf(root);
|
|
@@ -3533,7 +3666,7 @@ async function evidenceSupports(root, changeId, cite) {
|
|
|
3533
3666
|
const config = await loadConfig(root);
|
|
3534
3667
|
const expected = evidenceRel(config.changes_path, changeId, sha);
|
|
3535
3668
|
if (cite.trim().replace(/\\/g, "/").replace(/^\.\//, "") !== expected) return false;
|
|
3536
|
-
const file =
|
|
3669
|
+
const file = join12(root, ...expected.split("/"));
|
|
3537
3670
|
if (!await pathExists(file)) return false;
|
|
3538
3671
|
let parsed;
|
|
3539
3672
|
try {
|
|
@@ -3564,7 +3697,7 @@ async function allowList(root) {
|
|
|
3564
3697
|
const stage = getStage(ctx.workflow, ctx.meta, ctx.meta.stage);
|
|
3565
3698
|
for (const command of stage?.verify?.commands ?? []) stageRuns.push(command.run);
|
|
3566
3699
|
}
|
|
3567
|
-
const file =
|
|
3700
|
+
const file = join12(root, ".sdd", "tools.yaml");
|
|
3568
3701
|
if (!await pathExists(file)) return stageRuns;
|
|
3569
3702
|
let raw;
|
|
3570
3703
|
try {
|
|
@@ -3584,7 +3717,7 @@ async function readFileTool(root, args) {
|
|
|
3584
3717
|
const rel = typeof args.path === "string" ? args.path : "";
|
|
3585
3718
|
if (!safePath(rel)) return result("read_file", false, "That path is outside the project.");
|
|
3586
3719
|
if (isSecretPath(rel)) return result("read_file", false, "That path is not readable.");
|
|
3587
|
-
const file =
|
|
3720
|
+
const file = join12(root, ...rel.split(/[/\\]/));
|
|
3588
3721
|
if (!await pathExists(file)) return result("read_file", false, "That path is not readable.");
|
|
3589
3722
|
try {
|
|
3590
3723
|
const text2 = await readText(file);
|
|
@@ -3641,7 +3774,7 @@ async function appendRun(root, changeId, sha, command, exitCode, output) {
|
|
|
3641
3774
|
}
|
|
3642
3775
|
}
|
|
3643
3776
|
body.runs.push({ command, exitCode, output: clip(output) });
|
|
3644
|
-
await ensureDir(
|
|
3777
|
+
await ensureDir(join12(file, ".."));
|
|
3645
3778
|
await writeText(file, `${JSON.stringify(body, null, 2)}
|
|
3646
3779
|
`);
|
|
3647
3780
|
const config = await loadConfig(root);
|
|
@@ -3661,7 +3794,7 @@ async function runCheck(root, args) {
|
|
|
3661
3794
|
if (!change) return result("run_check", false, "No active change.");
|
|
3662
3795
|
const sha = await headOf(root);
|
|
3663
3796
|
if (!sha) return result("run_check", false, "Git HEAD is unavailable.");
|
|
3664
|
-
const dir = await mkdtemp(
|
|
3797
|
+
const dir = await mkdtemp(join12(tmpdir(), "sdd-check-"));
|
|
3665
3798
|
const added = await git(root, ["worktree", "add", "--detach", dir, sha]);
|
|
3666
3799
|
if (added.code !== 0) {
|
|
3667
3800
|
await rm(dir, { recursive: true, force: true });
|
|
@@ -3688,7 +3821,7 @@ async function writeFileTool(root, args) {
|
|
|
3688
3821
|
if (!safePath(rel) || typeof contents !== "string" || isSecretPath(rel)) {
|
|
3689
3822
|
return result("write_file", false, "That file cannot be written.");
|
|
3690
3823
|
}
|
|
3691
|
-
await writeText(
|
|
3824
|
+
await writeText(join12(root, ...rel.split(/[/\\]/)), contents);
|
|
3692
3825
|
return result("write_file", true, `Wrote ${rel}.`);
|
|
3693
3826
|
}
|
|
3694
3827
|
function branchName(team, bot, changeId) {
|
|
@@ -3764,10 +3897,10 @@ var RoomEntrySchema = z4.object({
|
|
|
3764
3897
|
sha: z4.string().optional(),
|
|
3765
3898
|
evidence: z4.array(z4.string()).optional()
|
|
3766
3899
|
}).strict();
|
|
3767
|
-
function
|
|
3900
|
+
function ok4(stdout = "") {
|
|
3768
3901
|
return { exitCode: 0, stdout };
|
|
3769
3902
|
}
|
|
3770
|
-
function
|
|
3903
|
+
function fail4(stdout) {
|
|
3771
3904
|
return { exitCode: 1, stdout };
|
|
3772
3905
|
}
|
|
3773
3906
|
function safeId(value) {
|
|
@@ -3775,13 +3908,13 @@ function safeId(value) {
|
|
|
3775
3908
|
}
|
|
3776
3909
|
function oneLine(value, empty, broken) {
|
|
3777
3910
|
const raw = value ?? "";
|
|
3778
|
-
if (raw.includes("\n") || raw.includes("\r")) return
|
|
3911
|
+
if (raw.includes("\n") || raw.includes("\r")) return fail4(broken);
|
|
3779
3912
|
const words = raw.trim();
|
|
3780
|
-
if (!words) return
|
|
3913
|
+
if (!words) return fail4(empty);
|
|
3781
3914
|
return words;
|
|
3782
3915
|
}
|
|
3783
3916
|
function roomPath(root, change, stage) {
|
|
3784
|
-
return
|
|
3917
|
+
return join13(root, ".sdd", "rooms", change, `${stage}.jsonl`);
|
|
3785
3918
|
}
|
|
3786
3919
|
function roomRounds(team) {
|
|
3787
3920
|
return team.limits?.room_rounds ?? 2;
|
|
@@ -3820,21 +3953,28 @@ async function openChange(root, change) {
|
|
|
3820
3953
|
try {
|
|
3821
3954
|
config = await loadConfig(root);
|
|
3822
3955
|
} catch (error) {
|
|
3823
|
-
return
|
|
3956
|
+
return fail4(error instanceof Error ? error.message : "Config is invalid.");
|
|
3824
3957
|
}
|
|
3825
3958
|
const id = (change?.trim() || await getActiveChangeId(root, config) || "").trim();
|
|
3826
|
-
if (!id) return
|
|
3827
|
-
if (!safeId(id)) return
|
|
3959
|
+
if (!id) return fail4("No active change.");
|
|
3960
|
+
if (!safeId(id)) return fail4("A change id is not valid.");
|
|
3828
3961
|
try {
|
|
3829
3962
|
const ctx = await buildContext(root, config, id);
|
|
3830
3963
|
return { id, path: ctx.path, stage: ctx.meta.stage, workflow: ctx.workflow, meta: ctx.meta };
|
|
3831
3964
|
} catch (error) {
|
|
3832
|
-
return
|
|
3965
|
+
return fail4(error instanceof Error ? error.message : "Change not found.");
|
|
3833
3966
|
}
|
|
3834
3967
|
}
|
|
3835
3968
|
function isFailure(value) {
|
|
3836
3969
|
return "exitCode" in value;
|
|
3837
3970
|
}
|
|
3971
|
+
function stageMemberNames(stage, listed) {
|
|
3972
|
+
const named = [stage.owner, ...stage.collaborates ?? []].filter(
|
|
3973
|
+
(name) => !!name
|
|
3974
|
+
);
|
|
3975
|
+
if (named.length > 0) return named;
|
|
3976
|
+
return listed;
|
|
3977
|
+
}
|
|
3838
3978
|
function memberBots(team, names) {
|
|
3839
3979
|
const bots = [];
|
|
3840
3980
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -3852,7 +3992,7 @@ function memberBots(team, names) {
|
|
|
3852
3992
|
async function draftOf(changePath2, stage) {
|
|
3853
3993
|
const rel = stage?.artifacts[0]?.path;
|
|
3854
3994
|
if (!rel || rel.includes("..")) return "";
|
|
3855
|
-
const file =
|
|
3995
|
+
const file = join13(changePath2, rel);
|
|
3856
3996
|
if (!await pathExists(file)) return "";
|
|
3857
3997
|
return readText(file);
|
|
3858
3998
|
}
|
|
@@ -3861,7 +4001,7 @@ function formatEntry(entry) {
|
|
|
3861
4001
|
return `${entry.round} ${entry.from} ${entry.type}${cite} ${entry.text}`;
|
|
3862
4002
|
}
|
|
3863
4003
|
async function appendEntry(file, entry) {
|
|
3864
|
-
await ensureDir(
|
|
4004
|
+
await ensureDir(join13(file, ".."));
|
|
3865
4005
|
await appendFile3(file, `${JSON.stringify(entry)}
|
|
3866
4006
|
`, "utf8");
|
|
3867
4007
|
}
|
|
@@ -3870,36 +4010,36 @@ async function consensusLine(changePath2, stage, sha, entries) {
|
|
|
3870
4010
|
const agreed = found.agreed.join(",") || "none";
|
|
3871
4011
|
const open = found.open.map((entry) => entry.cite || entry.text).join(",") || "none";
|
|
3872
4012
|
await appendFile3(
|
|
3873
|
-
|
|
4013
|
+
join13(changePath2, "decisions.md"),
|
|
3874
4014
|
`${nowIso()} room ${stage} ${sha ?? "none"} agreed ${agreed} open ${open}
|
|
3875
4015
|
`
|
|
3876
4016
|
);
|
|
3877
4017
|
}
|
|
3878
4018
|
async function showRoom(root, input) {
|
|
3879
4019
|
const team = await readTeamConfig(root);
|
|
3880
|
-
if ("error" in team) return
|
|
4020
|
+
if ("error" in team) return fail4(team.error);
|
|
3881
4021
|
const opened = await openChange(root, input.change);
|
|
3882
4022
|
if (isFailure(opened)) return opened;
|
|
3883
4023
|
const stage = (input.stage?.trim() || opened.stage).trim();
|
|
3884
|
-
if (!safeId(stage)) return
|
|
4024
|
+
if (!safeId(stage)) return fail4("A stage id is not valid.");
|
|
3885
4025
|
const entries = await readEntries(roomPath(root, opened.id, stage));
|
|
3886
|
-
if (entries.length === 0) return
|
|
3887
|
-
return
|
|
4026
|
+
if (entries.length === 0) return ok4("No room.");
|
|
4027
|
+
return ok4(entries.map(formatEntry).join("\n"));
|
|
3888
4028
|
}
|
|
3889
4029
|
async function postRoom(root, input) {
|
|
3890
4030
|
const team = await readTeamConfig(root);
|
|
3891
|
-
if ("error" in team) return
|
|
4031
|
+
if ("error" in team) return fail4(team.error);
|
|
3892
4032
|
const opened = await openChange(root, input.change);
|
|
3893
4033
|
if (isFailure(opened)) return opened;
|
|
3894
4034
|
const stage = (input.stage?.trim() || opened.stage).trim();
|
|
3895
|
-
if (!safeId(stage)) return
|
|
4035
|
+
if (!safeId(stage)) return fail4("A stage id is not valid.");
|
|
3896
4036
|
const text2 = oneLine(input.text, "A room entry needs text.", "A room entry is one line.");
|
|
3897
4037
|
if (typeof text2 !== "string") return text2;
|
|
3898
4038
|
const type = input.type?.trim() || "post";
|
|
3899
|
-
if (!ROOM_TYPES.includes(type)) return
|
|
4039
|
+
if (!ROOM_TYPES.includes(type)) return fail4("A room entry is not valid.");
|
|
3900
4040
|
const cite = input.cite?.trim() ?? "";
|
|
3901
|
-
if (cite && (cite.includes("\n") || cite.includes("\r"))) return
|
|
3902
|
-
if (type === "objection" && !cite) return
|
|
4041
|
+
if (cite && (cite.includes("\n") || cite.includes("\r"))) return fail4("A cite is one line.");
|
|
4042
|
+
if (type === "objection" && !cite) return fail4("An objection needs a cite.");
|
|
3903
4043
|
const stageDef = getStage(opened.workflow, opened.meta, stage);
|
|
3904
4044
|
const needed = await reviewNeedsEvidence(
|
|
3905
4045
|
root,
|
|
@@ -3909,7 +4049,7 @@ async function postRoom(root, input) {
|
|
|
3909
4049
|
text2,
|
|
3910
4050
|
cite
|
|
3911
4051
|
);
|
|
3912
|
-
if (needed) return
|
|
4052
|
+
if (needed) return fail4(needed);
|
|
3913
4053
|
const evidence = [];
|
|
3914
4054
|
for (const item of input.evidence ?? []) {
|
|
3915
4055
|
const line = oneLine(item, "Evidence needs text.", "Evidence is one line.");
|
|
@@ -3931,12 +4071,12 @@ async function postRoom(root, input) {
|
|
|
3931
4071
|
if (sha) entry.sha = sha;
|
|
3932
4072
|
if (evidence.length > 0) entry.evidence = evidence;
|
|
3933
4073
|
await appendEntry(file, entry);
|
|
3934
|
-
return
|
|
4074
|
+
return ok4("Posted.");
|
|
3935
4075
|
}
|
|
3936
4076
|
async function classifyReply(root, text2, env, runCommand2) {
|
|
3937
4077
|
const loaded = await loadModels(root);
|
|
3938
4078
|
if (loaded === null) return "post";
|
|
3939
|
-
if ("error" in loaded) return
|
|
4079
|
+
if ("error" in loaded) return fail4(loaded.error);
|
|
3940
4080
|
const decision = await decide({
|
|
3941
4081
|
models: loaded.config,
|
|
3942
4082
|
ctx: { workType: "classify" },
|
|
@@ -3950,31 +4090,38 @@ async function classifyReply(root, text2, env, runCommand2) {
|
|
|
3950
4090
|
return decision.pick;
|
|
3951
4091
|
}
|
|
3952
4092
|
async function reviewRoom(root, input) {
|
|
3953
|
-
if (!input.model) return
|
|
4093
|
+
if (!input.model) return fail4(ROLE_TEXT.noModel);
|
|
3954
4094
|
const loaded = await readTeamConfig(root);
|
|
3955
|
-
if ("error" in loaded) return
|
|
4095
|
+
if ("error" in loaded) return fail4(loaded.error);
|
|
3956
4096
|
const opened = await openChange(root, input.change);
|
|
3957
4097
|
if (isFailure(opened)) return opened;
|
|
3958
4098
|
const stageId = (input.stage?.trim() || opened.stage).trim();
|
|
3959
|
-
if (!safeId(stageId)) return
|
|
4099
|
+
if (!safeId(stageId)) return fail4("A stage id is not valid.");
|
|
3960
4100
|
const stage = getStage(opened.workflow, opened.meta, stageId);
|
|
3961
|
-
if (!stage) return
|
|
3962
|
-
const
|
|
4101
|
+
if (!stage) return fail4("Stage not found.");
|
|
4102
|
+
const baked = stageMemberNames(stage, []);
|
|
4103
|
+
let listed = [];
|
|
4104
|
+
if ((!input.roles || input.roles.length === 0) && baked.length === 0) {
|
|
4105
|
+
const found = await rolesListingStage(root, stageId);
|
|
4106
|
+
if ("error" in found) return fail4(found.error);
|
|
4107
|
+
listed = found;
|
|
4108
|
+
}
|
|
4109
|
+
const names = input.roles && input.roles.length > 0 ? input.roles : stageMemberNames(stage, listed);
|
|
3963
4110
|
const bots = memberBots(loaded.team, names);
|
|
3964
|
-
if ("error" in bots) return
|
|
4111
|
+
if ("error" in bots) return fail4(bots.error);
|
|
3965
4112
|
const file = roomPath(root, opened.id, stageId);
|
|
3966
4113
|
const prior = await readEntries(file);
|
|
3967
4114
|
const highest = prior.reduce((max, entry) => Math.max(max, entry.round), 0);
|
|
3968
4115
|
const round = highest + 1;
|
|
3969
4116
|
const cap = roomRounds(loaded.team);
|
|
3970
|
-
if (round > cap) return
|
|
4117
|
+
if (round > cap) return ok4("Round limit reached.");
|
|
3971
4118
|
const draft = await draftOf(opened.path, stage);
|
|
3972
4119
|
const before = consensus(prior.filter((entry) => entry.round === highest));
|
|
3973
4120
|
const sha = await headSha(root);
|
|
3974
4121
|
const written = [];
|
|
3975
4122
|
for (const bot of bots) {
|
|
3976
4123
|
const role = await readRole(root, bot.role);
|
|
3977
|
-
if (!role) return
|
|
4124
|
+
if (!role) return fail4(ROLE_TEXT.notFound);
|
|
3978
4125
|
const section = {
|
|
3979
4126
|
id: stageId,
|
|
3980
4127
|
decision: "room",
|
|
@@ -3984,7 +4131,8 @@ async function reviewRoom(root, input) {
|
|
|
3984
4131
|
if (section.checks.length === 0) section.checks = ["room"];
|
|
3985
4132
|
const rows = [critiquePrompt(role, section, draft || "(none)"), "Room:"];
|
|
3986
4133
|
for (const entry2 of prior) rows.push(formatEntry(entry2));
|
|
3987
|
-
const
|
|
4134
|
+
const listedOwner = stage.owner ?? (listed.length === 1 ? listed[0] : void 0);
|
|
4135
|
+
const owner = listedOwner === bot.role || listedOwner === bot.name;
|
|
3988
4136
|
if (owner && before.open.length > 0) {
|
|
3989
4137
|
rows.push(
|
|
3990
4138
|
revisionPrompt(
|
|
@@ -4000,7 +4148,7 @@ async function reviewRoom(root, input) {
|
|
|
4000
4148
|
try {
|
|
4001
4149
|
raw = await input.model.complete({ roleId: role.name, prompt: rows.join("\n") });
|
|
4002
4150
|
} catch {
|
|
4003
|
-
return
|
|
4151
|
+
return fail4("A room entry is not valid.");
|
|
4004
4152
|
}
|
|
4005
4153
|
const stored = await entryFromReply(root, raw, input.env, input.runCommand);
|
|
4006
4154
|
if (isFailure(stored)) return stored;
|
|
@@ -4012,7 +4160,7 @@ async function reviewRoom(root, input) {
|
|
|
4012
4160
|
stored.text,
|
|
4013
4161
|
stored.cite ?? ""
|
|
4014
4162
|
);
|
|
4015
|
-
if (needed) return
|
|
4163
|
+
if (needed) return fail4(needed);
|
|
4016
4164
|
const entry = {
|
|
4017
4165
|
ts: nowIso(),
|
|
4018
4166
|
from: bot.name,
|
|
@@ -4033,9 +4181,9 @@ async function reviewRoom(root, input) {
|
|
|
4033
4181
|
note: `Room ${stageId} still open after ${round} rounds.`
|
|
4034
4182
|
});
|
|
4035
4183
|
if (asked.exitCode !== 0) return asked;
|
|
4036
|
-
return
|
|
4184
|
+
return ok4("Decision needed.");
|
|
4037
4185
|
}
|
|
4038
|
-
return
|
|
4186
|
+
return ok4(`Round ${round}.`);
|
|
4039
4187
|
}
|
|
4040
4188
|
async function entryFromReply(root, raw, env = process.env, runCommand2) {
|
|
4041
4189
|
let value;
|
|
@@ -4052,18 +4200,18 @@ async function entryFromReply(root, raw, env = process.env, runCommand2) {
|
|
|
4052
4200
|
}
|
|
4053
4201
|
const text2 = raw.trim();
|
|
4054
4202
|
if (!text2 || text2.includes("\n") || text2.includes("\r"))
|
|
4055
|
-
return
|
|
4203
|
+
return fail4("A room entry is not valid.");
|
|
4056
4204
|
const typed = await classifyReply(root, text2, env, runCommand2);
|
|
4057
4205
|
if (typeof typed !== "string") return typed;
|
|
4058
4206
|
return checked(typed, text2, "");
|
|
4059
4207
|
}
|
|
4060
4208
|
function checked(type, text2, cite) {
|
|
4061
|
-
if (!ROOM_TYPES.includes(type)) return
|
|
4209
|
+
if (!ROOM_TYPES.includes(type)) return fail4("A room entry is not valid.");
|
|
4062
4210
|
const words = oneLine(text2, "A room entry needs text.", "A room entry is one line.");
|
|
4063
4211
|
if (typeof words !== "string") return words;
|
|
4064
4212
|
const citeWords = cite.trim();
|
|
4065
|
-
if (citeWords && (cite.includes("\n") || cite.includes("\r"))) return
|
|
4066
|
-
if (type === "objection" && !citeWords) return
|
|
4213
|
+
if (citeWords && (cite.includes("\n") || cite.includes("\r"))) return fail4("A cite is one line.");
|
|
4214
|
+
if (type === "objection" && !citeWords) return fail4("An objection needs a cite.");
|
|
4067
4215
|
return citeWords ? { type, text: words, cite: citeWords } : { type, text: words };
|
|
4068
4216
|
}
|
|
4069
4217
|
async function activeRoom(root) {
|
|
@@ -4143,14 +4291,14 @@ var LoopStepSchema = z5.discriminatedUnion("step", [
|
|
|
4143
4291
|
askStep,
|
|
4144
4292
|
doneStep
|
|
4145
4293
|
]);
|
|
4146
|
-
function
|
|
4294
|
+
function ok5(stdout = "") {
|
|
4147
4295
|
return { exitCode: 0, stdout };
|
|
4148
4296
|
}
|
|
4149
|
-
function
|
|
4297
|
+
function fail5(stdout) {
|
|
4150
4298
|
return { exitCode: 1, stdout };
|
|
4151
4299
|
}
|
|
4152
4300
|
async function appendLine(file, value) {
|
|
4153
|
-
await ensureDir(
|
|
4301
|
+
await ensureDir(join14(file, ".."));
|
|
4154
4302
|
await appendFile4(file, `${JSON.stringify(value)}
|
|
4155
4303
|
`, "utf8");
|
|
4156
4304
|
}
|
|
@@ -4184,13 +4332,13 @@ ${piece}`.trim().slice(-8e3);
|
|
|
4184
4332
|
raw = await input.model.complete({ roleId: input.roleName, prompt: promptFor() });
|
|
4185
4333
|
} catch {
|
|
4186
4334
|
await appendLine(input.file, { type: "bad_call", roleId: input.roleName });
|
|
4187
|
-
return
|
|
4335
|
+
return fail5("A loop step is not valid.");
|
|
4188
4336
|
}
|
|
4189
4337
|
taken += 1;
|
|
4190
4338
|
const step = parseStep(raw);
|
|
4191
4339
|
if (!step) {
|
|
4192
4340
|
await appendLine(input.file, { type: "bad_call", roleId: input.roleName });
|
|
4193
|
-
return
|
|
4341
|
+
return fail5("A loop step is not valid.");
|
|
4194
4342
|
}
|
|
4195
4343
|
if (step.kind === "understand") {
|
|
4196
4344
|
await appendLine(input.file, { type: "step", step: "understand" });
|
|
@@ -4242,7 +4390,7 @@ ${piece}`.trim().slice(-8e3);
|
|
|
4242
4390
|
await appendLine(input.file, { type: "step", step: "done", summary: body.summary });
|
|
4243
4391
|
await copyReviewer();
|
|
4244
4392
|
await postLoopDone(input.root, input.brain.bot.name, body.summary, cites[0]);
|
|
4245
|
-
return
|
|
4393
|
+
return ok5(body.summary);
|
|
4246
4394
|
}
|
|
4247
4395
|
}
|
|
4248
4396
|
return ask(input.root, input.file, `Stopped after ${max} steps.`, ["continue", "stop"]);
|
|
@@ -4253,7 +4401,7 @@ async function ask(root, file, question, options) {
|
|
|
4253
4401
|
if (note && !note.includes("\n") && !note.includes("\r")) {
|
|
4254
4402
|
await recordDecisionRequest(root, { note });
|
|
4255
4403
|
}
|
|
4256
|
-
return
|
|
4404
|
+
return ok5(question);
|
|
4257
4405
|
}
|
|
4258
4406
|
function parseStep(raw) {
|
|
4259
4407
|
let value;
|
|
@@ -4292,7 +4440,7 @@ async function priorLesson(root, logName, file, roleName) {
|
|
|
4292
4440
|
const names = (await listFiles(dir)).filter((item) => item.endsWith(".jsonl")).sort();
|
|
4293
4441
|
const index = names.indexOf(basename(file));
|
|
4294
4442
|
if (index <= 0) return null;
|
|
4295
|
-
const prior =
|
|
4443
|
+
const prior = join14(dir, names[index - 1]);
|
|
4296
4444
|
if (!await pathExists(prior)) return null;
|
|
4297
4445
|
let fromChange = null;
|
|
4298
4446
|
let fromOther = null;
|
|
@@ -4330,10 +4478,10 @@ var FORBIDDEN = /* @__PURE__ */ new Set([
|
|
|
4330
4478
|
]);
|
|
4331
4479
|
var UNAVAILABLE = /* @__PURE__ */ new Set(["commit", "push", "tag", "pull_request", "merge", "deploy"]);
|
|
4332
4480
|
var CLOSED = /* @__PURE__ */ new Set(["applied", "denied", "rejected", "unavailable", "stopped", "expired"]);
|
|
4333
|
-
function
|
|
4481
|
+
function ok6(stdout = "", runId) {
|
|
4334
4482
|
return runId ? { exitCode: 0, stdout, runId } : { exitCode: 0, stdout };
|
|
4335
4483
|
}
|
|
4336
|
-
function
|
|
4484
|
+
function fail6(stdout, runId) {
|
|
4337
4485
|
return runId ? { exitCode: 1, stdout, runId } : { exitCode: 1, stdout };
|
|
4338
4486
|
}
|
|
4339
4487
|
function linesOf(...parts) {
|
|
@@ -4470,7 +4618,7 @@ function parseReply(raw) {
|
|
|
4470
4618
|
return { text: rec.text, gaps, changed, action };
|
|
4471
4619
|
}
|
|
4472
4620
|
async function appendJson(file, value) {
|
|
4473
|
-
await ensureDir(
|
|
4621
|
+
await ensureDir(join15(file, ".."));
|
|
4474
4622
|
await appendFile5(file, `${JSON.stringify(value)}
|
|
4475
4623
|
`, "utf8");
|
|
4476
4624
|
}
|
|
@@ -4487,7 +4635,7 @@ async function readJsonLines(file) {
|
|
|
4487
4635
|
return lines;
|
|
4488
4636
|
}
|
|
4489
4637
|
async function timeoutMinutes(root) {
|
|
4490
|
-
const path =
|
|
4638
|
+
const path = join15(root, ".sdd", "role.yaml");
|
|
4491
4639
|
if (!await pathExists(path)) return 10;
|
|
4492
4640
|
let data;
|
|
4493
4641
|
try {
|
|
@@ -4506,13 +4654,21 @@ async function timeoutMinutes(root) {
|
|
|
4506
4654
|
function citeOk(cite, section) {
|
|
4507
4655
|
return section.checks.includes(cite) || cite === `${section.id}:${section.decision}`;
|
|
4508
4656
|
}
|
|
4509
|
-
function
|
|
4510
|
-
const rows = [
|
|
4511
|
-
|
|
4512
|
-
|
|
4513
|
-
|
|
4514
|
-
|
|
4515
|
-
|
|
4657
|
+
function purviewLines(role) {
|
|
4658
|
+
const rows = [];
|
|
4659
|
+
if (role.stages.length > 0) rows.push(`Stages: ${role.stages.join(", ")}`);
|
|
4660
|
+
if (role.owns.length > 0) rows.push(`Owns: ${role.owns.join(", ")}`);
|
|
4661
|
+
if (role.skills.length > 0) rows.push(`Skills: ${role.skills.join(", ")}`);
|
|
4662
|
+
if (role.never.length > 0) rows.push(`Never: ${role.never.join(", ")}`);
|
|
4663
|
+
if (role.askHumanWhen.length > 0) rows.push(`Ask when: ${role.askHumanWhen.join(", ")}`);
|
|
4664
|
+
if (rows.length > 0) rows.push("Stay inside this role.");
|
|
4665
|
+
return rows;
|
|
4666
|
+
}
|
|
4667
|
+
function authorPrompt(role, task, kind, section, context = "", guide = false) {
|
|
4668
|
+
const rows = [`Job: ${role.job}`, `Description: ${role.description}`, ...purviewLines(role)];
|
|
4669
|
+
if (context.trim()) rows.push(`Context:
|
|
4670
|
+
${context.trim()}`);
|
|
4671
|
+
rows.push(`Task: ${task}`, `Kind: ${kind}`);
|
|
4516
4672
|
if (section) {
|
|
4517
4673
|
rows.push(
|
|
4518
4674
|
`Section: ${section.id}`,
|
|
@@ -4522,12 +4678,32 @@ function authorPrompt(role, task, kind, section) {
|
|
|
4522
4678
|
);
|
|
4523
4679
|
rows.push("Section text:", section.body);
|
|
4524
4680
|
}
|
|
4681
|
+
if (guide) {
|
|
4682
|
+
rows.push('If one fact is missing, reply with one line that starts with "Question: ".');
|
|
4683
|
+
rows.push("Otherwise write the draft for this stage.");
|
|
4684
|
+
}
|
|
4525
4685
|
return rows.join("\n");
|
|
4526
4686
|
}
|
|
4687
|
+
function neverBlocks(role, action) {
|
|
4688
|
+
const words = action.words.trim().toLowerCase();
|
|
4689
|
+
const name = action.name.trim().toLowerCase();
|
|
4690
|
+
return role.never.some((item) => {
|
|
4691
|
+
const line = item.trim().toLowerCase();
|
|
4692
|
+
return line.length > 0 && (line === words || line === name);
|
|
4693
|
+
});
|
|
4694
|
+
}
|
|
4695
|
+
function lockedToChange(role) {
|
|
4696
|
+
return role.never.some((item) => item.trim().toLowerCase() === "write product code");
|
|
4697
|
+
}
|
|
4698
|
+
function outsideChange(changeDir, rel) {
|
|
4699
|
+
const path = rel.replace(/\\/g, "/").replace(/^\.\//, "");
|
|
4700
|
+
const dir = changeDir.replace(/\\/g, "/").replace(/\/$/, "");
|
|
4701
|
+
return path !== dir && !path.startsWith(`${dir}/`);
|
|
4702
|
+
}
|
|
4527
4703
|
async function acceptedFile(root, changeId) {
|
|
4528
|
-
if (!await pathExists(configPath(root))) return
|
|
4704
|
+
if (!await pathExists(configPath(root))) return join15(root, "changes", changeId, "accepted.md");
|
|
4529
4705
|
const config = await loadConfig(root);
|
|
4530
|
-
return
|
|
4706
|
+
return join15(changesDir(root, config), changeId, "accepted.md");
|
|
4531
4707
|
}
|
|
4532
4708
|
async function callModel(model, file, roleId, prompt, pass) {
|
|
4533
4709
|
let raw;
|
|
@@ -4582,24 +4758,51 @@ async function applyAction(root, file, roleName, action, mode) {
|
|
|
4582
4758
|
await appendJson(file, { type: "rejected" });
|
|
4583
4759
|
return { exitCode: 1, stdout: ROLE_TEXT.cannot, applied: false };
|
|
4584
4760
|
}
|
|
4585
|
-
await writeText(
|
|
4761
|
+
await writeText(join15(root, ...path.split(/[/\\]/)), contents);
|
|
4586
4762
|
if (mode === "always")
|
|
4587
4763
|
await saveRule(root, roleName, { name: action.name, inputs: action.inputs });
|
|
4588
4764
|
await appendJson(file, { type: "applied", name: "write_file" });
|
|
4589
4765
|
return { exitCode: 0, stdout: "", applied: true };
|
|
4590
4766
|
}
|
|
4591
|
-
async function
|
|
4767
|
+
async function finishGuideRun(root, file, role, final, runId, changeDir) {
|
|
4768
|
+
if (final.text.trim().startsWith("Question:")) return ok6(final.text, runId);
|
|
4769
|
+
const action = final.action;
|
|
4770
|
+
if (!action) return ok6(final.text, runId);
|
|
4771
|
+
if (FORBIDDEN.has(action.name) || UNAVAILABLE.has(action.name) || neverBlocks(role, action)) {
|
|
4772
|
+
await appendJson(file, { type: "rejected" });
|
|
4773
|
+
return fail6("This role does not do that.", runId);
|
|
4774
|
+
}
|
|
4775
|
+
if (action.name !== "write_file") {
|
|
4776
|
+
await appendJson(file, { type: "rejected" });
|
|
4777
|
+
return fail6(ROLE_TEXT.cannot, runId);
|
|
4778
|
+
}
|
|
4779
|
+
const path = action.inputs.path;
|
|
4780
|
+
const contents = action.inputs.contents;
|
|
4781
|
+
if (typeof path !== "string" || typeof contents !== "string" || !isSafeProductPath(path)) {
|
|
4782
|
+
await appendJson(file, { type: "rejected" });
|
|
4783
|
+
return fail6(ROLE_TEXT.cannot, runId);
|
|
4784
|
+
}
|
|
4785
|
+
if (isSecretPath(path) || lockedToChange(role) && outsideChange(changeDir, path)) {
|
|
4786
|
+
await appendJson(file, { type: "rejected" });
|
|
4787
|
+
return fail6("That file is outside this role.", runId);
|
|
4788
|
+
}
|
|
4789
|
+
const applied = await applyAction(root, file, role.name, action, "once");
|
|
4790
|
+
if (applied.exitCode !== 0) return fail6(applied.stdout || ROLE_TEXT.cannot, runId);
|
|
4791
|
+
return ok6(final.text, runId);
|
|
4792
|
+
}
|
|
4793
|
+
async function finishRun(root, file, role, final, minutes, now, runId, guide) {
|
|
4794
|
+
if (guide) return finishGuideRun(root, file, role, final, runId, guide.changeDir);
|
|
4592
4795
|
const action = final.action;
|
|
4593
|
-
if (!action) return
|
|
4796
|
+
if (!action) return ok6(final.text, runId);
|
|
4594
4797
|
if (FORBIDDEN.has(action.name) || !UNAVAILABLE.has(action.name) && action.name !== "write_file") {
|
|
4595
4798
|
await appendJson(file, { type: "rejected" });
|
|
4596
|
-
return
|
|
4799
|
+
return fail6(linesOf(final.text, ROLE_TEXT.cannot), runId);
|
|
4597
4800
|
}
|
|
4598
4801
|
const rule = await readRule(root, role.name);
|
|
4599
4802
|
if (!role.askFirst && rule && rule.name === action.name && sameInputs(rule.inputs, action.inputs)) {
|
|
4600
4803
|
const applied = await applyAction(root, file, role.name, action, "always");
|
|
4601
4804
|
const stdout = applied.applied ? final.text : linesOf(final.text, applied.stdout);
|
|
4602
|
-
return applied.exitCode === 0 ?
|
|
4805
|
+
return applied.exitCode === 0 ? ok6(stdout, runId) : fail6(stdout, runId);
|
|
4603
4806
|
}
|
|
4604
4807
|
const expiresAt = new Date(now.getTime() + minutes * 6e4).toISOString();
|
|
4605
4808
|
await appendJson(file, {
|
|
@@ -4607,12 +4810,12 @@ async function finishRun(root, file, role, final, minutes, now, runId) {
|
|
|
4607
4810
|
action: { name: action.name, words: action.words, inputs: action.inputs },
|
|
4608
4811
|
expiresAt
|
|
4609
4812
|
});
|
|
4610
|
-
if (role.askFirst) return
|
|
4611
|
-
return
|
|
4813
|
+
if (role.askFirst) return ok6(linesOf(action.words, ROLE_TEXT.askFirst), runId);
|
|
4814
|
+
return ok6(linesOf(action.words, ROLE_TEXT.choice), runId);
|
|
4612
4815
|
}
|
|
4613
4816
|
async function settleRoleReply(root, file, role, reply, now, runId) {
|
|
4614
4817
|
const minutes = await timeoutMinutes(root);
|
|
4615
|
-
if (minutes === "invalid") return
|
|
4818
|
+
if (minutes === "invalid") return fail6(ROLE_TEXT.badYaml, runId);
|
|
4616
4819
|
return finishRun(
|
|
4617
4820
|
root,
|
|
4618
4821
|
file,
|
|
@@ -4627,27 +4830,27 @@ async function latestRunFile(root, name) {
|
|
|
4627
4830
|
const dir = conversationDir(root, name);
|
|
4628
4831
|
const files = (await listFiles(dir)).filter((file) => file.endsWith(".jsonl")).sort();
|
|
4629
4832
|
const last = files[files.length - 1];
|
|
4630
|
-
return last ?
|
|
4833
|
+
return last ? join15(dir, last) : null;
|
|
4631
4834
|
}
|
|
4632
4835
|
async function runRole(root, input) {
|
|
4633
4836
|
const name = canonicalRoleName(input.name);
|
|
4634
|
-
if (!name) return
|
|
4837
|
+
if (!name) return fail6(ROLE_TEXT.badName);
|
|
4635
4838
|
if (input.stop) {
|
|
4636
|
-
if (!await readRole(root, name)) return
|
|
4839
|
+
if (!await readRole(root, name)) return fail6(ROLE_TEXT.notFound);
|
|
4637
4840
|
const latest = await latestRunFile(root, name);
|
|
4638
|
-
if (!latest) return
|
|
4841
|
+
if (!latest) return fail6(ROLE_TEXT.runNotFound);
|
|
4639
4842
|
await appendJson(latest, { type: "stopped" });
|
|
4640
|
-
return
|
|
4843
|
+
return ok6();
|
|
4641
4844
|
}
|
|
4642
4845
|
let roleName = name;
|
|
4643
4846
|
let team = null;
|
|
4644
4847
|
let botName = "";
|
|
4645
|
-
if (await pathExists(
|
|
4848
|
+
if (await pathExists(join15(root, ".sdd", "team.yaml"))) {
|
|
4646
4849
|
const loaded = await readTeamConfig(root);
|
|
4647
|
-
if ("error" in loaded) return
|
|
4850
|
+
if ("error" in loaded) return fail6(loaded.error);
|
|
4648
4851
|
const resolved = resolveBot(loaded.team, name);
|
|
4649
4852
|
if ("error" in resolved) {
|
|
4650
|
-
if (resolved.error === "Name the bot.") return
|
|
4853
|
+
if (resolved.error === "Name the bot.") return fail6(resolved.error);
|
|
4651
4854
|
} else {
|
|
4652
4855
|
team = loaded.team;
|
|
4653
4856
|
botName = resolved.name;
|
|
@@ -4655,41 +4858,41 @@ async function runRole(root, input) {
|
|
|
4655
4858
|
}
|
|
4656
4859
|
}
|
|
4657
4860
|
const role = await readRole(root, roleName);
|
|
4658
|
-
if (!role) return
|
|
4659
|
-
if (!role.job.trim()) return
|
|
4861
|
+
if (!role) return fail6(ROLE_TEXT.notFound);
|
|
4862
|
+
if (!role.job.trim()) return fail6(ROLE_TEXT.needsJob);
|
|
4660
4863
|
const task = input.task ?? "";
|
|
4661
4864
|
let withName = "";
|
|
4662
4865
|
if (input.withRole !== void 0 && input.withRole.trim() !== "") {
|
|
4663
4866
|
const canonical = canonicalRoleName(input.withRole);
|
|
4664
|
-
if (!canonical) return
|
|
4665
|
-
if (canonical === name) return
|
|
4867
|
+
if (!canonical) return fail6(ROLE_TEXT.badName);
|
|
4868
|
+
if (canonical === name) return fail6(ROLE_TEXT.self);
|
|
4666
4869
|
withName = canonical;
|
|
4667
4870
|
}
|
|
4668
|
-
if (mentionsX(task)) return
|
|
4871
|
+
if (mentionsX(task)) return fail6(ROLE_TEXT.x);
|
|
4669
4872
|
const kind = input.kind?.trim() || "reading";
|
|
4670
4873
|
let section;
|
|
4671
4874
|
if (kind !== "reading") {
|
|
4672
|
-
if (!DRAFT_KINDS.has(kind)) return
|
|
4875
|
+
if (!DRAFT_KINDS.has(kind)) return fail6(ROLE_TEXT.needsSection);
|
|
4673
4876
|
const changeId = input.change?.trim() ?? "";
|
|
4674
4877
|
const sectionId = input.section?.trim() ?? "";
|
|
4675
|
-
if (!safeChangeId(changeId) || !sectionId) return
|
|
4878
|
+
if (!safeChangeId(changeId) || !sectionId) return fail6(ROLE_TEXT.needsSection);
|
|
4676
4879
|
const acceptedPath = await acceptedFile(root, changeId);
|
|
4677
|
-
if (!await pathExists(acceptedPath)) return
|
|
4880
|
+
if (!await pathExists(acceptedPath)) return fail6(ROLE_TEXT.needsSection);
|
|
4678
4881
|
const parsed = parseSection(await readText(acceptedPath), sectionId);
|
|
4679
|
-
if (!parsed) return
|
|
4882
|
+
if (!parsed) return fail6(ROLE_TEXT.needsSection);
|
|
4680
4883
|
section = parsed;
|
|
4681
4884
|
}
|
|
4682
4885
|
let critic = null;
|
|
4683
4886
|
if (withName && section) {
|
|
4684
4887
|
critic = await readRole(root, withName);
|
|
4685
|
-
if (!critic) return
|
|
4888
|
+
if (!critic) return fail6(ROLE_TEXT.notFound);
|
|
4686
4889
|
}
|
|
4687
4890
|
const minutes = await timeoutMinutes(root);
|
|
4688
|
-
if (minutes === "invalid") return
|
|
4689
|
-
if (!input.model) return
|
|
4891
|
+
if (minutes === "invalid") return fail6(ROLE_TEXT.badYaml);
|
|
4892
|
+
if (!input.model) return fail6(ROLE_TEXT.noModel);
|
|
4690
4893
|
const now = (input.now ?? (() => /* @__PURE__ */ new Date()))();
|
|
4691
4894
|
const runId = runIdFrom(now);
|
|
4692
|
-
const file =
|
|
4895
|
+
const file = join15(conversationDir(root, name), `${runId}.jsonl`);
|
|
4693
4896
|
if (section) {
|
|
4694
4897
|
await appendJson(file, {
|
|
4695
4898
|
type: "checks",
|
|
@@ -4698,17 +4901,17 @@ async function runRole(root, input) {
|
|
|
4698
4901
|
decision: section.decision
|
|
4699
4902
|
});
|
|
4700
4903
|
}
|
|
4701
|
-
if (team && botName) {
|
|
4904
|
+
if (team && botName && !input.guide) {
|
|
4702
4905
|
let budget = 12e3;
|
|
4703
4906
|
try {
|
|
4704
4907
|
const search = await loadSearchConfig(root);
|
|
4705
4908
|
budget = budgetForStage(search, void 0);
|
|
4706
4909
|
} catch (error) {
|
|
4707
|
-
if (error instanceof SearchConfigError) return
|
|
4910
|
+
if (error instanceof SearchConfigError) return fail6("search.yaml is invalid", runId);
|
|
4708
4911
|
throw error;
|
|
4709
4912
|
}
|
|
4710
4913
|
const brain = await loadBrain(root, team, botName, { text: task }, budget);
|
|
4711
|
-
if ("error" in brain) return
|
|
4914
|
+
if ("error" in brain) return fail6(brain.error, runId);
|
|
4712
4915
|
return runLoop({
|
|
4713
4916
|
root,
|
|
4714
4917
|
cfg: team,
|
|
@@ -4723,10 +4926,17 @@ async function runRole(root, input) {
|
|
|
4723
4926
|
input.model,
|
|
4724
4927
|
file,
|
|
4725
4928
|
role.name,
|
|
4726
|
-
authorPrompt(
|
|
4929
|
+
authorPrompt(
|
|
4930
|
+
role,
|
|
4931
|
+
task,
|
|
4932
|
+
kind,
|
|
4933
|
+
section,
|
|
4934
|
+
await roleContextBody(root, role.name),
|
|
4935
|
+
input.guide !== void 0
|
|
4936
|
+
),
|
|
4727
4937
|
1
|
|
4728
4938
|
);
|
|
4729
|
-
if (!draft) return
|
|
4939
|
+
if (!draft) return fail6("", runId);
|
|
4730
4940
|
let final = draft;
|
|
4731
4941
|
if (critic && section) {
|
|
4732
4942
|
const accepted = section;
|
|
@@ -4737,10 +4947,10 @@ async function runRole(root, input) {
|
|
|
4737
4947
|
critiquePrompt(critic, accepted, draft.text),
|
|
4738
4948
|
2
|
|
4739
4949
|
);
|
|
4740
|
-
if (!critique) return
|
|
4950
|
+
if (!critique) return fail6(draft.text, runId);
|
|
4741
4951
|
if (critique.gaps.some((gap) => !citeOk(gap.cite, accepted))) {
|
|
4742
4952
|
await appendJson(file, { type: "bad_call", roleId: critic.name });
|
|
4743
|
-
return
|
|
4953
|
+
return fail6(draft.text, runId);
|
|
4744
4954
|
}
|
|
4745
4955
|
if (critique.gaps.length > 0) {
|
|
4746
4956
|
const revision = await callModel(
|
|
@@ -4750,16 +4960,16 @@ async function runRole(root, input) {
|
|
|
4750
4960
|
revisionPrompt(role, draft.text, critique.gaps),
|
|
4751
4961
|
3
|
|
4752
4962
|
);
|
|
4753
|
-
if (!revision) return
|
|
4963
|
+
if (!revision) return fail6(draft.text, runId);
|
|
4754
4964
|
const cites = critique.gaps.map((gap) => gap.cite);
|
|
4755
4965
|
if (revision.changed === null || !sameCiteSet(revision.changed, cites)) {
|
|
4756
4966
|
await appendJson(file, { type: "bad_call", roleId: role.name });
|
|
4757
|
-
return
|
|
4967
|
+
return fail6(draft.text, runId);
|
|
4758
4968
|
}
|
|
4759
4969
|
final = revision;
|
|
4760
4970
|
}
|
|
4761
4971
|
}
|
|
4762
|
-
return finishRun(root, file, role, final, minutes, now, runId);
|
|
4972
|
+
return finishRun(root, file, role, final, minutes, now, runId, input.guide);
|
|
4763
4973
|
}
|
|
4764
4974
|
function openPending(lines) {
|
|
4765
4975
|
let pending = null;
|
|
@@ -4793,7 +5003,7 @@ async function findRun(root, runId) {
|
|
|
4793
5003
|
if (!safeRunId(runId)) return null;
|
|
4794
5004
|
const hits = [];
|
|
4795
5005
|
for (const roleName of await listConversationRoles(root)) {
|
|
4796
|
-
const file =
|
|
5006
|
+
const file = join15(conversationDir(root, roleName), `${runId}.jsonl`);
|
|
4797
5007
|
if (await pathExists(file)) hits.push({ file, roleName });
|
|
4798
5008
|
}
|
|
4799
5009
|
if (hits.length === 1) return hits[0];
|
|
@@ -4805,24 +5015,24 @@ async function findRun(root, runId) {
|
|
|
4805
5015
|
}
|
|
4806
5016
|
async function allowRole(root, input) {
|
|
4807
5017
|
const found = await findRun(root, input.runId);
|
|
4808
|
-
if (!found) return
|
|
5018
|
+
if (!found) return fail6(ROLE_TEXT.runNotFound);
|
|
4809
5019
|
const pending = openPending(await readJsonLines(found.file));
|
|
4810
|
-
if (!pending) return
|
|
5020
|
+
if (!pending) return fail6(ROLE_TEXT.runNotFound);
|
|
4811
5021
|
const now = (input.now ?? (() => /* @__PURE__ */ new Date()))();
|
|
4812
5022
|
const expires = Date.parse(pending.expiresAt);
|
|
4813
5023
|
if (!Number.isFinite(expires) || now.getTime() >= expires) {
|
|
4814
5024
|
await appendJson(found.file, { type: "expired" });
|
|
4815
|
-
return
|
|
5025
|
+
return fail6(ROLE_TEXT.expired);
|
|
4816
5026
|
}
|
|
4817
5027
|
if (input.mode === "deny") {
|
|
4818
5028
|
await appendJson(found.file, { type: "denied" });
|
|
4819
|
-
return
|
|
5029
|
+
return ok6("", input.runId);
|
|
4820
5030
|
}
|
|
4821
5031
|
const role = await readRole(root, found.roleName);
|
|
4822
|
-
if (!role) return
|
|
4823
|
-
if (role.askFirst) return
|
|
5032
|
+
if (!role) return fail6(ROLE_TEXT.notFound);
|
|
5033
|
+
if (role.askFirst) return ok6(ROLE_TEXT.askFirst, input.runId);
|
|
4824
5034
|
const applied = await applyAction(root, found.file, found.roleName, pending, input.mode);
|
|
4825
|
-
return applied.exitCode === 0 ?
|
|
5035
|
+
return applied.exitCode === 0 ? ok6(applied.stdout, input.runId) : fail6(applied.stdout, input.runId);
|
|
4826
5036
|
}
|
|
4827
5037
|
|
|
4828
5038
|
// src/role/routing.ts
|
|
@@ -4857,10 +5067,10 @@ var RoutingSchema = z6.object({
|
|
|
4857
5067
|
seen.add(rule.id);
|
|
4858
5068
|
}
|
|
4859
5069
|
});
|
|
4860
|
-
function
|
|
5070
|
+
function ok7(stdout = "") {
|
|
4861
5071
|
return { exitCode: 0, stdout };
|
|
4862
5072
|
}
|
|
4863
|
-
function
|
|
5073
|
+
function fail7(stdout) {
|
|
4864
5074
|
return { exitCode: 1, stdout };
|
|
4865
5075
|
}
|
|
4866
5076
|
function formatRoutingError(error) {
|
|
@@ -4889,7 +5099,7 @@ function cleanItem(input) {
|
|
|
4889
5099
|
function inboxFile(root, name) {
|
|
4890
5100
|
const canonical = canonicalRoleName(name);
|
|
4891
5101
|
if (!canonical) return null;
|
|
4892
|
-
return
|
|
5102
|
+
return join16(root, ".sdd", "agents", canonical, "inbox.jsonl");
|
|
4893
5103
|
}
|
|
4894
5104
|
async function writeHandoffInboxes(root, record) {
|
|
4895
5105
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -4902,7 +5112,7 @@ async function writeHandoffInboxes(root, record) {
|
|
|
4902
5112
|
}
|
|
4903
5113
|
}
|
|
4904
5114
|
async function readRouting(root) {
|
|
4905
|
-
const path =
|
|
5115
|
+
const path = join16(root, ".sdd", "routing.yaml");
|
|
4906
5116
|
if (!await pathExists(path)) return { rules: [] };
|
|
4907
5117
|
let raw;
|
|
4908
5118
|
try {
|
|
@@ -4915,14 +5125,14 @@ async function readRouting(root) {
|
|
|
4915
5125
|
return parsed.data;
|
|
4916
5126
|
}
|
|
4917
5127
|
async function loadRoles(root) {
|
|
4918
|
-
const dir =
|
|
5128
|
+
const dir = join16(root, ".sdd", "roles");
|
|
4919
5129
|
const files = (await listFiles(dir)).filter((file) => file.endsWith(".md")).sort();
|
|
4920
5130
|
const roles = [];
|
|
4921
5131
|
for (const file of files) {
|
|
4922
5132
|
const name = file.slice(0, -".md".length);
|
|
4923
5133
|
let parsed;
|
|
4924
5134
|
try {
|
|
4925
|
-
parsed = parseRoleDocument(name, await readText(
|
|
5135
|
+
parsed = parseRoleDocument(name, await readText(join16(dir, file)));
|
|
4926
5136
|
} catch {
|
|
4927
5137
|
return { error: `Role file is invalid: ${name}.` };
|
|
4928
5138
|
}
|
|
@@ -5005,6 +5215,24 @@ async function matchRoute(root, input) {
|
|
|
5005
5215
|
}
|
|
5006
5216
|
};
|
|
5007
5217
|
}
|
|
5218
|
+
if (item.stage) {
|
|
5219
|
+
const named = roles.filter((role) => role.stages.includes(item.stage ?? ""));
|
|
5220
|
+
if (named.length > 1) {
|
|
5221
|
+
return { ok: true, match: { kind: "ask", reason: "Two roles list this stage." } };
|
|
5222
|
+
}
|
|
5223
|
+
if (named.length === 1) {
|
|
5224
|
+
const role = named[0]?.name ?? "";
|
|
5225
|
+
return {
|
|
5226
|
+
ok: true,
|
|
5227
|
+
match: {
|
|
5228
|
+
kind: "role",
|
|
5229
|
+
role,
|
|
5230
|
+
reason: `Role ${role} lists stage ${item.stage}.`,
|
|
5231
|
+
rule: "role-stage"
|
|
5232
|
+
}
|
|
5233
|
+
};
|
|
5234
|
+
}
|
|
5235
|
+
}
|
|
5008
5236
|
if (item.stage) {
|
|
5009
5237
|
const owner = await stageOwner(root, item.stage);
|
|
5010
5238
|
if (owner && typeof owner === "object") return { ok: false, message: owner.error };
|
|
@@ -5080,17 +5308,17 @@ async function routeMetrics(root) {
|
|
|
5080
5308
|
}
|
|
5081
5309
|
async function routeRole(root, input, deps) {
|
|
5082
5310
|
const item = await prepareItem(root, input);
|
|
5083
|
-
if ("error" in item) return
|
|
5311
|
+
if ("error" in item) return fail7(item.error);
|
|
5084
5312
|
const matched = await matchRoute(root, item);
|
|
5085
|
-
if (!matched.ok) return
|
|
5313
|
+
if (!matched.ok) return fail7(matched.message);
|
|
5086
5314
|
if (matched.match.kind !== "ask" || matched.match.reason !== "No rule matched.") {
|
|
5087
|
-
return
|
|
5315
|
+
return ok7(formatHit(matched.match));
|
|
5088
5316
|
}
|
|
5089
5317
|
const loaded = await loadModels(root);
|
|
5090
|
-
if (loaded === null) return
|
|
5091
|
-
if ("error" in loaded) return
|
|
5318
|
+
if (loaded === null) return ok7(formatHit(matched.match));
|
|
5319
|
+
if ("error" in loaded) return fail7(loaded.error);
|
|
5092
5320
|
const roles = await loadRoles(root);
|
|
5093
|
-
if ("error" in roles) return
|
|
5321
|
+
if ("error" in roles) return fail7(roles.error);
|
|
5094
5322
|
const names = roles.map((role) => role.name).sort((left, right) => left.localeCompare(right));
|
|
5095
5323
|
const options = names.includes("human") ? names : [...names, "human"];
|
|
5096
5324
|
const decision = await decide({
|
|
@@ -5107,10 +5335,10 @@ async function routeRole(root, input, deps) {
|
|
|
5107
5335
|
});
|
|
5108
5336
|
if (decision.pick !== "human" && names.includes(decision.pick)) {
|
|
5109
5337
|
const id = decision.choice?.id ?? "model";
|
|
5110
|
-
return
|
|
5338
|
+
return ok7(`role ${decision.pick}
|
|
5111
5339
|
reason Model ${id} picked ${decision.pick}.`);
|
|
5112
5340
|
}
|
|
5113
|
-
return
|
|
5341
|
+
return ok7("Ask the human.\nNo rule matched.");
|
|
5114
5342
|
}
|
|
5115
5343
|
async function readInbox(root, name) {
|
|
5116
5344
|
const file = inboxFile(root, name);
|
|
@@ -5186,20 +5414,20 @@ async function queueNames(root, bot) {
|
|
|
5186
5414
|
}
|
|
5187
5415
|
const team = await readTeamConfig(root);
|
|
5188
5416
|
if ("team" in team) return team.team.bots.map((item) => item.name);
|
|
5189
|
-
const dirs = (await listDirs(
|
|
5417
|
+
const dirs = (await listDirs(join16(root, ".sdd", "agents"))).sort();
|
|
5190
5418
|
if (dirs.length === 0) return { error: team.error };
|
|
5191
5419
|
return dirs;
|
|
5192
5420
|
}
|
|
5193
5421
|
async function queueRole(root, bot) {
|
|
5194
5422
|
const names = await queueNames(root, bot?.trim());
|
|
5195
|
-
if ("error" in names) return
|
|
5423
|
+
if ("error" in names) return fail7(names.error);
|
|
5196
5424
|
const blocks = [];
|
|
5197
5425
|
for (const name of names) {
|
|
5198
5426
|
const entries = await readInbox(root, name);
|
|
5199
|
-
if ("error" in entries) return
|
|
5427
|
+
if ("error" in entries) return fail7(entries.error);
|
|
5200
5428
|
blocks.push(formatInbox(name, entries));
|
|
5201
5429
|
}
|
|
5202
|
-
return
|
|
5430
|
+
return ok7(blocks.join("\n\n"));
|
|
5203
5431
|
}
|
|
5204
5432
|
function itemId(item) {
|
|
5205
5433
|
if (item.id) return item.id;
|
|
@@ -5212,26 +5440,26 @@ function itemId(item) {
|
|
|
5212
5440
|
}
|
|
5213
5441
|
async function takeRole(root, input) {
|
|
5214
5442
|
const item = await prepareItem(root, input);
|
|
5215
|
-
if ("error" in item) return
|
|
5443
|
+
if ("error" in item) return fail7(item.error);
|
|
5216
5444
|
const matched = await matchRoute(root, item);
|
|
5217
|
-
if (!matched.ok) return
|
|
5445
|
+
if (!matched.ok) return fail7(matched.message);
|
|
5218
5446
|
const match = matched.match;
|
|
5219
|
-
if (match.kind === "ask") return
|
|
5447
|
+
if (match.kind === "ask") return fail7(formatHit(match));
|
|
5220
5448
|
const team = await readTeamConfig(root);
|
|
5221
|
-
if ("error" in team) return
|
|
5449
|
+
if ("error" in team) return fail7(team.error);
|
|
5222
5450
|
const bots = team.team.bots.filter((bot) => bot.role === match.role);
|
|
5223
|
-
if (bots.length === 0) return
|
|
5451
|
+
if (bots.length === 0) return fail7(`No bot has role ${match.role}.`);
|
|
5224
5452
|
const requested = input.bot?.trim();
|
|
5225
|
-
if (requested && !canonicalRoleName(requested)) return
|
|
5453
|
+
if (requested && !canonicalRoleName(requested)) return fail7(ROLE_TEXT.badName);
|
|
5226
5454
|
if (requested && !bots.some((bot) => bot.name === requested)) {
|
|
5227
5455
|
const known = team.team.bots.some((bot) => bot.name === requested);
|
|
5228
|
-
if (!known) return
|
|
5229
|
-
return
|
|
5456
|
+
if (!known) return fail7("Bot not found.");
|
|
5457
|
+
return fail7(`${requested} is not a ${match.role}.`);
|
|
5230
5458
|
}
|
|
5231
5459
|
const counts = /* @__PURE__ */ new Map();
|
|
5232
5460
|
for (const bot of team.team.bots) {
|
|
5233
5461
|
const entries = await readInbox(root, bot.name);
|
|
5234
|
-
if ("error" in entries) return
|
|
5462
|
+
if ("error" in entries) return fail7(entries.error);
|
|
5235
5463
|
counts.set(
|
|
5236
5464
|
bot.name,
|
|
5237
5465
|
entries.filter((entry) => entry.type === "item" && entry.status === "active").length
|
|
@@ -5240,19 +5468,19 @@ async function takeRole(root, input) {
|
|
|
5240
5468
|
const roleCount = bots.reduce((sum, bot) => sum + (counts.get(bot.name) ?? 0), 0);
|
|
5241
5469
|
const roleCap = team.team.role_wip?.[match.role];
|
|
5242
5470
|
if (roleCap !== void 0 && roleCount >= roleCap) {
|
|
5243
|
-
return
|
|
5471
|
+
return fail7(`${match.role} is at the limit.`);
|
|
5244
5472
|
}
|
|
5245
5473
|
const chosen = requested ? bots.find((bot) => bot.name === requested) : bots.find((bot) => (counts.get(bot.name) ?? 0) < bot.wip);
|
|
5246
5474
|
if (!chosen || (counts.get(chosen.name) ?? 0) >= chosen.wip) {
|
|
5247
|
-
return
|
|
5475
|
+
return fail7(`${requested || bots[0]?.name || match.role} is at the limit.`);
|
|
5248
5476
|
}
|
|
5249
5477
|
const id = itemId(item);
|
|
5250
|
-
if (!id) return
|
|
5478
|
+
if (!id) return fail7("An item needs an id.");
|
|
5251
5479
|
for (const bot of team.team.bots) {
|
|
5252
5480
|
const entries = await readInbox(root, bot.name);
|
|
5253
|
-
if ("error" in entries) return
|
|
5481
|
+
if ("error" in entries) return fail7(entries.error);
|
|
5254
5482
|
if (entries.some((entry) => entry.type === "item" && entry.status === "active" && entry.id === id)) {
|
|
5255
|
-
return
|
|
5483
|
+
return fail7(`${id} is already active.`);
|
|
5256
5484
|
}
|
|
5257
5485
|
}
|
|
5258
5486
|
const record = {
|
|
@@ -5267,9 +5495,9 @@ async function takeRole(root, input) {
|
|
|
5267
5495
|
if (item.artifact) record.artifact = item.artifact;
|
|
5268
5496
|
if (item.skill) record.skill = item.skill;
|
|
5269
5497
|
const file = inboxFile(root, chosen.name);
|
|
5270
|
-
if (!file) return
|
|
5498
|
+
if (!file) return fail7(ROLE_TEXT.badName);
|
|
5271
5499
|
await appendJson(file, record);
|
|
5272
|
-
return
|
|
5500
|
+
return ok7(`${chosen.name} took ${id}
|
|
5273
5501
|
role ${match.role}
|
|
5274
5502
|
rule ${match.rule}`);
|
|
5275
5503
|
}
|
|
@@ -5277,10 +5505,10 @@ rule ${match.rule}`);
|
|
|
5277
5505
|
// src/role/handoff.ts
|
|
5278
5506
|
var ID = /^[a-z][a-z0-9-]{0,39}$/;
|
|
5279
5507
|
var ROLE_YAML = ["role.yaml"];
|
|
5280
|
-
function
|
|
5508
|
+
function ok8(stdout = "", runId) {
|
|
5281
5509
|
return runId ? { exitCode: 0, stdout, runId } : { exitCode: 0, stdout };
|
|
5282
5510
|
}
|
|
5283
|
-
function
|
|
5511
|
+
function fail8(stdout, runId) {
|
|
5284
5512
|
return runId ? { exitCode: 1, stdout, runId } : { exitCode: 1, stdout };
|
|
5285
5513
|
}
|
|
5286
5514
|
function mentionsX2(task) {
|
|
@@ -5291,10 +5519,10 @@ function safeChangeId2(id) {
|
|
|
5291
5519
|
return !id.includes("/") && !id.includes("\\") && !id.includes("\0");
|
|
5292
5520
|
}
|
|
5293
5521
|
function yamlPath(root) {
|
|
5294
|
-
return
|
|
5522
|
+
return join17(root, ".sdd", ...ROLE_YAML);
|
|
5295
5523
|
}
|
|
5296
5524
|
function planPath(root, changeId, sectionId) {
|
|
5297
|
-
return
|
|
5525
|
+
return join17(root, ".sdd", "role-runs", "plans", changeId, `${sectionId}.json`);
|
|
5298
5526
|
}
|
|
5299
5527
|
async function loadYaml(root) {
|
|
5300
5528
|
const path = yamlPath(root);
|
|
@@ -5325,11 +5553,11 @@ async function messagingOn(root) {
|
|
|
5325
5553
|
}
|
|
5326
5554
|
async function setRoleMessaging(root, enabled) {
|
|
5327
5555
|
const loaded = await loadYaml(root);
|
|
5328
|
-
if (loaded === "invalid") return
|
|
5556
|
+
if (loaded === "invalid") return fail8(ROLE_TEXT.badYaml);
|
|
5329
5557
|
const raw = loaded === "missing" ? {} : loaded;
|
|
5330
5558
|
raw.role_messaging = enabled;
|
|
5331
5559
|
await writeText(yamlPath(root), stringifyYaml(raw));
|
|
5332
|
-
return
|
|
5560
|
+
return ok8(enabled ? "Messaging between roles is on." : "Messaging between roles is off.");
|
|
5333
5561
|
}
|
|
5334
5562
|
function field(body, prefix) {
|
|
5335
5563
|
const found = [];
|
|
@@ -5428,26 +5656,26 @@ function receiverPrompt(role, task, section) {
|
|
|
5428
5656
|
async function handoffRole(root, input) {
|
|
5429
5657
|
const from = canonicalRoleName(input.from);
|
|
5430
5658
|
const to = canonicalRoleName(input.to);
|
|
5431
|
-
if (!from || !to) return
|
|
5432
|
-
if (from === to) return
|
|
5659
|
+
if (!from || !to) return fail8(ROLE_TEXT.badName);
|
|
5660
|
+
if (from === to) return fail8("A role cannot hand off to itself.");
|
|
5433
5661
|
const sender = await readRole(root, from);
|
|
5434
5662
|
const receiver = await readRole(root, to);
|
|
5435
|
-
if (!sender || !receiver) return
|
|
5663
|
+
if (!sender || !receiver) return fail8(ROLE_TEXT.notFound);
|
|
5436
5664
|
const task = input.task ?? "";
|
|
5437
|
-
if (mentionsX2(task)) return
|
|
5665
|
+
if (mentionsX2(task)) return fail8(ROLE_TEXT.x);
|
|
5438
5666
|
const loaded = await acceptedFacts(root, input.change?.trim() ?? "", input.section?.trim() ?? "");
|
|
5439
|
-
if (!loaded) return
|
|
5667
|
+
if (!loaded) return fail8(ROLE_TEXT.needsSection);
|
|
5440
5668
|
if (loaded.current.owners.length > 0 && !loaded.current.owners.includes(to)) {
|
|
5441
|
-
return
|
|
5669
|
+
return fail8("One owner handles this section.");
|
|
5442
5670
|
}
|
|
5443
5671
|
const messaging = await messagingOn(root);
|
|
5444
|
-
if (messaging === false) return
|
|
5445
|
-
if (!input.model) return
|
|
5446
|
-
if (messaging === "invalid") return
|
|
5672
|
+
if (messaging === false) return ok8("Messaging between roles is off.");
|
|
5673
|
+
if (!input.model) return fail8(ROLE_TEXT.noModel);
|
|
5674
|
+
if (messaging === "invalid") return fail8(ROLE_TEXT.badYaml);
|
|
5447
5675
|
const now = (input.now ?? (() => /* @__PURE__ */ new Date()))();
|
|
5448
5676
|
const runId = runIdFrom(now);
|
|
5449
|
-
const senderFile =
|
|
5450
|
-
const receiverFile =
|
|
5677
|
+
const senderFile = join17(conversationDir(root, from), `${runId}.jsonl`);
|
|
5678
|
+
const receiverFile = join17(conversationDir(root, to), `${runId}.jsonl`);
|
|
5451
5679
|
const record = handoffRecord(from, to, input.change?.trim() ?? "", loaded.current);
|
|
5452
5680
|
await appendJson(senderFile, record);
|
|
5453
5681
|
await appendJson(receiverFile, record);
|
|
@@ -5460,12 +5688,12 @@ async function handoffRole(root, input) {
|
|
|
5460
5688
|
});
|
|
5461
5689
|
} catch {
|
|
5462
5690
|
await appendJson(receiverFile, { type: "bad_call", roleId: to });
|
|
5463
|
-
return
|
|
5691
|
+
return fail8("", runId);
|
|
5464
5692
|
}
|
|
5465
5693
|
const parsed = parseReply(raw);
|
|
5466
5694
|
if (!parsed) {
|
|
5467
5695
|
await appendJson(receiverFile, { type: "bad_call", roleId: to });
|
|
5468
|
-
return
|
|
5696
|
+
return fail8("", runId);
|
|
5469
5697
|
}
|
|
5470
5698
|
await appendJson(receiverFile, { type: "model", pass: 1, roleId: to, text: parsed.text });
|
|
5471
5699
|
const settled = await settleRoleReply(root, receiverFile, receiver, parsed, now, runId);
|
|
@@ -5482,7 +5710,7 @@ async function handoffRole(root, input) {
|
|
|
5482
5710
|
}
|
|
5483
5711
|
async function showRoleGroup(root, changeId) {
|
|
5484
5712
|
const path = safeChangeId2(changeId) ? await acceptedFile(root, changeId) : "";
|
|
5485
|
-
if (!path || !await pathExists(path)) return
|
|
5713
|
+
if (!path || !await pathExists(path)) return ok8("No roles in this group.");
|
|
5486
5714
|
const seen = /* @__PURE__ */ new Set();
|
|
5487
5715
|
const owners = [];
|
|
5488
5716
|
for (const section of factsFrom(await readText(path))) {
|
|
@@ -5492,7 +5720,7 @@ async function showRoleGroup(root, changeId) {
|
|
|
5492
5720
|
owners.push(owner);
|
|
5493
5721
|
}
|
|
5494
5722
|
}
|
|
5495
|
-
return
|
|
5723
|
+
return ok8(owners.length > 0 ? owners.join("\n") : "No roles in this group.");
|
|
5496
5724
|
}
|
|
5497
5725
|
function distinctServices(all) {
|
|
5498
5726
|
const services = /* @__PURE__ */ new Map();
|
|
@@ -5620,20 +5848,20 @@ function leadPrompt(task, services) {
|
|
|
5620
5848
|
}
|
|
5621
5849
|
async function connectionResult(root, changeId, section, now) {
|
|
5622
5850
|
const messaging = await messagingOn(root);
|
|
5623
|
-
if (messaging === "invalid") return
|
|
5624
|
-
if (!messaging) return
|
|
5851
|
+
if (messaging === "invalid") return fail8(ROLE_TEXT.badYaml);
|
|
5852
|
+
if (!messaging) return ok8("Messaging between roles is off.");
|
|
5625
5853
|
const owners = unique(section.owners);
|
|
5626
5854
|
const runId = runIdFrom(now);
|
|
5627
5855
|
for (let i = 0; i < owners.length - 1; i++) {
|
|
5628
5856
|
const from = owners[i] ?? "";
|
|
5629
5857
|
const to = owners[i + 1] ?? "";
|
|
5630
5858
|
const record = handoffRecord(from, to, changeId, section);
|
|
5631
|
-
await appendJson(
|
|
5632
|
-
await appendJson(
|
|
5859
|
+
await appendJson(join17(conversationDir(root, from), `${runId}.jsonl`), record);
|
|
5860
|
+
await appendJson(join17(conversationDir(root, to), `${runId}.jsonl`), record);
|
|
5633
5861
|
await writeHandoffInboxes(root, record);
|
|
5634
5862
|
}
|
|
5635
5863
|
const lines = ["Connection hands off. Lead is absent.", ...owners];
|
|
5636
|
-
return
|
|
5864
|
+
return ok8(lines.join("\n"));
|
|
5637
5865
|
}
|
|
5638
5866
|
function freshPlan(task, nodes) {
|
|
5639
5867
|
return { status: "pending", rebuilds: 0, stalled: false, task, nodes, results: [] };
|
|
@@ -5642,7 +5870,7 @@ async function planNetwork(root, input) {
|
|
|
5642
5870
|
const changeId = input.change?.trim() ?? "";
|
|
5643
5871
|
const sectionId = input.section?.trim() ?? "";
|
|
5644
5872
|
const loaded = await acceptedFacts(root, changeId, sectionId);
|
|
5645
|
-
if (!loaded) return
|
|
5873
|
+
if (!loaded) return fail8(ROLE_TEXT.needsSection);
|
|
5646
5874
|
const task = input.task ?? "";
|
|
5647
5875
|
const now = (input.now ?? (() => /* @__PURE__ */ new Date()))();
|
|
5648
5876
|
const connection = loaded.all.find(
|
|
@@ -5650,23 +5878,23 @@ async function planNetwork(root, input) {
|
|
|
5650
5878
|
);
|
|
5651
5879
|
if (connection?.connection) return connectionResult(root, changeId, connection, now);
|
|
5652
5880
|
const services = distinctServices(loaded.all);
|
|
5653
|
-
if (services.size === 0) return
|
|
5881
|
+
if (services.size === 0) return ok8("One scope. No lead.");
|
|
5654
5882
|
if (services.size === 1) {
|
|
5655
5883
|
const entry = [...services.entries()][0];
|
|
5656
|
-
if (!entry) return
|
|
5884
|
+
if (!entry) return ok8("One scope. No lead.");
|
|
5657
5885
|
const [name, section] = entry;
|
|
5658
5886
|
const owner = section.owners[0];
|
|
5659
|
-
if (!owner) return
|
|
5660
|
-
if (!await readRole(root, owner)) return
|
|
5661
|
-
return
|
|
5887
|
+
if (!owner) return fail8("This service needs an owner.");
|
|
5888
|
+
if (!await readRole(root, owner)) return fail8(ROLE_TEXT.notFound);
|
|
5889
|
+
return ok8(`Service ${name} runs ${owner}.`);
|
|
5662
5890
|
}
|
|
5663
5891
|
const picked = await pickLead(root, loaded.all, services);
|
|
5664
|
-
if (picked.kind === "none") return
|
|
5665
|
-
if (picked.kind === "missing") return
|
|
5892
|
+
if (picked.kind === "none") return ok8("One scope. No lead.");
|
|
5893
|
+
if (picked.kind === "missing") return fail8(picked.sentence);
|
|
5666
5894
|
const lead = await readRole(root, picked.name);
|
|
5667
|
-
if (!lead) return
|
|
5668
|
-
if (await messagingOn(root) === "invalid") return
|
|
5669
|
-
if (!input.model) return
|
|
5895
|
+
if (!lead) return fail8(ROLE_TEXT.notFound);
|
|
5896
|
+
if (await messagingOn(root) === "invalid") return fail8(ROLE_TEXT.badYaml);
|
|
5897
|
+
if (!input.model) return fail8(ROLE_TEXT.noModel);
|
|
5670
5898
|
return storeLeadPlan(
|
|
5671
5899
|
root,
|
|
5672
5900
|
changeId,
|
|
@@ -5684,17 +5912,17 @@ async function storeLeadPlan(root, changeId, sectionId, task, lead, services, al
|
|
|
5684
5912
|
try {
|
|
5685
5913
|
raw = await model.complete({ roleId: lead.name, prompt: leadPrompt(task, services) });
|
|
5686
5914
|
} catch {
|
|
5687
|
-
return
|
|
5915
|
+
return fail8("");
|
|
5688
5916
|
}
|
|
5689
5917
|
const parsed = await parsePlan(root, raw, contextOf(all));
|
|
5690
|
-
if (previous && (parsed === "invalid" || parsed === "deep")) return
|
|
5691
|
-
if (parsed === "invalid") return
|
|
5692
|
-
if (parsed === "deep") return
|
|
5918
|
+
if (previous && (parsed === "invalid" || parsed === "deep")) return fail8(planFailure(parsed));
|
|
5919
|
+
if (parsed === "invalid") return fail8("The plan is not valid.");
|
|
5920
|
+
if (parsed === "deep") return fail8("A plan is deeper than 2.");
|
|
5693
5921
|
if (previous && sameArtifacts(previous.nodes, parsed)) {
|
|
5694
5922
|
previous.stalled = true;
|
|
5695
5923
|
previous.rebuilds += 1;
|
|
5696
5924
|
await writePlan(root, changeId, sectionId, previous);
|
|
5697
|
-
return
|
|
5925
|
+
return ok8("Stopped. The plan made no progress.");
|
|
5698
5926
|
}
|
|
5699
5927
|
const next = previous ? {
|
|
5700
5928
|
...previous,
|
|
@@ -5705,7 +5933,7 @@ async function storeLeadPlan(root, changeId, sectionId, task, lead, services, al
|
|
|
5705
5933
|
results: []
|
|
5706
5934
|
} : freshPlan(task, parsed);
|
|
5707
5935
|
await writePlan(root, changeId, sectionId, next);
|
|
5708
|
-
return
|
|
5936
|
+
return ok8(["Accept this plan before anyone writes.", ...planLines(parsed)].join("\n"));
|
|
5709
5937
|
}
|
|
5710
5938
|
function planFailure(parsed) {
|
|
5711
5939
|
return parsed === "deep" ? "A plan is deeper than 2." : "The plan is not valid.";
|
|
@@ -5718,13 +5946,13 @@ function sameArtifacts(left, right) {
|
|
|
5718
5946
|
async function acceptNetwork(root, input) {
|
|
5719
5947
|
const changeId = input.change?.trim() ?? "";
|
|
5720
5948
|
const sectionId = input.section?.trim() ?? "";
|
|
5721
|
-
if (!safeChangeId2(changeId) || !sectionId) return
|
|
5949
|
+
if (!safeChangeId2(changeId) || !sectionId) return fail8("No plan is stored.");
|
|
5722
5950
|
const plan = await readPlan(root, changeId, sectionId);
|
|
5723
|
-
if (!plan) return
|
|
5724
|
-
if (plan.status !== "pending") return
|
|
5951
|
+
if (!plan) return fail8("No plan is stored.");
|
|
5952
|
+
if (plan.status !== "pending") return fail8("The plan is not waiting for acceptance.");
|
|
5725
5953
|
plan.status = "accepted";
|
|
5726
5954
|
await writePlan(root, changeId, sectionId, plan);
|
|
5727
|
-
return
|
|
5955
|
+
return ok8("Plan accepted.");
|
|
5728
5956
|
}
|
|
5729
5957
|
function walk(nodes) {
|
|
5730
5958
|
const flat = [];
|
|
@@ -5745,16 +5973,16 @@ function ready(node, results) {
|
|
|
5745
5973
|
async function runNetwork(root, input) {
|
|
5746
5974
|
const changeId = input.change?.trim() ?? "";
|
|
5747
5975
|
const sectionId = input.section?.trim() ?? "";
|
|
5748
|
-
if (!safeChangeId2(changeId) || !sectionId) return
|
|
5976
|
+
if (!safeChangeId2(changeId) || !sectionId) return fail8("No plan is stored.");
|
|
5749
5977
|
const plan = await readPlan(root, changeId, sectionId);
|
|
5750
|
-
if (!plan) return
|
|
5751
|
-
if (plan.status === "pending") return
|
|
5752
|
-
if (plan.status === "done") return
|
|
5753
|
-
if (plan.status !== "accepted" && plan.status !== "open") return
|
|
5754
|
-
if (await messagingOn(root) === "invalid") return
|
|
5978
|
+
if (!plan) return fail8("No plan is stored.");
|
|
5979
|
+
if (plan.status === "pending") return ok8("Accept this plan before anyone writes.");
|
|
5980
|
+
if (plan.status === "done") return ok8("The parent is done.");
|
|
5981
|
+
if (plan.status !== "accepted" && plan.status !== "open") return fail8("No plan is stored.");
|
|
5982
|
+
if (await messagingOn(root) === "invalid") return fail8(ROLE_TEXT.badYaml);
|
|
5755
5983
|
const minutes = await timeoutMinutes(root);
|
|
5756
|
-
if (minutes === "invalid") return
|
|
5757
|
-
if (!input.model) return
|
|
5984
|
+
if (minutes === "invalid") return fail8(ROLE_TEXT.badYaml);
|
|
5985
|
+
if (!input.model) return fail8(ROLE_TEXT.noModel);
|
|
5758
5986
|
const now = (input.now ?? (() => /* @__PURE__ */ new Date()))();
|
|
5759
5987
|
const summaries = [];
|
|
5760
5988
|
const called = /* @__PURE__ */ new Set();
|
|
@@ -5774,7 +6002,7 @@ async function runNetwork(root, input) {
|
|
|
5774
6002
|
const when = new Date(now.getTime() + index * 1e3);
|
|
5775
6003
|
index += 1;
|
|
5776
6004
|
const runId = runIdFrom(when);
|
|
5777
|
-
const file =
|
|
6005
|
+
const file = join17(conversationDir(root, owner.name), `${runId}.jsonl`);
|
|
5778
6006
|
let raw;
|
|
5779
6007
|
try {
|
|
5780
6008
|
raw = await input.model.complete({
|
|
@@ -5818,7 +6046,7 @@ async function runNetwork(root, input) {
|
|
|
5818
6046
|
const waiting = all.some((node) => !plan.results.some((item) => item.decision === node.decision));
|
|
5819
6047
|
plan.status = waiting ? "open" : "done";
|
|
5820
6048
|
await writePlan(root, changeId, sectionId, plan);
|
|
5821
|
-
if (missingOwner) return
|
|
6049
|
+
if (missingOwner) return fail8(ROLE_TEXT.notFound);
|
|
5822
6050
|
const lines = [...summaries];
|
|
5823
6051
|
if (waiting) {
|
|
5824
6052
|
lines.push("A branch is waiting for its prerequisites.");
|
|
@@ -5826,25 +6054,25 @@ async function runNetwork(root, input) {
|
|
|
5826
6054
|
} else {
|
|
5827
6055
|
lines.push("The parent is done.");
|
|
5828
6056
|
}
|
|
5829
|
-
return
|
|
6057
|
+
return ok8(lines.join("\n"));
|
|
5830
6058
|
}
|
|
5831
6059
|
async function rebuildNetwork(root, input) {
|
|
5832
6060
|
const changeId = input.change?.trim() ?? "";
|
|
5833
6061
|
const sectionId = input.section?.trim() ?? "";
|
|
5834
|
-
if (!safeChangeId2(changeId) || !sectionId) return
|
|
6062
|
+
if (!safeChangeId2(changeId) || !sectionId) return fail8("No plan is stored.");
|
|
5835
6063
|
const plan = await readPlan(root, changeId, sectionId);
|
|
5836
|
-
if (!plan) return
|
|
6064
|
+
if (!plan) return fail8("No plan is stored.");
|
|
5837
6065
|
const allowed = plan.rebuilds === 0 || plan.rebuilds === 1 && !plan.stalled && plan.status === "accepted";
|
|
5838
|
-
if (!allowed) return
|
|
6066
|
+
if (!allowed) return fail8("A second rebuild does not start.");
|
|
5839
6067
|
const loaded = await acceptedFacts(root, changeId, sectionId);
|
|
5840
|
-
if (!loaded) return
|
|
5841
|
-
if (!input.model) return
|
|
6068
|
+
if (!loaded) return fail8(ROLE_TEXT.needsSection);
|
|
6069
|
+
if (!input.model) return fail8(ROLE_TEXT.noModel);
|
|
5842
6070
|
const services = distinctServices(loaded.all);
|
|
5843
6071
|
const picked = await pickLead(root, loaded.all, services);
|
|
5844
|
-
if (picked.kind === "missing") return
|
|
5845
|
-
if (picked.kind !== "lead") return
|
|
6072
|
+
if (picked.kind === "missing") return fail8(picked.sentence);
|
|
6073
|
+
if (picked.kind !== "lead") return fail8("This module needs a lead.");
|
|
5846
6074
|
const lead = await readRole(root, picked.name);
|
|
5847
|
-
if (!lead) return
|
|
6075
|
+
if (!lead) return fail8(ROLE_TEXT.notFound);
|
|
5848
6076
|
const before = JSON.stringify(plan);
|
|
5849
6077
|
const result2 = await storeLeadPlan(
|
|
5850
6078
|
root,
|
|
@@ -5911,6 +6139,378 @@ async function pickLead(root, all, services) {
|
|
|
5911
6139
|
return { kind: "none" };
|
|
5912
6140
|
}
|
|
5913
6141
|
|
|
6142
|
+
// src/role/guide.ts
|
|
6143
|
+
import { join as join18 } from "pathe";
|
|
6144
|
+
var PLANNING = /* @__PURE__ */ new Set([
|
|
6145
|
+
"intent",
|
|
6146
|
+
"vision",
|
|
6147
|
+
"requirements",
|
|
6148
|
+
"feature",
|
|
6149
|
+
"stories",
|
|
6150
|
+
"clarify_intent",
|
|
6151
|
+
"brainstorm",
|
|
6152
|
+
"hl_arb",
|
|
6153
|
+
"research",
|
|
6154
|
+
"outcome",
|
|
6155
|
+
"plan"
|
|
6156
|
+
]);
|
|
6157
|
+
var ARCHITECTURE = /* @__PURE__ */ new Set([
|
|
6158
|
+
"design",
|
|
6159
|
+
"architecture",
|
|
6160
|
+
"clarify_design",
|
|
6161
|
+
"lld",
|
|
6162
|
+
"db_design",
|
|
6163
|
+
"code_research"
|
|
6164
|
+
]);
|
|
6165
|
+
var CONSTRUCTION = /* @__PURE__ */ new Set([
|
|
6166
|
+
"tasks",
|
|
6167
|
+
"clarify_tasks",
|
|
6168
|
+
"implement",
|
|
6169
|
+
"local_verify",
|
|
6170
|
+
"acceptance",
|
|
6171
|
+
"pr_review"
|
|
6172
|
+
]);
|
|
6173
|
+
var DUTIES = [
|
|
6174
|
+
{
|
|
6175
|
+
name: "planner",
|
|
6176
|
+
label: "Planner",
|
|
6177
|
+
job: "Plan the change",
|
|
6178
|
+
bucket: "planning",
|
|
6179
|
+
owns: ["the request", "the plan"],
|
|
6180
|
+
skills: ["plan the change"],
|
|
6181
|
+
never: ["accept a spec", "write product code"],
|
|
6182
|
+
askHumanWhen: ["Who is this for?", "How will you know it worked?"],
|
|
6183
|
+
description: "The planner proposes. The person decides."
|
|
6184
|
+
},
|
|
6185
|
+
{
|
|
6186
|
+
name: "architect",
|
|
6187
|
+
label: "Architect",
|
|
6188
|
+
job: "Name the modules",
|
|
6189
|
+
bucket: "architecture",
|
|
6190
|
+
owns: ["the design"],
|
|
6191
|
+
skills: ["name the modules"],
|
|
6192
|
+
never: ["accept a spec", "write product code"],
|
|
6193
|
+
askHumanWhen: ["Which module owns this change?"],
|
|
6194
|
+
description: "The architect names the modules. The person decides."
|
|
6195
|
+
},
|
|
6196
|
+
{
|
|
6197
|
+
name: "builder",
|
|
6198
|
+
label: "Builder",
|
|
6199
|
+
job: "Build the change",
|
|
6200
|
+
bucket: "construction",
|
|
6201
|
+
owns: ["the change pack"],
|
|
6202
|
+
skills: ["build the change"],
|
|
6203
|
+
never: ["accept a spec", "generate a release"],
|
|
6204
|
+
askHumanWhen: [],
|
|
6205
|
+
description: "The builder builds the change. The person decides."
|
|
6206
|
+
}
|
|
6207
|
+
];
|
|
6208
|
+
function ok9(stdout) {
|
|
6209
|
+
return { exitCode: 0, stdout };
|
|
6210
|
+
}
|
|
6211
|
+
function fail9(stdout) {
|
|
6212
|
+
return { exitCode: 1, stdout };
|
|
6213
|
+
}
|
|
6214
|
+
function statePath(root) {
|
|
6215
|
+
return join18(root, ".sdd", "role-runs", "guide.yaml");
|
|
6216
|
+
}
|
|
6217
|
+
function dutyForStage(id, allIds) {
|
|
6218
|
+
if (PLANNING.has(id)) return "planning";
|
|
6219
|
+
if (ARCHITECTURE.has(id)) return "architecture";
|
|
6220
|
+
if (CONSTRUCTION.has(id)) return "construction";
|
|
6221
|
+
const index = allIds.indexOf(id);
|
|
6222
|
+
const archAt = allIds.findIndex((item) => ARCHITECTURE.has(item));
|
|
6223
|
+
const buildAt = allIds.findIndex((item) => CONSTRUCTION.has(item));
|
|
6224
|
+
if (archAt >= 0 && index >= 0 && index < archAt) return "planning";
|
|
6225
|
+
if (buildAt >= 0 && index >= buildAt) return "construction";
|
|
6226
|
+
if (archAt >= 0 && (buildAt < 0 || index < buildAt)) return "architecture";
|
|
6227
|
+
const third = Math.max(1, Math.ceil(allIds.length / 3));
|
|
6228
|
+
if (index < third) return "planning";
|
|
6229
|
+
if (index < third * 2) return "architecture";
|
|
6230
|
+
return "construction";
|
|
6231
|
+
}
|
|
6232
|
+
function asText(value) {
|
|
6233
|
+
return typeof value === "string" ? value.trim() : "";
|
|
6234
|
+
}
|
|
6235
|
+
function parseState(value) {
|
|
6236
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
6237
|
+
const rec = value;
|
|
6238
|
+
const change = asText(rec.change);
|
|
6239
|
+
const request = asText(rec.request);
|
|
6240
|
+
if (!change || !request || !Array.isArray(rec.answers)) return null;
|
|
6241
|
+
const answers = [];
|
|
6242
|
+
for (const item of rec.answers) {
|
|
6243
|
+
if (!item || typeof item !== "object" || Array.isArray(item)) return null;
|
|
6244
|
+
const row = item;
|
|
6245
|
+
const role = asText(row.role);
|
|
6246
|
+
const question = asText(row.question);
|
|
6247
|
+
const answer = asText(row.answer);
|
|
6248
|
+
if (!role || !question || !answer) return null;
|
|
6249
|
+
answers.push({ role, question, answer });
|
|
6250
|
+
}
|
|
6251
|
+
let pending;
|
|
6252
|
+
if (rec.pending !== void 0) {
|
|
6253
|
+
if (!rec.pending || typeof rec.pending !== "object" || Array.isArray(rec.pending)) return null;
|
|
6254
|
+
const row = rec.pending;
|
|
6255
|
+
const role = asText(row.role);
|
|
6256
|
+
const question = asText(row.question);
|
|
6257
|
+
const kind = row.kind === "model" ? "model" : row.kind === "role" ? "role" : "";
|
|
6258
|
+
if (!role || !question || !kind) return null;
|
|
6259
|
+
pending = { role, kind, question };
|
|
6260
|
+
}
|
|
6261
|
+
const state = { change, request, answers };
|
|
6262
|
+
if (pending) state.pending = pending;
|
|
6263
|
+
return state;
|
|
6264
|
+
}
|
|
6265
|
+
async function readState(root) {
|
|
6266
|
+
const path = statePath(root);
|
|
6267
|
+
if (!await pathExists(path)) return null;
|
|
6268
|
+
try {
|
|
6269
|
+
const parsed = parseState(await readYaml(path));
|
|
6270
|
+
return parsed ?? "invalid";
|
|
6271
|
+
} catch {
|
|
6272
|
+
return "invalid";
|
|
6273
|
+
}
|
|
6274
|
+
}
|
|
6275
|
+
async function writeState(root, state) {
|
|
6276
|
+
await ensureDir(join18(root, ".sdd", "role-runs"));
|
|
6277
|
+
const body = {
|
|
6278
|
+
change: state.change,
|
|
6279
|
+
request: state.request,
|
|
6280
|
+
answers: state.answers
|
|
6281
|
+
};
|
|
6282
|
+
if (state.pending) body.pending = state.pending;
|
|
6283
|
+
await writeYaml(statePath(root), body);
|
|
6284
|
+
}
|
|
6285
|
+
function questionText(role, question) {
|
|
6286
|
+
return [`Question:`, `${role} asks:`, question, `Answer with: sdd go --answer "..."`.trim()].join(
|
|
6287
|
+
"\n"
|
|
6288
|
+
);
|
|
6289
|
+
}
|
|
6290
|
+
function nextRoleQuestion(role, answers) {
|
|
6291
|
+
const answered = new Set(
|
|
6292
|
+
answers.filter((item) => item.role === role.name).map((item) => item.question)
|
|
6293
|
+
);
|
|
6294
|
+
return role.askHumanWhen.find((item) => !answered.has(item));
|
|
6295
|
+
}
|
|
6296
|
+
function taskFor(request, role, answers) {
|
|
6297
|
+
const rows = [`Request: ${request}`];
|
|
6298
|
+
const mine = answers.filter((item) => item.role === role);
|
|
6299
|
+
if (mine.length > 0) {
|
|
6300
|
+
rows.push("Answers:");
|
|
6301
|
+
for (const item of mine) rows.push(`- ${item.question}: ${item.answer}`);
|
|
6302
|
+
}
|
|
6303
|
+
return rows.join("\n");
|
|
6304
|
+
}
|
|
6305
|
+
async function roleFiles(root) {
|
|
6306
|
+
const names = await listFiles(join18(root, ".sdd", "roles"));
|
|
6307
|
+
return names.filter((name) => name.endsWith(".md"));
|
|
6308
|
+
}
|
|
6309
|
+
async function prepareGuide(root, input = {}) {
|
|
6310
|
+
const workflowName = input.workflow?.trim() || "feature";
|
|
6311
|
+
try {
|
|
6312
|
+
if (!await isInitialized(root)) {
|
|
6313
|
+
await initProject({ projectRoot: root, agents: input.agents ?? false });
|
|
6314
|
+
}
|
|
6315
|
+
const workflow = await loadWorkflow(root, workflowName);
|
|
6316
|
+
const ids = workflow.stages.map((stage) => stage.id);
|
|
6317
|
+
const existing = await roleFiles(root);
|
|
6318
|
+
if (existing.length === 0) {
|
|
6319
|
+
for (const duty of DUTIES) {
|
|
6320
|
+
const { bucket, ...role } = duty;
|
|
6321
|
+
const stages = ids.filter((id) => dutyForStage(id, ids) === bucket);
|
|
6322
|
+
const created = await createRole(root, { ...role, stages });
|
|
6323
|
+
if (created.exitCode !== 0) return created;
|
|
6324
|
+
}
|
|
6325
|
+
}
|
|
6326
|
+
const lines = ["Ready.", `Workflow: ${workflowName}.`];
|
|
6327
|
+
if (existing.length > 0) {
|
|
6328
|
+
lines.push("Roles are already here.");
|
|
6329
|
+
} else {
|
|
6330
|
+
for (const duty of DUTIES) {
|
|
6331
|
+
const stages = ids.filter((id) => dutyForStage(id, ids) === duty.bucket);
|
|
6332
|
+
lines.push(`${duty.name}: ${stages.join(", ") || "no stage on this workflow"}`);
|
|
6333
|
+
}
|
|
6334
|
+
}
|
|
6335
|
+
lines.push('Next: sdd go "what you want to build"');
|
|
6336
|
+
return ok9(lines.join("\n"));
|
|
6337
|
+
} catch (error) {
|
|
6338
|
+
return fail9(error instanceof Error ? error.message : String(error));
|
|
6339
|
+
}
|
|
6340
|
+
}
|
|
6341
|
+
async function writeDraft(ctxPath, stage, role, text2, request) {
|
|
6342
|
+
const body = [
|
|
6343
|
+
`# ${stage.title ?? stage.id}`,
|
|
6344
|
+
"",
|
|
6345
|
+
`${role} drafted this stage. The person has not signed it.`,
|
|
6346
|
+
"",
|
|
6347
|
+
text2.trim(),
|
|
6348
|
+
"",
|
|
6349
|
+
`Request: ${request}`,
|
|
6350
|
+
""
|
|
6351
|
+
].join("\n");
|
|
6352
|
+
for (const artifact of stage.artifacts) {
|
|
6353
|
+
if (!artifact.required) continue;
|
|
6354
|
+
const abs = join18(ctxPath, artifact.path);
|
|
6355
|
+
if (await pathExists(abs)) {
|
|
6356
|
+
const current = await readText(abs);
|
|
6357
|
+
if (isSubstantiveArtifactContent(current)) continue;
|
|
6358
|
+
}
|
|
6359
|
+
await writeText(abs, body);
|
|
6360
|
+
}
|
|
6361
|
+
}
|
|
6362
|
+
function optionalStage(stage) {
|
|
6363
|
+
return stage.optional || stage.kind === "clarify" || stage.kind === "brainstorm";
|
|
6364
|
+
}
|
|
6365
|
+
async function runGuide(root, input = {}) {
|
|
6366
|
+
const prepared = await prepareGuide(root, {
|
|
6367
|
+
workflow: input.workflow,
|
|
6368
|
+
agents: input.agents
|
|
6369
|
+
});
|
|
6370
|
+
if (prepared.exitCode !== 0) return prepared;
|
|
6371
|
+
const title = input.title?.trim() ?? "";
|
|
6372
|
+
const answer = input.answer?.trim() ?? "";
|
|
6373
|
+
if (input.answer !== void 0 && !answer) return fail9("An answer needs text.");
|
|
6374
|
+
let loaded;
|
|
6375
|
+
try {
|
|
6376
|
+
loaded = await readState(root);
|
|
6377
|
+
} catch (error) {
|
|
6378
|
+
return fail9(error instanceof Error ? error.message : String(error));
|
|
6379
|
+
}
|
|
6380
|
+
if (loaded === "invalid") return fail9("The guide file is invalid.");
|
|
6381
|
+
const config = await loadConfig(root);
|
|
6382
|
+
let state = loaded;
|
|
6383
|
+
if (title) {
|
|
6384
|
+
try {
|
|
6385
|
+
const ctx = await createChange({
|
|
6386
|
+
projectRoot: root,
|
|
6387
|
+
config,
|
|
6388
|
+
title,
|
|
6389
|
+
workflowName: input.workflow?.trim() || "feature"
|
|
6390
|
+
});
|
|
6391
|
+
state = { change: ctx.id, request: title, answers: [] };
|
|
6392
|
+
await writeState(root, state);
|
|
6393
|
+
} catch (error) {
|
|
6394
|
+
return fail9(error instanceof Error ? error.message : String(error));
|
|
6395
|
+
}
|
|
6396
|
+
} else if (!state) {
|
|
6397
|
+
const active = await getActiveChangeId(root, config);
|
|
6398
|
+
if (!active) return prepared;
|
|
6399
|
+
try {
|
|
6400
|
+
const ctx = await buildContext(root, config, active);
|
|
6401
|
+
state = { change: active, request: ctx.meta.title, answers: [] };
|
|
6402
|
+
} catch (error) {
|
|
6403
|
+
return fail9(error instanceof Error ? error.message : String(error));
|
|
6404
|
+
}
|
|
6405
|
+
}
|
|
6406
|
+
if (answer && !state.pending) return fail9("No question is waiting.");
|
|
6407
|
+
if (answer && state.pending) {
|
|
6408
|
+
state.answers.push({
|
|
6409
|
+
role: state.pending.role,
|
|
6410
|
+
question: state.pending.question,
|
|
6411
|
+
answer
|
|
6412
|
+
});
|
|
6413
|
+
state.pending = void 0;
|
|
6414
|
+
await writeState(root, state);
|
|
6415
|
+
} else if (state.pending) {
|
|
6416
|
+
return ok9(questionText(state.pending.role, state.pending.question));
|
|
6417
|
+
}
|
|
6418
|
+
const lines = [];
|
|
6419
|
+
let lastRole = "";
|
|
6420
|
+
for (let step = 0; step < 16; step++) {
|
|
6421
|
+
let ctx;
|
|
6422
|
+
try {
|
|
6423
|
+
ctx = await buildContext(root, config, state.change);
|
|
6424
|
+
} catch (error) {
|
|
6425
|
+
return fail9(error instanceof Error ? error.message : String(error));
|
|
6426
|
+
}
|
|
6427
|
+
const stage = getStage(ctx.workflow, ctx.meta, ctx.meta.stage);
|
|
6428
|
+
if (!stage) return fail9("No active change.");
|
|
6429
|
+
if (optionalStage(stage) && stage.skippable) {
|
|
6430
|
+
const before = ctx.meta.stage;
|
|
6431
|
+
try {
|
|
6432
|
+
await skipStage(root, config, ctx.id, stage.id, "No role runs this optional stage.");
|
|
6433
|
+
} catch (error) {
|
|
6434
|
+
return fail9(error instanceof Error ? error.message : String(error));
|
|
6435
|
+
}
|
|
6436
|
+
const after = await buildContext(root, config, state.change);
|
|
6437
|
+
if (after.meta.stage === before) return fail9("This stage stayed put.");
|
|
6438
|
+
lines.push(`Skipped ${stage.id}.`);
|
|
6439
|
+
continue;
|
|
6440
|
+
}
|
|
6441
|
+
const routed = await matchRoute(root, { stage: stage.id });
|
|
6442
|
+
if (!routed.ok) return fail9(routed.message);
|
|
6443
|
+
if (routed.match.kind === "ask") {
|
|
6444
|
+
return ok9(["Stopped.", "Ask the human.", routed.match.reason, ...lines].join("\n"));
|
|
6445
|
+
}
|
|
6446
|
+
const role = await readRole(root, routed.match.role);
|
|
6447
|
+
if (!role) return fail9(ROLE_TEXT.notFound);
|
|
6448
|
+
if (lastRole && lastRole !== role.name) lines.push(`Handoff: ${lastRole} to ${role.name}.`);
|
|
6449
|
+
const question = nextRoleQuestion(role, state.answers);
|
|
6450
|
+
if (question) {
|
|
6451
|
+
state.pending = { role: role.name, kind: "role", question };
|
|
6452
|
+
await writeState(root, state);
|
|
6453
|
+
return ok9(
|
|
6454
|
+
[
|
|
6455
|
+
"Question:",
|
|
6456
|
+
...lines,
|
|
6457
|
+
`${role.name} asks:`,
|
|
6458
|
+
question,
|
|
6459
|
+
'Answer with: sdd go --answer "..."'
|
|
6460
|
+
].join("\n")
|
|
6461
|
+
);
|
|
6462
|
+
}
|
|
6463
|
+
if (!input.model) return fail9(ROLE_TEXT.noModel);
|
|
6464
|
+
const ran = await runRole(root, {
|
|
6465
|
+
name: role.name,
|
|
6466
|
+
task: taskFor(state.request, role.name, state.answers),
|
|
6467
|
+
guide: { changeDir: ctx.displayPath },
|
|
6468
|
+
model: input.model,
|
|
6469
|
+
now: input.now
|
|
6470
|
+
});
|
|
6471
|
+
if (ran.exitCode !== 0) {
|
|
6472
|
+
return fail9([ran.stdout, ...lines].filter((line) => line.length > 0).join("\n"));
|
|
6473
|
+
}
|
|
6474
|
+
const text2 = ran.stdout.trim();
|
|
6475
|
+
if (text2.startsWith("Question:")) {
|
|
6476
|
+
const asked = text2.replace(/^Question:\s*/, "").trim();
|
|
6477
|
+
state.pending = { role: role.name, kind: "model", question: asked || text2 };
|
|
6478
|
+
await writeState(root, state);
|
|
6479
|
+
return ok9(
|
|
6480
|
+
[
|
|
6481
|
+
"Question:",
|
|
6482
|
+
...lines,
|
|
6483
|
+
`${role.name} asks:`,
|
|
6484
|
+
state.pending.question,
|
|
6485
|
+
'Answer with: sdd go --answer "..."'
|
|
6486
|
+
].join("\n")
|
|
6487
|
+
);
|
|
6488
|
+
}
|
|
6489
|
+
await writeDraft(ctx.path, stage, role.name, text2, state.request);
|
|
6490
|
+
try {
|
|
6491
|
+
const advanced = await advanceStage(root, config, ctx.id);
|
|
6492
|
+
lines.push(`${role.name} drafted ${stage.id}.`);
|
|
6493
|
+
lastRole = role.name;
|
|
6494
|
+
if (!advanced.to) {
|
|
6495
|
+
await writeState(root, state);
|
|
6496
|
+
return ok9(
|
|
6497
|
+
[
|
|
6498
|
+
"Done.",
|
|
6499
|
+
...lines,
|
|
6500
|
+
"No further stages. Run `sdd complete` when ready.",
|
|
6501
|
+
"The person has not signed this change."
|
|
6502
|
+
].join("\n")
|
|
6503
|
+
);
|
|
6504
|
+
}
|
|
6505
|
+
} catch (error) {
|
|
6506
|
+
await writeState(root, state);
|
|
6507
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
6508
|
+
return ok9(["Stopped.", `${role.name} drafted ${stage.id}.`, message, ...lines].join("\n"));
|
|
6509
|
+
}
|
|
6510
|
+
}
|
|
6511
|
+
return fail9("The guide stopped after 16 steps.");
|
|
6512
|
+
}
|
|
6513
|
+
|
|
5914
6514
|
// src/role/models/show.ts
|
|
5915
6515
|
function formatModelChoice(workType, choice) {
|
|
5916
6516
|
return `${workType} ${choice.id} ${choice.kind} ${choice.provider} ${choice.why}`;
|
|
@@ -5949,7 +6549,7 @@ function explainModel(config, workType, ctx) {
|
|
|
5949
6549
|
}
|
|
5950
6550
|
|
|
5951
6551
|
// src/stories.ts
|
|
5952
|
-
import { join as
|
|
6552
|
+
import { join as join19 } from "pathe";
|
|
5953
6553
|
var STORY_ID = /^S-\d+$/i;
|
|
5954
6554
|
var EPIC_KEY = /^[A-Za-z0-9][A-Za-z0-9-]{0,39}$/;
|
|
5955
6555
|
function validBranchName(name) {
|
|
@@ -5991,7 +6591,7 @@ function parseStoryDraft(input) {
|
|
|
5991
6591
|
async function metasIn(dir) {
|
|
5992
6592
|
const out = [];
|
|
5993
6593
|
for (const id of await listDirs(dir)) {
|
|
5994
|
-
const path =
|
|
6594
|
+
const path = join19(dir, id, "meta.yaml");
|
|
5995
6595
|
if (!await pathExists(path)) continue;
|
|
5996
6596
|
try {
|
|
5997
6597
|
out.push(ChangeMetaSchema.parse(await readYaml(path)));
|
|
@@ -6212,6 +6812,7 @@ export {
|
|
|
6212
6812
|
displayChangePath,
|
|
6213
6813
|
domainsDir,
|
|
6214
6814
|
duplicateRole,
|
|
6815
|
+
dutyForStage,
|
|
6215
6816
|
editRole,
|
|
6216
6817
|
embedSearchQuery,
|
|
6217
6818
|
emptyChangeMetrics,
|
|
@@ -6324,6 +6925,7 @@ export {
|
|
|
6324
6925
|
planAgentIntegration,
|
|
6325
6926
|
planNetwork,
|
|
6326
6927
|
postRoom,
|
|
6928
|
+
prepareGuide,
|
|
6327
6929
|
productTeamHome,
|
|
6328
6930
|
promoteGreenfieldToMemory,
|
|
6329
6931
|
queueRole,
|
|
@@ -6365,8 +6967,10 @@ export {
|
|
|
6365
6967
|
resolveSearchProvider,
|
|
6366
6968
|
resolveStages,
|
|
6367
6969
|
reviewRoom,
|
|
6970
|
+
roleContextBody,
|
|
6368
6971
|
routeRole,
|
|
6369
6972
|
rrf,
|
|
6973
|
+
runGuide,
|
|
6370
6974
|
runLocalVerify,
|
|
6371
6975
|
runLoop,
|
|
6372
6976
|
runNetwork,
|
|
@@ -6382,6 +6986,7 @@ export {
|
|
|
6382
6986
|
selectModel,
|
|
6383
6987
|
setActiveChange,
|
|
6384
6988
|
setFact,
|
|
6989
|
+
setRoleContext,
|
|
6385
6990
|
setRoleMessaging,
|
|
6386
6991
|
shouldAutoSkip,
|
|
6387
6992
|
showFact,
|