sb-codex-tool 0.1.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/README.ko.md +304 -0
- package/README.md +309 -0
- package/bin/sb-codex-tool.js +18 -0
- package/dist/cli.js +3583 -0
- package/package.json +50 -0
package/dist/cli.js
ADDED
|
@@ -0,0 +1,3583 @@
|
|
|
1
|
+
// src/lib/assignment.ts
|
|
2
|
+
import path5 from "node:path";
|
|
3
|
+
|
|
4
|
+
// src/lib/cycle.ts
|
|
5
|
+
import path2 from "node:path";
|
|
6
|
+
|
|
7
|
+
// src/lib/fs.ts
|
|
8
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
9
|
+
import path from "node:path";
|
|
10
|
+
function ensureDir(directory) {
|
|
11
|
+
mkdirSync(directory, { recursive: true });
|
|
12
|
+
}
|
|
13
|
+
function readTextIfPresent(filePath) {
|
|
14
|
+
if (!existsSync(filePath)) {
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
return readFileSync(filePath, "utf8");
|
|
18
|
+
}
|
|
19
|
+
function readJsonIfPresent(filePath) {
|
|
20
|
+
const text = readTextIfPresent(filePath);
|
|
21
|
+
if (text === null) {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
return JSON.parse(text);
|
|
25
|
+
}
|
|
26
|
+
function writeFileIfMissing(filePath, content) {
|
|
27
|
+
ensureDir(path.dirname(filePath));
|
|
28
|
+
if (existsSync(filePath)) {
|
|
29
|
+
return "kept";
|
|
30
|
+
}
|
|
31
|
+
writeFileSync(filePath, content, "utf8");
|
|
32
|
+
return "created";
|
|
33
|
+
}
|
|
34
|
+
function writeFileIfChanged(filePath, content) {
|
|
35
|
+
ensureDir(path.dirname(filePath));
|
|
36
|
+
if (!existsSync(filePath)) {
|
|
37
|
+
writeFileSync(filePath, content, "utf8");
|
|
38
|
+
return "created";
|
|
39
|
+
}
|
|
40
|
+
const current = readFileSync(filePath, "utf8");
|
|
41
|
+
if (current === content) {
|
|
42
|
+
return "unchanged";
|
|
43
|
+
}
|
|
44
|
+
writeFileSync(filePath, content, "utf8");
|
|
45
|
+
return "updated";
|
|
46
|
+
}
|
|
47
|
+
function ensureLines(filePath, lines, header) {
|
|
48
|
+
ensureDir(path.dirname(filePath));
|
|
49
|
+
if (!existsSync(filePath)) {
|
|
50
|
+
const content = `${header}
|
|
51
|
+
|
|
52
|
+
${lines.join("\n")}
|
|
53
|
+
`;
|
|
54
|
+
writeFileSync(filePath, content, "utf8");
|
|
55
|
+
return { created: true, added: [...lines] };
|
|
56
|
+
}
|
|
57
|
+
const current = readFileSync(filePath, "utf8");
|
|
58
|
+
const missing = lines.filter((line) => !current.includes(line));
|
|
59
|
+
if (missing.length === 0) {
|
|
60
|
+
return { created: false, added: [] };
|
|
61
|
+
}
|
|
62
|
+
const next = current.includes(header) ? `${current.trimEnd()}
|
|
63
|
+
${missing.join("\n")}
|
|
64
|
+
` : `${current.trimEnd()}
|
|
65
|
+
|
|
66
|
+
${header}
|
|
67
|
+
|
|
68
|
+
${missing.join("\n")}
|
|
69
|
+
`;
|
|
70
|
+
writeFileSync(filePath, next, "utf8");
|
|
71
|
+
return { created: false, added: missing };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// src/lib/cycle.ts
|
|
75
|
+
function formatDateStamp(date = /* @__PURE__ */ new Date()) {
|
|
76
|
+
const year = String(date.getFullYear());
|
|
77
|
+
const month = String(date.getMonth() + 1).padStart(2, "0");
|
|
78
|
+
const day = String(date.getDate()).padStart(2, "0");
|
|
79
|
+
return `${year}-${month}-${day}`;
|
|
80
|
+
}
|
|
81
|
+
function normalizeSlug(input) {
|
|
82
|
+
return input.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
83
|
+
}
|
|
84
|
+
function humanizeSlug(slug) {
|
|
85
|
+
return slug.split("-").filter((part) => part.length > 0).map((part) => part[0].toUpperCase() + part.slice(1)).join(" ");
|
|
86
|
+
}
|
|
87
|
+
function parseCycleDescriptor(planPath) {
|
|
88
|
+
const match = planPath.match(
|
|
89
|
+
/^\.sb-codex-tool\/plans\/(\d{4}-\d{2}-\d{2})-(.+)-approved\.md$/
|
|
90
|
+
);
|
|
91
|
+
if (match === null) {
|
|
92
|
+
throw new Error(`Unable to derive cycle metadata from ${planPath}.`);
|
|
93
|
+
}
|
|
94
|
+
return {
|
|
95
|
+
dateStamp: match[1],
|
|
96
|
+
slug: match[2]
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
function readCycleTitle(root, planPath, fallbackSlug) {
|
|
100
|
+
const text = readTextIfPresent(path2.join(root, planPath));
|
|
101
|
+
const heading = text?.match(/^# Approved Plan: (.+)$/m)?.[1];
|
|
102
|
+
if (heading !== void 0) {
|
|
103
|
+
return heading.trim();
|
|
104
|
+
}
|
|
105
|
+
return humanizeSlug(fallbackSlug);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// src/lib/paths.ts
|
|
109
|
+
import { existsSync as existsSync2 } from "node:fs";
|
|
110
|
+
import path3 from "node:path";
|
|
111
|
+
var STATE_ROOT_NAME = ".sb-codex-tool";
|
|
112
|
+
var WORKFLOW_NAMES = [
|
|
113
|
+
"clarify",
|
|
114
|
+
"plan",
|
|
115
|
+
"execute",
|
|
116
|
+
"refactor",
|
|
117
|
+
"verify"
|
|
118
|
+
];
|
|
119
|
+
var REQUIRED_DIRECTORIES = [
|
|
120
|
+
STATE_ROOT_NAME,
|
|
121
|
+
`${STATE_ROOT_NAME}/plans`,
|
|
122
|
+
`${STATE_ROOT_NAME}/runs`,
|
|
123
|
+
`${STATE_ROOT_NAME}/summaries`,
|
|
124
|
+
`${STATE_ROOT_NAME}/handoffs`,
|
|
125
|
+
`${STATE_ROOT_NAME}/guides`,
|
|
126
|
+
`${STATE_ROOT_NAME}/index`,
|
|
127
|
+
`${STATE_ROOT_NAME}/reviews`,
|
|
128
|
+
`${STATE_ROOT_NAME}/logs`,
|
|
129
|
+
`${STATE_ROOT_NAME}/logs/work-journal`,
|
|
130
|
+
`${STATE_ROOT_NAME}/ignore`,
|
|
131
|
+
`${STATE_ROOT_NAME}/workflows`
|
|
132
|
+
];
|
|
133
|
+
var REQUIRED_FILES = [
|
|
134
|
+
`${STATE_ROOT_NAME}/project.md`,
|
|
135
|
+
`${STATE_ROOT_NAME}/state.md`,
|
|
136
|
+
`${STATE_ROOT_NAME}/guides/read-this-first.md`,
|
|
137
|
+
`${STATE_ROOT_NAME}/guides/code-consistency.md`,
|
|
138
|
+
`${STATE_ROOT_NAME}/index/current.json`,
|
|
139
|
+
`${STATE_ROOT_NAME}/index/README.md`,
|
|
140
|
+
`${STATE_ROOT_NAME}/plans/README.md`,
|
|
141
|
+
`${STATE_ROOT_NAME}/runs/README.md`,
|
|
142
|
+
`${STATE_ROOT_NAME}/summaries/README.md`,
|
|
143
|
+
`${STATE_ROOT_NAME}/handoffs/README.md`,
|
|
144
|
+
`${STATE_ROOT_NAME}/reviews/README.md`,
|
|
145
|
+
`${STATE_ROOT_NAME}/logs/work-journal/README.md`,
|
|
146
|
+
`${STATE_ROOT_NAME}/ignore/base.ignore`,
|
|
147
|
+
"AGENTS.md",
|
|
148
|
+
".gitignore",
|
|
149
|
+
".ignore",
|
|
150
|
+
".rgignore"
|
|
151
|
+
];
|
|
152
|
+
var REQUIRED_WORKFLOW_FILES = WORKFLOW_NAMES.map(
|
|
153
|
+
(name) => `${STATE_ROOT_NAME}/workflows/${name}.md`
|
|
154
|
+
);
|
|
155
|
+
var ROOT_MARKERS = [
|
|
156
|
+
"package.json",
|
|
157
|
+
`${STATE_ROOT_NAME}/state.md`,
|
|
158
|
+
"docs/menu/implementation.md",
|
|
159
|
+
".git"
|
|
160
|
+
];
|
|
161
|
+
function hasRootMarker(directory) {
|
|
162
|
+
return ROOT_MARKERS.some((marker) => existsSync2(path3.join(directory, marker)));
|
|
163
|
+
}
|
|
164
|
+
function resolveProjectRoot(start = process.cwd()) {
|
|
165
|
+
let current = path3.resolve(start);
|
|
166
|
+
while (true) {
|
|
167
|
+
if (hasRootMarker(current)) {
|
|
168
|
+
return current;
|
|
169
|
+
}
|
|
170
|
+
const parent = path3.dirname(current);
|
|
171
|
+
if (parent === current) {
|
|
172
|
+
return path3.resolve(start);
|
|
173
|
+
}
|
|
174
|
+
current = parent;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
function statePath(root, ...segments) {
|
|
178
|
+
return path3.join(root, STATE_ROOT_NAME, ...segments);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// src/lib/current-state.ts
|
|
182
|
+
function renderBulletList(items) {
|
|
183
|
+
if (items.length === 0) {
|
|
184
|
+
return "- none";
|
|
185
|
+
}
|
|
186
|
+
return items.map((item) => `- ${item}`).join("\n");
|
|
187
|
+
}
|
|
188
|
+
function renderReference(label, value) {
|
|
189
|
+
return `- ${label}: ${value ?? "none yet"}`;
|
|
190
|
+
}
|
|
191
|
+
function renderHotPath(hotPath) {
|
|
192
|
+
return hotPath.map((file, index) => `${index + 1}. ${file}`).join("\n");
|
|
193
|
+
}
|
|
194
|
+
function renderCurrentCycle(current) {
|
|
195
|
+
return [
|
|
196
|
+
`- Current stage: ${current.currentStage}`,
|
|
197
|
+
`- Latest approved plan: ${current.latestApprovedPlan ?? "none yet"}`,
|
|
198
|
+
`- Latest relevant summary: ${current.latestRelevantSummary ?? "none yet"}`,
|
|
199
|
+
`- Latest lifecycle run: ${current.latestRun ?? "none yet"}`,
|
|
200
|
+
`- Latest consistency review: ${current.latestConsistencyReview ?? "none yet"}`,
|
|
201
|
+
`- Latest assignment lifecycle: ${current.latestAssignmentLifecycle ?? "none yet"}`,
|
|
202
|
+
`- Current task guide: ${current.currentGuide ?? "none yet"}`,
|
|
203
|
+
`- Current handoff: ${current.currentHandoff ?? "none yet"}`,
|
|
204
|
+
`- Current review: ${current.currentReview ?? "none yet"}`
|
|
205
|
+
].join("\n");
|
|
206
|
+
}
|
|
207
|
+
function renderAssignmentGuides(assignmentGuides) {
|
|
208
|
+
const entries = Object.entries(assignmentGuides);
|
|
209
|
+
if (entries.length === 0) {
|
|
210
|
+
return "- none";
|
|
211
|
+
}
|
|
212
|
+
return entries.sort(([left], [right]) => left.localeCompare(right)).map(([agent, guidePath]) => `- ${agent}: ${guidePath}`).join("\n");
|
|
213
|
+
}
|
|
214
|
+
function renderImplementationReference(implementationMenuPath) {
|
|
215
|
+
if (implementationMenuPath === null) {
|
|
216
|
+
return "- If this repo has implementation contracts, add them here.\n";
|
|
217
|
+
}
|
|
218
|
+
return `- ${implementationMenuPath}
|
|
219
|
+
`;
|
|
220
|
+
}
|
|
221
|
+
function createInitialCurrentIndex() {
|
|
222
|
+
return {
|
|
223
|
+
version: 2,
|
|
224
|
+
currentStage: "clarify",
|
|
225
|
+
nextAction: "Review AGENTS.md and customize .sb-codex-tool/project.md.",
|
|
226
|
+
latestApprovedPlan: null,
|
|
227
|
+
latestRelevantSummary: null,
|
|
228
|
+
latestRun: null,
|
|
229
|
+
latestConsistencyReview: null,
|
|
230
|
+
latestAssignmentLifecycle: null,
|
|
231
|
+
currentGuide: null,
|
|
232
|
+
currentHandoff: null,
|
|
233
|
+
currentReview: null,
|
|
234
|
+
currentFocusModules: [
|
|
235
|
+
"AGENTS.md",
|
|
236
|
+
`${STATE_ROOT_NAME}/project.md`,
|
|
237
|
+
`${STATE_ROOT_NAME}/state.md`,
|
|
238
|
+
`${STATE_ROOT_NAME}/guides/code-consistency.md`
|
|
239
|
+
],
|
|
240
|
+
hotPath: [
|
|
241
|
+
`${STATE_ROOT_NAME}/project.md`,
|
|
242
|
+
`${STATE_ROOT_NAME}/state.md`,
|
|
243
|
+
`${STATE_ROOT_NAME}/guides/read-this-first.md`,
|
|
244
|
+
`${STATE_ROOT_NAME}/guides/code-consistency.md`
|
|
245
|
+
],
|
|
246
|
+
activeAgents: {
|
|
247
|
+
main: "unassigned",
|
|
248
|
+
subagents: [],
|
|
249
|
+
verification: null,
|
|
250
|
+
consistency: null
|
|
251
|
+
},
|
|
252
|
+
assignmentGuides: {},
|
|
253
|
+
notes: [
|
|
254
|
+
"Keep this file current enough for a fresh agent to start from the hot path."
|
|
255
|
+
]
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
function normalizeCurrentIndex(current) {
|
|
259
|
+
const initial = createInitialCurrentIndex();
|
|
260
|
+
return {
|
|
261
|
+
...initial,
|
|
262
|
+
...current,
|
|
263
|
+
version: Math.max(current?.version ?? 0, initial.version),
|
|
264
|
+
currentFocusModules: current?.currentFocusModules ?? initial.currentFocusModules,
|
|
265
|
+
hotPath: current?.hotPath ?? initial.hotPath,
|
|
266
|
+
activeAgents: {
|
|
267
|
+
...initial.activeAgents,
|
|
268
|
+
...current?.activeAgents,
|
|
269
|
+
subagents: current?.activeAgents?.subagents ?? initial.activeAgents.subagents
|
|
270
|
+
},
|
|
271
|
+
assignmentGuides: current?.assignmentGuides ?? initial.assignmentGuides,
|
|
272
|
+
notes: current?.notes ?? initial.notes
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
function renderStateMarkdown(current) {
|
|
276
|
+
return `# Current State
|
|
277
|
+
|
|
278
|
+
## Current Workflow Stage
|
|
279
|
+
|
|
280
|
+
- ${current.currentStage}
|
|
281
|
+
|
|
282
|
+
## One Next Action
|
|
283
|
+
|
|
284
|
+
- ${current.nextAction}
|
|
285
|
+
|
|
286
|
+
## Current Focus Modules
|
|
287
|
+
|
|
288
|
+
${renderBulletList(current.currentFocusModules)}
|
|
289
|
+
|
|
290
|
+
## References
|
|
291
|
+
|
|
292
|
+
${renderReference("Latest approved plan", current.latestApprovedPlan)}
|
|
293
|
+
${renderReference("Latest relevant summary", current.latestRelevantSummary)}
|
|
294
|
+
${renderReference("Latest lifecycle run", current.latestRun)}
|
|
295
|
+
${renderReference("Latest consistency review", current.latestConsistencyReview)}
|
|
296
|
+
${renderReference("Latest assignment lifecycle", current.latestAssignmentLifecycle)}
|
|
297
|
+
${renderReference("Current task guide", current.currentGuide)}
|
|
298
|
+
${renderReference("Current handoff", current.currentHandoff)}
|
|
299
|
+
${renderReference("Current review", current.currentReview)}
|
|
300
|
+
|
|
301
|
+
## Active Agent Map
|
|
302
|
+
|
|
303
|
+
- Main agent: ${current.activeAgents.main ?? "none"}
|
|
304
|
+
- Execution subagents: ${current.activeAgents.subagents.length > 0 ? current.activeAgents.subagents.join(", ") : "none"}
|
|
305
|
+
- Verification agent: ${current.activeAgents.verification ?? "none"}
|
|
306
|
+
- Code consistency agent: ${current.activeAgents.consistency ?? "none"}
|
|
307
|
+
|
|
308
|
+
## Active Assignment Guides
|
|
309
|
+
|
|
310
|
+
${renderAssignmentGuides(current.assignmentGuides)}
|
|
311
|
+
|
|
312
|
+
## Notes
|
|
313
|
+
|
|
314
|
+
${renderBulletList(current.notes)}
|
|
315
|
+
`;
|
|
316
|
+
}
|
|
317
|
+
function renderReadThisFirstMarkdown(current, implementationMenuPath) {
|
|
318
|
+
return `# Read This First
|
|
319
|
+
|
|
320
|
+
## Hot Path
|
|
321
|
+
|
|
322
|
+
Read in this order before implementation or verification:
|
|
323
|
+
|
|
324
|
+
${renderHotPath(current.hotPath)}
|
|
325
|
+
|
|
326
|
+
## Additional Repo Docs
|
|
327
|
+
|
|
328
|
+
${renderImplementationReference(implementationMenuPath)}
|
|
329
|
+
- docs/implementation/verification-contract.md
|
|
330
|
+
- docs/implementation/acceptance-checklist.md
|
|
331
|
+
|
|
332
|
+
## Current Cycle
|
|
333
|
+
|
|
334
|
+
${renderCurrentCycle(current)}
|
|
335
|
+
|
|
336
|
+
## Default Ignore Guidance
|
|
337
|
+
|
|
338
|
+
- Ignore build outputs, caches, and bulky generated artifacts by default.
|
|
339
|
+
- Keep ${STATE_ROOT_NAME}/logs/work-journal/ out of the default hot path.
|
|
340
|
+
- Do not ignore AGENTS.md, guide files, state files, or current summaries.
|
|
341
|
+
`;
|
|
342
|
+
}
|
|
343
|
+
function readCurrentIndex(root) {
|
|
344
|
+
const current = readJsonIfPresent(statePath(root, "index", "current.json"));
|
|
345
|
+
if (current === null) {
|
|
346
|
+
return null;
|
|
347
|
+
}
|
|
348
|
+
return normalizeCurrentIndex(current);
|
|
349
|
+
}
|
|
350
|
+
function writeCurrentIndex(root, current) {
|
|
351
|
+
return writeFileIfChanged(
|
|
352
|
+
statePath(root, "index", "current.json"),
|
|
353
|
+
JSON.stringify(current, null, 2) + "\n"
|
|
354
|
+
);
|
|
355
|
+
}
|
|
356
|
+
function writeCurrentState(root, current) {
|
|
357
|
+
return writeFileIfChanged(
|
|
358
|
+
statePath(root, "state.md"),
|
|
359
|
+
renderStateMarkdown(current)
|
|
360
|
+
);
|
|
361
|
+
}
|
|
362
|
+
function writeCurrentArtifacts(root, current, implementationMenuPath) {
|
|
363
|
+
return {
|
|
364
|
+
index: writeCurrentIndex(root, current),
|
|
365
|
+
state: writeCurrentState(root, current),
|
|
366
|
+
readThisFirst: writeFileIfChanged(
|
|
367
|
+
statePath(root, "guides", "read-this-first.md"),
|
|
368
|
+
renderReadThisFirstMarkdown(current, implementationMenuPath)
|
|
369
|
+
)
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// src/lib/markdown-sections.ts
|
|
374
|
+
function readSectionBody(content, heading) {
|
|
375
|
+
const escapedHeading = heading.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
376
|
+
const section = content.match(
|
|
377
|
+
new RegExp(`## ${escapedHeading}\\s+([\\s\\S]*?)(?=\\n## |$)`)
|
|
378
|
+
);
|
|
379
|
+
return section?.[1] ?? null;
|
|
380
|
+
}
|
|
381
|
+
function collectBulletItems(sectionBody) {
|
|
382
|
+
const items = [];
|
|
383
|
+
let currentItem = [];
|
|
384
|
+
const flushCurrentItem = () => {
|
|
385
|
+
if (currentItem.length === 0) {
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
388
|
+
items.push(currentItem.join("\n"));
|
|
389
|
+
currentItem = [];
|
|
390
|
+
};
|
|
391
|
+
const lines = sectionBody.trim().split("\n").map((line) => line.replace(/\s+$/u, ""));
|
|
392
|
+
for (const line of lines) {
|
|
393
|
+
const trimmed = line.trim();
|
|
394
|
+
if (trimmed.length === 0) {
|
|
395
|
+
continue;
|
|
396
|
+
}
|
|
397
|
+
if (trimmed.startsWith("- ")) {
|
|
398
|
+
flushCurrentItem();
|
|
399
|
+
currentItem = [trimmed];
|
|
400
|
+
continue;
|
|
401
|
+
}
|
|
402
|
+
if (currentItem.length > 0) {
|
|
403
|
+
currentItem.push(` ${trimmed}`);
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
flushCurrentItem();
|
|
407
|
+
return items;
|
|
408
|
+
}
|
|
409
|
+
function extractSectionLines(content, heading) {
|
|
410
|
+
const sectionBody = readSectionBody(content, heading);
|
|
411
|
+
if (sectionBody === null) {
|
|
412
|
+
return [];
|
|
413
|
+
}
|
|
414
|
+
return collectBulletItems(sectionBody);
|
|
415
|
+
}
|
|
416
|
+
function stripBulletPrefix(lines) {
|
|
417
|
+
return lines.map(
|
|
418
|
+
(line) => line.split("\n").map(
|
|
419
|
+
(segment, index) => index === 0 ? segment.replace(/^- /u, "") : segment.replace(/^\s+/u, "")
|
|
420
|
+
).join("\n")
|
|
421
|
+
);
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// src/lib/templates/context.ts
|
|
425
|
+
import { existsSync as existsSync3 } from "node:fs";
|
|
426
|
+
import path4 from "node:path";
|
|
427
|
+
function buildTemplateContext(root) {
|
|
428
|
+
const implementationMenu = path4.join(root, "docs/menu/implementation.md");
|
|
429
|
+
return {
|
|
430
|
+
root,
|
|
431
|
+
projectName: path4.basename(root),
|
|
432
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
433
|
+
implementationMenuPath: existsSync3(implementationMenu) ? "docs/menu/implementation.md" : null
|
|
434
|
+
};
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
// src/lib/templates/shared.ts
|
|
438
|
+
function alwaysReadList(context) {
|
|
439
|
+
const items = [
|
|
440
|
+
"- AGENTS.md",
|
|
441
|
+
`- ${STATE_ROOT_NAME}/project.md`,
|
|
442
|
+
`- ${STATE_ROOT_NAME}/state.md`,
|
|
443
|
+
`- ${STATE_ROOT_NAME}/guides/read-this-first.md`,
|
|
444
|
+
`- ${STATE_ROOT_NAME}/guides/code-consistency.md`
|
|
445
|
+
];
|
|
446
|
+
if (context.implementationMenuPath !== null) {
|
|
447
|
+
items.push(`- ${context.implementationMenuPath}`);
|
|
448
|
+
}
|
|
449
|
+
return items.join("\n");
|
|
450
|
+
}
|
|
451
|
+
function dirReadmeTemplate(title, purpose) {
|
|
452
|
+
return `# ${title}
|
|
453
|
+
|
|
454
|
+
${purpose}
|
|
455
|
+
`;
|
|
456
|
+
}
|
|
457
|
+
function workflowTemplate(name, purpose, outputs) {
|
|
458
|
+
return `# $${name}
|
|
459
|
+
|
|
460
|
+
## Purpose
|
|
461
|
+
|
|
462
|
+
${purpose}
|
|
463
|
+
|
|
464
|
+
## Required Outputs
|
|
465
|
+
|
|
466
|
+
${outputs}
|
|
467
|
+
|
|
468
|
+
## Completion Rule
|
|
469
|
+
|
|
470
|
+
- This stage is complete only when its required outputs are written clearly
|
|
471
|
+
enough for a fresh agent to inspect.
|
|
472
|
+
|
|
473
|
+
## Failure Conditions
|
|
474
|
+
|
|
475
|
+
- Required outputs are missing
|
|
476
|
+
- The stage overlaps ambiguously with another stage
|
|
477
|
+
- The next agent cannot tell what happened or what to do next
|
|
478
|
+
|
|
479
|
+
## Related State Files
|
|
480
|
+
|
|
481
|
+
- ${STATE_ROOT_NAME}/state.md
|
|
482
|
+
- ${STATE_ROOT_NAME}/plans/
|
|
483
|
+
- ${STATE_ROOT_NAME}/summaries/
|
|
484
|
+
- ${STATE_ROOT_NAME}/guides/
|
|
485
|
+
`;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
// src/lib/templates/repo-documents.ts
|
|
489
|
+
function agentsTemplate(context) {
|
|
490
|
+
return `# AGENTS.md
|
|
491
|
+
|
|
492
|
+
## Purpose
|
|
493
|
+
|
|
494
|
+
This repository uses \`sb-codex-tool\` as its workflow and runtime scaffold.
|
|
495
|
+
|
|
496
|
+
Use the toolkit state and guide files to keep work inspectable, compact, and
|
|
497
|
+
verification-friendly.
|
|
498
|
+
|
|
499
|
+
## Required Workflow
|
|
500
|
+
|
|
501
|
+
1. Non-trivial work starts with an approved plan.
|
|
502
|
+
2. Each task defines \`files\`, \`action\`, \`verify\`, and \`done\`.
|
|
503
|
+
3. Non-trivial work proceeds through \`execute -> refactor -> verify\`.
|
|
504
|
+
4. Final verification is always performed by a fresh agent.
|
|
505
|
+
5. Verification is a closure step, not test-only behavior.
|
|
506
|
+
|
|
507
|
+
## Agent Roles
|
|
508
|
+
|
|
509
|
+
- The main agent owns orchestration and user communication.
|
|
510
|
+
- Main-agent progress updates to the user are always in Korean.
|
|
511
|
+
- Subagents are bounded workers and must be reset or replaced after completion.
|
|
512
|
+
- Every new implementation agent reads \`${STATE_ROOT_NAME}/guides/code-consistency.md\` first.
|
|
513
|
+
- The main agent references \`${STATE_ROOT_NAME}/guides/code-consistency.md\` before assigning new work.
|
|
514
|
+
|
|
515
|
+
## Code Quality Rules
|
|
516
|
+
|
|
517
|
+
- Prefer reuse before adding parallel duplicate implementations.
|
|
518
|
+
- Keep files short and single-purpose.
|
|
519
|
+
- Keep functions short and single-purpose.
|
|
520
|
+
- Prefer simple top-down control flow.
|
|
521
|
+
- Avoid clever abstractions and speculative generalization.
|
|
522
|
+
- Keep naming predictable and module boundaries explicit.
|
|
523
|
+
- Write code that a fresh agent can read quickly.
|
|
524
|
+
|
|
525
|
+
## Verification Rules
|
|
526
|
+
|
|
527
|
+
- Final verification must be performed by a fresh agent.
|
|
528
|
+
- Verification compares plan vs actual and records deferred issues.
|
|
529
|
+
- Verification checks next-agent guidance and readability, not only tests.
|
|
530
|
+
- Keep work journal updates outside the default hot path.
|
|
531
|
+
|
|
532
|
+
## State and Guide Rules
|
|
533
|
+
|
|
534
|
+
- Hot path starts with \`${STATE_ROOT_NAME}/project.md\` and \`${STATE_ROOT_NAME}/state.md\`.
|
|
535
|
+
- Update next-agent guidance after non-trivial code changes.
|
|
536
|
+
- If new conventions are introduced, update \`${STATE_ROOT_NAME}/guides/code-consistency.md\`.
|
|
537
|
+
|
|
538
|
+
## Work Journal Rules
|
|
539
|
+
|
|
540
|
+
- After verified completion, update the human work journal under
|
|
541
|
+
\`${STATE_ROOT_NAME}/logs/work-journal/\`.
|
|
542
|
+
- The work journal is for people, not for agent continuity state.
|
|
543
|
+
|
|
544
|
+
## Git Rules
|
|
545
|
+
|
|
546
|
+
- Use git as context support only.
|
|
547
|
+
- Keep changed-file scope visible when available.
|
|
548
|
+
- Do not rely on destructive git automation.
|
|
549
|
+
|
|
550
|
+
## Repo Entry Points
|
|
551
|
+
|
|
552
|
+
${alwaysReadList(context)}
|
|
553
|
+
`;
|
|
554
|
+
}
|
|
555
|
+
function projectTemplate(context) {
|
|
556
|
+
return `# Project Brief
|
|
557
|
+
|
|
558
|
+
## Purpose
|
|
559
|
+
|
|
560
|
+
- Project name: ${context.projectName}
|
|
561
|
+
- Replace this section with the actual project purpose.
|
|
562
|
+
- Keep this file short and update it when the architecture truth changes.
|
|
563
|
+
|
|
564
|
+
## Core Constraints
|
|
565
|
+
|
|
566
|
+
- Follow the workflow defined in AGENTS.md.
|
|
567
|
+
- Keep state inspectable through ${STATE_ROOT_NAME}/.
|
|
568
|
+
- Optimize for reuse, readability, and low complexity.
|
|
569
|
+
|
|
570
|
+
## Important Entrypoints
|
|
571
|
+
|
|
572
|
+
- AGENTS.md
|
|
573
|
+
- ${STATE_ROOT_NAME}/state.md
|
|
574
|
+
- ${STATE_ROOT_NAME}/guides/read-this-first.md
|
|
575
|
+
- ${STATE_ROOT_NAME}/guides/code-consistency.md
|
|
576
|
+
|
|
577
|
+
## Always-Read Docs or Files
|
|
578
|
+
|
|
579
|
+
${alwaysReadList(context)}
|
|
580
|
+
|
|
581
|
+
## Architecture Truth
|
|
582
|
+
|
|
583
|
+
- Replace with a short summary of the current architecture.
|
|
584
|
+
- Keep module boundaries explicit.
|
|
585
|
+
- Prefer top-down, easy-to-modify flows.
|
|
586
|
+
`;
|
|
587
|
+
}
|
|
588
|
+
function codeConsistencyTemplate() {
|
|
589
|
+
return `# Code Consistency Guide
|
|
590
|
+
|
|
591
|
+
## Purpose
|
|
592
|
+
|
|
593
|
+
This file defines the structural consistency rules every new agent reads before
|
|
594
|
+
implementation.
|
|
595
|
+
|
|
596
|
+
## Architecture Style Summary
|
|
597
|
+
|
|
598
|
+
- Prefer small modules with single responsibilities.
|
|
599
|
+
- Keep orchestration code separate from filesystem, git, and template logic.
|
|
600
|
+
- Favor simple top-down flow over clever abstraction chains.
|
|
601
|
+
|
|
602
|
+
## Naming Rules
|
|
603
|
+
|
|
604
|
+
- Use direct names that reveal responsibility quickly.
|
|
605
|
+
- Keep command names aligned with workflow stages.
|
|
606
|
+
- Keep template and state file names predictable.
|
|
607
|
+
|
|
608
|
+
## Module Boundary Rules
|
|
609
|
+
|
|
610
|
+
- CLI parsing stays separate from command logic.
|
|
611
|
+
- Command logic stays separate from state persistence.
|
|
612
|
+
- Template generation stays separate from command output.
|
|
613
|
+
- Verification logic stays separate from execution logic.
|
|
614
|
+
|
|
615
|
+
## Reuse Rules
|
|
616
|
+
|
|
617
|
+
- Reuse existing helpers before creating parallel implementations.
|
|
618
|
+
- Extract shared logic only when reuse is current and obvious.
|
|
619
|
+
- Do not over-generalize for hypothetical future use.
|
|
620
|
+
|
|
621
|
+
## Readability Rules
|
|
622
|
+
|
|
623
|
+
- Keep files short and focused.
|
|
624
|
+
- Keep functions short and focused.
|
|
625
|
+
- Prefer explicit data flow over hidden side effects.
|
|
626
|
+
- Add short comments only when the control flow is not self-evident.
|
|
627
|
+
|
|
628
|
+
## Anti-Patterns
|
|
629
|
+
|
|
630
|
+
- Clever abstractions that hide simple behavior
|
|
631
|
+
- Overlong files and functions
|
|
632
|
+
- Mixed responsibilities in one module
|
|
633
|
+
- Vague naming
|
|
634
|
+
- Hidden state transitions
|
|
635
|
+
|
|
636
|
+
## Reference Files To Read First
|
|
637
|
+
|
|
638
|
+
- AGENTS.md
|
|
639
|
+
- ${STATE_ROOT_NAME}/project.md
|
|
640
|
+
- ${STATE_ROOT_NAME}/state.md
|
|
641
|
+
- ${STATE_ROOT_NAME}/guides/read-this-first.md
|
|
642
|
+
|
|
643
|
+
## Known Consistency Debt
|
|
644
|
+
|
|
645
|
+
- Fill this section with real debt as the project evolves.
|
|
646
|
+
`;
|
|
647
|
+
}
|
|
648
|
+
function buildRepoDocumentFiles(context) {
|
|
649
|
+
return [
|
|
650
|
+
{ path: "AGENTS.md", content: agentsTemplate(context) },
|
|
651
|
+
{ path: `${STATE_ROOT_NAME}/project.md`, content: projectTemplate(context) },
|
|
652
|
+
{
|
|
653
|
+
path: `${STATE_ROOT_NAME}/guides/code-consistency.md`,
|
|
654
|
+
content: codeConsistencyTemplate()
|
|
655
|
+
}
|
|
656
|
+
];
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
// src/lib/templates/state-documents.ts
|
|
660
|
+
function stateTemplate() {
|
|
661
|
+
return renderStateMarkdown(createInitialCurrentIndex());
|
|
662
|
+
}
|
|
663
|
+
function readThisFirstTemplate(context) {
|
|
664
|
+
return renderReadThisFirstMarkdown(
|
|
665
|
+
createInitialCurrentIndex(),
|
|
666
|
+
context.implementationMenuPath
|
|
667
|
+
);
|
|
668
|
+
}
|
|
669
|
+
function workJournalTemplate() {
|
|
670
|
+
return `# Work Journal
|
|
671
|
+
|
|
672
|
+
Daily human-readable logs live in this directory.
|
|
673
|
+
|
|
674
|
+
Recommended filename pattern:
|
|
675
|
+
|
|
676
|
+
- YYYY-MM-DD.md
|
|
677
|
+
|
|
678
|
+
Required sections for each entry:
|
|
679
|
+
|
|
680
|
+
- Date
|
|
681
|
+
- Summary
|
|
682
|
+
- Completed
|
|
683
|
+
- Changed Areas
|
|
684
|
+
- Verification
|
|
685
|
+
- Open Issues
|
|
686
|
+
- Next
|
|
687
|
+
`;
|
|
688
|
+
}
|
|
689
|
+
function indexReadmeTemplate() {
|
|
690
|
+
return `# Index
|
|
691
|
+
|
|
692
|
+
This directory stores compact navigational artifacts and structured current
|
|
693
|
+
state references used by status and verification helpers.
|
|
694
|
+
`;
|
|
695
|
+
}
|
|
696
|
+
function currentIndexTemplate() {
|
|
697
|
+
return JSON.stringify(createInitialCurrentIndex(), null, 2) + "\n";
|
|
698
|
+
}
|
|
699
|
+
function buildStateDocumentFiles(context) {
|
|
700
|
+
return [
|
|
701
|
+
{ path: `${STATE_ROOT_NAME}/state.md`, content: stateTemplate() },
|
|
702
|
+
{
|
|
703
|
+
path: `${STATE_ROOT_NAME}/guides/read-this-first.md`,
|
|
704
|
+
content: readThisFirstTemplate(context)
|
|
705
|
+
},
|
|
706
|
+
{
|
|
707
|
+
path: `${STATE_ROOT_NAME}/plans/README.md`,
|
|
708
|
+
content: dirReadmeTemplate("Plans", "Store draft and approved plans here.")
|
|
709
|
+
},
|
|
710
|
+
{
|
|
711
|
+
path: `${STATE_ROOT_NAME}/runs/README.md`,
|
|
712
|
+
content: dirReadmeTemplate("Runs", "Store launch and run metadata here.")
|
|
713
|
+
},
|
|
714
|
+
{
|
|
715
|
+
path: `${STATE_ROOT_NAME}/summaries/README.md`,
|
|
716
|
+
content: dirReadmeTemplate(
|
|
717
|
+
"Summaries",
|
|
718
|
+
"Store execution, verification, and reconciliation summaries here."
|
|
719
|
+
)
|
|
720
|
+
},
|
|
721
|
+
{
|
|
722
|
+
path: `${STATE_ROOT_NAME}/handoffs/README.md`,
|
|
723
|
+
content: dirReadmeTemplate(
|
|
724
|
+
"Handoffs",
|
|
725
|
+
"Store next-agent guidance and interrupted-work handoffs here."
|
|
726
|
+
)
|
|
727
|
+
},
|
|
728
|
+
{
|
|
729
|
+
path: `${STATE_ROOT_NAME}/reviews/README.md`,
|
|
730
|
+
content: dirReadmeTemplate(
|
|
731
|
+
"Reviews",
|
|
732
|
+
"Store consistency, verification, and acceptance review artifacts here."
|
|
733
|
+
)
|
|
734
|
+
},
|
|
735
|
+
{
|
|
736
|
+
path: `${STATE_ROOT_NAME}/logs/work-journal/README.md`,
|
|
737
|
+
content: workJournalTemplate()
|
|
738
|
+
},
|
|
739
|
+
{
|
|
740
|
+
path: `${STATE_ROOT_NAME}/index/README.md`,
|
|
741
|
+
content: indexReadmeTemplate()
|
|
742
|
+
},
|
|
743
|
+
{
|
|
744
|
+
path: `${STATE_ROOT_NAME}/index/current.json`,
|
|
745
|
+
content: currentIndexTemplate()
|
|
746
|
+
}
|
|
747
|
+
];
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
// src/lib/templates/documents.ts
|
|
751
|
+
function buildDocumentFiles(context) {
|
|
752
|
+
return [
|
|
753
|
+
...buildRepoDocumentFiles(context),
|
|
754
|
+
...buildStateDocumentFiles(context)
|
|
755
|
+
];
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
// src/lib/templates/ignore.ts
|
|
759
|
+
var SHARED_IGNORE_LINES = [
|
|
760
|
+
"node_modules/",
|
|
761
|
+
"dist/",
|
|
762
|
+
"build/",
|
|
763
|
+
"coverage/",
|
|
764
|
+
".DS_Store"
|
|
765
|
+
];
|
|
766
|
+
function buildBaseIgnoreTemplate() {
|
|
767
|
+
return `# sb-codex-tool default agent-ignore patterns
|
|
768
|
+
${SHARED_IGNORE_LINES.join("\n")}
|
|
769
|
+
${STATE_ROOT_NAME}/logs/work-journal/
|
|
770
|
+
`;
|
|
771
|
+
}
|
|
772
|
+
function getGitIgnoreLines() {
|
|
773
|
+
return [...SHARED_IGNORE_LINES];
|
|
774
|
+
}
|
|
775
|
+
function getSearchIgnoreLines() {
|
|
776
|
+
return [
|
|
777
|
+
...SHARED_IGNORE_LINES,
|
|
778
|
+
`${STATE_ROOT_NAME}/logs/work-journal/`
|
|
779
|
+
];
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
// src/lib/templates/workflows.ts
|
|
783
|
+
function buildWorkflowFile(name, purpose, outputs) {
|
|
784
|
+
return {
|
|
785
|
+
path: `${STATE_ROOT_NAME}/workflows/${name}.md`,
|
|
786
|
+
content: workflowTemplate(name, purpose, outputs.join("\n"))
|
|
787
|
+
};
|
|
788
|
+
}
|
|
789
|
+
function buildWorkflowFiles() {
|
|
790
|
+
return [
|
|
791
|
+
buildWorkflowFile(
|
|
792
|
+
"clarify",
|
|
793
|
+
"Turn an ambiguous request into a compact brief with clear acceptance criteria and boundaries.",
|
|
794
|
+
[
|
|
795
|
+
"- compact brief",
|
|
796
|
+
"- acceptance criteria",
|
|
797
|
+
"- boundaries",
|
|
798
|
+
"- assumptions",
|
|
799
|
+
"- non-goals"
|
|
800
|
+
]
|
|
801
|
+
),
|
|
802
|
+
buildWorkflowFile(
|
|
803
|
+
"plan",
|
|
804
|
+
"Create a decision-complete plan with executable tasks and explicit verification criteria.",
|
|
805
|
+
[
|
|
806
|
+
"- objective",
|
|
807
|
+
"- acceptance criteria",
|
|
808
|
+
"- boundaries",
|
|
809
|
+
"- task list with files/action/verify/done"
|
|
810
|
+
]
|
|
811
|
+
),
|
|
812
|
+
buildWorkflowFile(
|
|
813
|
+
"execute",
|
|
814
|
+
"Advance work task by task while keeping scope, blockers, and status explicit.",
|
|
815
|
+
[
|
|
816
|
+
"- task progression",
|
|
817
|
+
"- status updates",
|
|
818
|
+
"- blocker notes",
|
|
819
|
+
"- changed-file scope"
|
|
820
|
+
]
|
|
821
|
+
),
|
|
822
|
+
buildWorkflowFile(
|
|
823
|
+
"refactor",
|
|
824
|
+
"Reduce complexity and improve reuse, readability, and maintainability before closure.",
|
|
825
|
+
[
|
|
826
|
+
"- simplification notes",
|
|
827
|
+
"- reuse decisions",
|
|
828
|
+
"- readability improvements",
|
|
829
|
+
"- complexity reductions"
|
|
830
|
+
]
|
|
831
|
+
),
|
|
832
|
+
buildWorkflowFile(
|
|
833
|
+
"verify",
|
|
834
|
+
"Perform fresh-agent closure after refactor with evidence, reconciliation, and next-agent checks.",
|
|
835
|
+
[
|
|
836
|
+
"- fresh-agent verification result",
|
|
837
|
+
"- plan-vs-actual reconciliation",
|
|
838
|
+
"- deferred issues",
|
|
839
|
+
"- next-agent guidance check",
|
|
840
|
+
"- work journal precondition"
|
|
841
|
+
]
|
|
842
|
+
)
|
|
843
|
+
];
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
// src/lib/templates/generated-files.ts
|
|
847
|
+
function buildGeneratedFiles(context) {
|
|
848
|
+
return [
|
|
849
|
+
...buildDocumentFiles(context),
|
|
850
|
+
...buildWorkflowFiles(),
|
|
851
|
+
{
|
|
852
|
+
path: `${STATE_ROOT_NAME}/ignore/base.ignore`,
|
|
853
|
+
content: buildBaseIgnoreTemplate()
|
|
854
|
+
}
|
|
855
|
+
];
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
// src/lib/assignment.ts
|
|
859
|
+
function readAllowedFileScope(root, guidePath) {
|
|
860
|
+
if (guidePath === null) {
|
|
861
|
+
return [];
|
|
862
|
+
}
|
|
863
|
+
const text = readTextIfPresent(path5.join(root, guidePath));
|
|
864
|
+
if (text === null) {
|
|
865
|
+
return [];
|
|
866
|
+
}
|
|
867
|
+
return extractSectionLines(text, "Allowed File Scope");
|
|
868
|
+
}
|
|
869
|
+
function buildAssignmentGuide(agentName, title, current, assignmentSlug, allowedFileScope) {
|
|
870
|
+
const fileScope = allowedFileScope.length > 0 ? allowedFileScope.join("\n") : "- Replace with the bounded file scope for this assignment.";
|
|
871
|
+
return `# Assignment Guide: ${title}
|
|
872
|
+
|
|
873
|
+
## Assigned Agent
|
|
874
|
+
|
|
875
|
+
- ${agentName}
|
|
876
|
+
|
|
877
|
+
## Assignment Slug
|
|
878
|
+
|
|
879
|
+
- ${assignmentSlug}
|
|
880
|
+
|
|
881
|
+
## Objective
|
|
882
|
+
|
|
883
|
+
- Replace with the bounded objective for ${agentName}.
|
|
884
|
+
|
|
885
|
+
## Current Cycle References
|
|
886
|
+
|
|
887
|
+
- Current plan: ${current.latestApprovedPlan ?? "none yet"}
|
|
888
|
+
- Latest summary: ${current.latestRelevantSummary ?? "none yet"}
|
|
889
|
+
- Current guide: ${current.currentGuide ?? "none yet"}
|
|
890
|
+
- Latest lifecycle run: ${current.latestRun ?? "none yet"}
|
|
891
|
+
|
|
892
|
+
## Allowed File Scope
|
|
893
|
+
|
|
894
|
+
${fileScope}
|
|
895
|
+
|
|
896
|
+
## Required References
|
|
897
|
+
|
|
898
|
+
- AGENTS.md
|
|
899
|
+
- ${STATE_ROOT_NAME}/guides/code-consistency.md
|
|
900
|
+
- ${current.latestApprovedPlan ?? `${STATE_ROOT_NAME}/plans/README.md`}
|
|
901
|
+
- ${current.latestRelevantSummary ?? `${STATE_ROOT_NAME}/summaries/README.md`}
|
|
902
|
+
- ${current.currentGuide ?? `${STATE_ROOT_NAME}/guides/read-this-first.md`}
|
|
903
|
+
|
|
904
|
+
## Consistency Expectations
|
|
905
|
+
|
|
906
|
+
- Read ${STATE_ROOT_NAME}/guides/code-consistency.md before implementation.
|
|
907
|
+
- Reuse existing helpers before adding parallel implementations.
|
|
908
|
+
- Keep files and functions short, simple, and readable.
|
|
909
|
+
- Keep module boundaries explicit and avoid clever abstractions.
|
|
910
|
+
|
|
911
|
+
## Verification Expectations
|
|
912
|
+
|
|
913
|
+
- Keep the work bounded to the allowed file scope unless the main agent expands it.
|
|
914
|
+
- Report any blocker or scope mismatch back to the main agent immediately.
|
|
915
|
+
- Do not self-approve final completion; final verification stays fresh-agent-only.
|
|
916
|
+
|
|
917
|
+
## Completion Rule
|
|
918
|
+
|
|
919
|
+
- When the bounded task is complete, the main agent must either close and replace this subagent or clear its context before same-role reuse.
|
|
920
|
+
`;
|
|
921
|
+
}
|
|
922
|
+
function createAssignmentGuide(start, agentName, requestedSlug, requestedTitle) {
|
|
923
|
+
const root = resolveProjectRoot(start);
|
|
924
|
+
const current = normalizeCurrentIndex(readCurrentIndex(root));
|
|
925
|
+
if (current.latestApprovedPlan === null) {
|
|
926
|
+
throw new Error("assign requires a current approved plan.");
|
|
927
|
+
}
|
|
928
|
+
if (current.latestRelevantSummary === null) {
|
|
929
|
+
throw new Error("assign requires a current summary.");
|
|
930
|
+
}
|
|
931
|
+
if (current.currentGuide === null) {
|
|
932
|
+
throw new Error("assign requires a current task guide.");
|
|
933
|
+
}
|
|
934
|
+
const assignmentSlug = normalizeSlug(requestedSlug);
|
|
935
|
+
if (assignmentSlug.length === 0) {
|
|
936
|
+
throw new Error("Assignment slug must contain at least one alphanumeric character.");
|
|
937
|
+
}
|
|
938
|
+
const agentSlug = normalizeSlug(agentName);
|
|
939
|
+
if (agentSlug.length === 0) {
|
|
940
|
+
throw new Error("Agent name must contain at least one alphanumeric character.");
|
|
941
|
+
}
|
|
942
|
+
const title = requestedTitle?.trim().length ? requestedTitle.trim() : humanizeSlug(assignmentSlug);
|
|
943
|
+
const { dateStamp } = parseCycleDescriptor(current.latestApprovedPlan);
|
|
944
|
+
const assignmentPath = `${STATE_ROOT_NAME}/guides/${dateStamp}-${agentSlug}-${assignmentSlug}-assignment.md`;
|
|
945
|
+
const allowedFileScope = readAllowedFileScope(root, current.currentGuide);
|
|
946
|
+
const writeResult = writeFileIfMissing(
|
|
947
|
+
path5.join(root, assignmentPath),
|
|
948
|
+
buildAssignmentGuide(agentName, title, current, assignmentSlug, allowedFileScope)
|
|
949
|
+
);
|
|
950
|
+
const createdFiles = [];
|
|
951
|
+
const keptFiles = [];
|
|
952
|
+
if (writeResult === "created") {
|
|
953
|
+
createdFiles.push(assignmentPath);
|
|
954
|
+
} else {
|
|
955
|
+
keptFiles.push(assignmentPath);
|
|
956
|
+
}
|
|
957
|
+
const nextSubagents = current.activeAgents.subagents.includes(agentName) ? current.activeAgents.subagents : [...current.activeAgents.subagents, agentName];
|
|
958
|
+
const nextFocusModules = current.currentFocusModules.includes(assignmentPath) ? current.currentFocusModules : [...current.currentFocusModules, assignmentPath];
|
|
959
|
+
const nextCurrent = normalizeCurrentIndex({
|
|
960
|
+
...current,
|
|
961
|
+
currentFocusModules: nextFocusModules,
|
|
962
|
+
activeAgents: {
|
|
963
|
+
...current.activeAgents,
|
|
964
|
+
subagents: nextSubagents
|
|
965
|
+
},
|
|
966
|
+
assignmentGuides: {
|
|
967
|
+
...current.assignmentGuides,
|
|
968
|
+
[agentName]: assignmentPath
|
|
969
|
+
},
|
|
970
|
+
notes: [
|
|
971
|
+
`Current work cycle: ${humanizeSlug(parseCycleDescriptor(current.latestApprovedPlan).slug)}.`,
|
|
972
|
+
`Prepared assignment guide for ${agentName}: ${assignmentPath}.`,
|
|
973
|
+
`Previous relevant summary: ${current.latestRelevantSummary}`
|
|
974
|
+
]
|
|
975
|
+
});
|
|
976
|
+
const updatedFiles = [];
|
|
977
|
+
const currentWrites = writeCurrentArtifacts(
|
|
978
|
+
root,
|
|
979
|
+
nextCurrent,
|
|
980
|
+
buildTemplateContext(root).implementationMenuPath
|
|
981
|
+
);
|
|
982
|
+
if (currentWrites.index !== "unchanged") {
|
|
983
|
+
updatedFiles.push(`${STATE_ROOT_NAME}/index/current.json`);
|
|
984
|
+
}
|
|
985
|
+
if (currentWrites.state !== "unchanged") {
|
|
986
|
+
updatedFiles.push(`${STATE_ROOT_NAME}/state.md`);
|
|
987
|
+
}
|
|
988
|
+
if (currentWrites.readThisFirst !== "unchanged") {
|
|
989
|
+
updatedFiles.push(`${STATE_ROOT_NAME}/guides/read-this-first.md`);
|
|
990
|
+
}
|
|
991
|
+
return {
|
|
992
|
+
root,
|
|
993
|
+
agentName,
|
|
994
|
+
title,
|
|
995
|
+
assignmentPath,
|
|
996
|
+
createdFiles,
|
|
997
|
+
keptFiles,
|
|
998
|
+
updatedFiles
|
|
999
|
+
};
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
// src/commands/assign.ts
|
|
1003
|
+
function runAssign(args) {
|
|
1004
|
+
const [agentName, slug, ...titleParts] = args;
|
|
1005
|
+
if (agentName === void 0 || slug === void 0) {
|
|
1006
|
+
console.error("Usage: sb-codex-tool assign <agent-name> <slug> [title words]");
|
|
1007
|
+
return 1;
|
|
1008
|
+
}
|
|
1009
|
+
const title = titleParts.length > 0 ? titleParts.join(" ") : void 0;
|
|
1010
|
+
const result = createAssignmentGuide(process.cwd(), agentName, slug, title);
|
|
1011
|
+
console.log(`Project root: ${result.root}`);
|
|
1012
|
+
console.log(`Assigned agent: ${result.agentName}`);
|
|
1013
|
+
console.log(`Assignment: ${result.title}`);
|
|
1014
|
+
console.log("");
|
|
1015
|
+
console.log("Artifacts:");
|
|
1016
|
+
console.log(`- assignment guide: ${result.assignmentPath}`);
|
|
1017
|
+
console.log("");
|
|
1018
|
+
console.log(`Created files: ${result.createdFiles.length}`);
|
|
1019
|
+
for (const file of result.createdFiles) {
|
|
1020
|
+
console.log(`- ${file}`);
|
|
1021
|
+
}
|
|
1022
|
+
if (result.updatedFiles.length > 0) {
|
|
1023
|
+
console.log("");
|
|
1024
|
+
console.log("Updated state:");
|
|
1025
|
+
for (const file of result.updatedFiles) {
|
|
1026
|
+
console.log(`- ${file}`);
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
return 0;
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
// src/lib/work-cycle.ts
|
|
1033
|
+
import path7 from "node:path";
|
|
1034
|
+
|
|
1035
|
+
// src/lib/git.ts
|
|
1036
|
+
import { spawnSync } from "node:child_process";
|
|
1037
|
+
function runGit(root, args) {
|
|
1038
|
+
const result = spawnSync("git", args, {
|
|
1039
|
+
cwd: root,
|
|
1040
|
+
encoding: "utf8",
|
|
1041
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
1042
|
+
});
|
|
1043
|
+
if (result.status !== 0) {
|
|
1044
|
+
return null;
|
|
1045
|
+
}
|
|
1046
|
+
return result.stdout.trim();
|
|
1047
|
+
}
|
|
1048
|
+
function getGitContext(root) {
|
|
1049
|
+
const insideWorkTree = runGit(root, ["rev-parse", "--is-inside-work-tree"]);
|
|
1050
|
+
if (insideWorkTree !== "true") {
|
|
1051
|
+
return {
|
|
1052
|
+
available: false,
|
|
1053
|
+
branch: null,
|
|
1054
|
+
dirty: false,
|
|
1055
|
+
changedFiles: []
|
|
1056
|
+
};
|
|
1057
|
+
}
|
|
1058
|
+
const branch = runGit(root, ["branch", "--show-current"]);
|
|
1059
|
+
const status = runGit(root, ["status", "--porcelain"]) ?? "";
|
|
1060
|
+
const changedFiles = status.split("\n").map((line) => line.trim()).filter((line) => line.length > 0).map((line) => line.slice(3));
|
|
1061
|
+
return {
|
|
1062
|
+
available: true,
|
|
1063
|
+
branch,
|
|
1064
|
+
dirty: changedFiles.length > 0,
|
|
1065
|
+
changedFiles
|
|
1066
|
+
};
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
// src/lib/run-records.ts
|
|
1070
|
+
import path6 from "node:path";
|
|
1071
|
+
function readLifecycleRunRecord(root, relativePath) {
|
|
1072
|
+
if (relativePath === null) {
|
|
1073
|
+
return null;
|
|
1074
|
+
}
|
|
1075
|
+
return readJsonIfPresent(path6.join(root, relativePath));
|
|
1076
|
+
}
|
|
1077
|
+
function lifecycleRunPath(dateStamp, slug) {
|
|
1078
|
+
return `${STATE_ROOT_NAME}/runs/${dateStamp}-${slug}-run.json`;
|
|
1079
|
+
}
|
|
1080
|
+
function renderGitContextSection(runPath, git) {
|
|
1081
|
+
const lines = [
|
|
1082
|
+
"## Git Context",
|
|
1083
|
+
"",
|
|
1084
|
+
`- Run artifact: ${runPath}`,
|
|
1085
|
+
`- Git available: ${git.available ? "yes" : "no"}`,
|
|
1086
|
+
`- Branch: ${git.available ? git.branch ?? "detached" : "unavailable"}`,
|
|
1087
|
+
`- Dirty: ${git.available ? git.dirty ? "yes" : "no" : "unavailable"}`
|
|
1088
|
+
];
|
|
1089
|
+
if (!git.available) {
|
|
1090
|
+
lines.push("- Changed files: unavailable outside a Git repository");
|
|
1091
|
+
return `${lines.join("\n")}
|
|
1092
|
+
`;
|
|
1093
|
+
}
|
|
1094
|
+
if (git.changedFiles.length === 0) {
|
|
1095
|
+
lines.push("- Changed files: none");
|
|
1096
|
+
return `${lines.join("\n")}
|
|
1097
|
+
`;
|
|
1098
|
+
}
|
|
1099
|
+
lines.push("- Changed files:");
|
|
1100
|
+
for (const file of git.changedFiles) {
|
|
1101
|
+
lines.push(` - ${file}`);
|
|
1102
|
+
}
|
|
1103
|
+
return `${lines.join("\n")}
|
|
1104
|
+
`;
|
|
1105
|
+
}
|
|
1106
|
+
function writeLifecycleRunRecord(root, options) {
|
|
1107
|
+
const now = (options.now ?? /* @__PURE__ */ new Date()).toISOString();
|
|
1108
|
+
const relativePath = lifecycleRunPath(options.dateStamp, options.slug);
|
|
1109
|
+
const absolutePath = path6.join(root, relativePath);
|
|
1110
|
+
const previous = readJsonIfPresent(absolutePath);
|
|
1111
|
+
const record = {
|
|
1112
|
+
version: 1,
|
|
1113
|
+
cycleId: `${options.dateStamp}-${options.slug}`,
|
|
1114
|
+
dateStamp: options.dateStamp,
|
|
1115
|
+
slug: options.slug,
|
|
1116
|
+
title: options.title,
|
|
1117
|
+
phase: options.phase,
|
|
1118
|
+
stage: options.stage,
|
|
1119
|
+
verdict: options.verdict,
|
|
1120
|
+
git: options.git,
|
|
1121
|
+
paths: options.paths,
|
|
1122
|
+
startedAt: previous?.startedAt ?? now,
|
|
1123
|
+
updatedAt: now,
|
|
1124
|
+
closedAt: options.verdict === "pass" || options.verdict === "pass_with_concerns" ? now : previous?.closedAt ?? null
|
|
1125
|
+
};
|
|
1126
|
+
return {
|
|
1127
|
+
path: relativePath,
|
|
1128
|
+
writeResult: writeFileIfChanged(
|
|
1129
|
+
absolutePath,
|
|
1130
|
+
JSON.stringify(record, null, 2) + "\n"
|
|
1131
|
+
),
|
|
1132
|
+
record
|
|
1133
|
+
};
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
// src/lib/work-cycle.ts
|
|
1137
|
+
function planTemplate(title) {
|
|
1138
|
+
return `# Approved Plan: ${title}
|
|
1139
|
+
|
|
1140
|
+
## Objective
|
|
1141
|
+
|
|
1142
|
+
- Replace with the concrete objective for this work cycle.
|
|
1143
|
+
|
|
1144
|
+
## Acceptance Criteria
|
|
1145
|
+
|
|
1146
|
+
- Replace with the acceptance criteria that define completion.
|
|
1147
|
+
|
|
1148
|
+
## Boundaries
|
|
1149
|
+
|
|
1150
|
+
- Replace with in-scope and out-of-scope notes.
|
|
1151
|
+
|
|
1152
|
+
## Tasks
|
|
1153
|
+
|
|
1154
|
+
### Task 1
|
|
1155
|
+
|
|
1156
|
+
- files: \`fill-in-file-scope\`
|
|
1157
|
+
- action: describe the concrete implementation task
|
|
1158
|
+
- verify: describe how this task will be checked
|
|
1159
|
+
- done: no
|
|
1160
|
+
`;
|
|
1161
|
+
}
|
|
1162
|
+
function executionSummaryTemplate(title, guidePath, planPath) {
|
|
1163
|
+
return `# Execution Summary: ${title}
|
|
1164
|
+
|
|
1165
|
+
## Purpose
|
|
1166
|
+
|
|
1167
|
+
- Capture implementation progress for the current work cycle before fresh verification.
|
|
1168
|
+
|
|
1169
|
+
## Scope
|
|
1170
|
+
|
|
1171
|
+
- Replace with the actual implementation scope.
|
|
1172
|
+
|
|
1173
|
+
## Implemented Surface
|
|
1174
|
+
|
|
1175
|
+
- not started yet
|
|
1176
|
+
|
|
1177
|
+
## Checks Run
|
|
1178
|
+
|
|
1179
|
+
- none yet
|
|
1180
|
+
|
|
1181
|
+
## Plan vs Actual
|
|
1182
|
+
|
|
1183
|
+
- Update this section as implementation progresses.
|
|
1184
|
+
|
|
1185
|
+
## Refactor Notes
|
|
1186
|
+
|
|
1187
|
+
- Update this section after refactor.
|
|
1188
|
+
|
|
1189
|
+
## Deferred Issues
|
|
1190
|
+
|
|
1191
|
+
- Add deferred issues if they exist.
|
|
1192
|
+
|
|
1193
|
+
## Next-Agent Guidance
|
|
1194
|
+
|
|
1195
|
+
- Start from this execution summary as the latest relevant summary.
|
|
1196
|
+
- Then review \`${planPath}\`.
|
|
1197
|
+
- Then review \`${guidePath}\`.
|
|
1198
|
+
`;
|
|
1199
|
+
}
|
|
1200
|
+
function handoffTemplate(title, guidePath, planPath, summaryPath, gitContextSection) {
|
|
1201
|
+
return `# Handoff: ${title}
|
|
1202
|
+
|
|
1203
|
+
## Goal
|
|
1204
|
+
|
|
1205
|
+
- Enable the next fresh agent to continue this work cycle without hidden context.
|
|
1206
|
+
|
|
1207
|
+
## Read In This Order
|
|
1208
|
+
|
|
1209
|
+
1. AGENTS.md
|
|
1210
|
+
2. ${STATE_ROOT_NAME}/project.md
|
|
1211
|
+
3. ${STATE_ROOT_NAME}/state.md
|
|
1212
|
+
4. ${STATE_ROOT_NAME}/guides/read-this-first.md
|
|
1213
|
+
5. ${STATE_ROOT_NAME}/guides/code-consistency.md
|
|
1214
|
+
6. ${summaryPath}
|
|
1215
|
+
7. ${planPath}
|
|
1216
|
+
8. ${guidePath}
|
|
1217
|
+
|
|
1218
|
+
## Current Status
|
|
1219
|
+
|
|
1220
|
+
- Replace with the current implementation status.
|
|
1221
|
+
|
|
1222
|
+
${gitContextSection}
|
|
1223
|
+
|
|
1224
|
+
## Expected Verification Checks
|
|
1225
|
+
|
|
1226
|
+
- Replace with the checks relevant to this work cycle.
|
|
1227
|
+
|
|
1228
|
+
## Open Risks
|
|
1229
|
+
|
|
1230
|
+
- Replace with the current blocker or risk list.
|
|
1231
|
+
`;
|
|
1232
|
+
}
|
|
1233
|
+
function reviewTemplate(title) {
|
|
1234
|
+
return `# Fresh Verification Review: ${title}
|
|
1235
|
+
|
|
1236
|
+
## Verification Target
|
|
1237
|
+
|
|
1238
|
+
- ${title}
|
|
1239
|
+
|
|
1240
|
+
## Verdict
|
|
1241
|
+
|
|
1242
|
+
- pending
|
|
1243
|
+
|
|
1244
|
+
## Required Checks
|
|
1245
|
+
|
|
1246
|
+
- Contract reading order completed
|
|
1247
|
+
- Acceptance criteria reviewed
|
|
1248
|
+
- State and guide artifacts inspected
|
|
1249
|
+
- Relevant code and tests inspected
|
|
1250
|
+
- Required checks run
|
|
1251
|
+
|
|
1252
|
+
## Checks Run
|
|
1253
|
+
|
|
1254
|
+
- Add the checks run during the fresh verification pass.
|
|
1255
|
+
|
|
1256
|
+
## Findings
|
|
1257
|
+
|
|
1258
|
+
- Add findings here in severity order.
|
|
1259
|
+
|
|
1260
|
+
## Concerns
|
|
1261
|
+
|
|
1262
|
+
- Add non-blocking concerns here.
|
|
1263
|
+
|
|
1264
|
+
## Missing Evidence
|
|
1265
|
+
|
|
1266
|
+
- Add any blocked evidence here.
|
|
1267
|
+
|
|
1268
|
+
## Work Journal Decision
|
|
1269
|
+
|
|
1270
|
+
- Record whether verified closure is complete enough to update the work journal.
|
|
1271
|
+
`;
|
|
1272
|
+
}
|
|
1273
|
+
function scopeGuideTemplate(title, planPath, previousSummary) {
|
|
1274
|
+
return `# Scope Guide: ${title}
|
|
1275
|
+
|
|
1276
|
+
## Purpose
|
|
1277
|
+
|
|
1278
|
+
- Narrow the current work cycle to a bounded file scope and clear verification expectations.
|
|
1279
|
+
|
|
1280
|
+
## Working Goal
|
|
1281
|
+
|
|
1282
|
+
- Replace with the concrete goal for this cycle.
|
|
1283
|
+
|
|
1284
|
+
## Primary Plan
|
|
1285
|
+
|
|
1286
|
+
- ${planPath}
|
|
1287
|
+
|
|
1288
|
+
## Allowed File Scope
|
|
1289
|
+
|
|
1290
|
+
- Replace with the files or modules that should be touched.
|
|
1291
|
+
|
|
1292
|
+
## Recommended References
|
|
1293
|
+
|
|
1294
|
+
- AGENTS.md
|
|
1295
|
+
- ${STATE_ROOT_NAME}/project.md
|
|
1296
|
+
- ${STATE_ROOT_NAME}/state.md
|
|
1297
|
+
- ${STATE_ROOT_NAME}/guides/code-consistency.md
|
|
1298
|
+
${previousSummary === null ? "" : `- ${previousSummary}
|
|
1299
|
+
`}
|
|
1300
|
+
## Verification Expectations
|
|
1301
|
+
|
|
1302
|
+
- Replace with the checks and verdict expectations for this cycle.
|
|
1303
|
+
`;
|
|
1304
|
+
}
|
|
1305
|
+
function beginWorkCycle(start, requestedSlug, requestedTitle) {
|
|
1306
|
+
const root = resolveProjectRoot(start);
|
|
1307
|
+
const slug = normalizeSlug(requestedSlug);
|
|
1308
|
+
if (slug.length === 0) {
|
|
1309
|
+
throw new Error("Work-cycle slug must contain at least one alphanumeric character.");
|
|
1310
|
+
}
|
|
1311
|
+
const title = requestedTitle?.trim().length ? requestedTitle.trim() : humanizeSlug(slug);
|
|
1312
|
+
const dateStamp = formatDateStamp();
|
|
1313
|
+
const current = normalizeCurrentIndex(readCurrentIndex(root));
|
|
1314
|
+
const previousSummary = current.latestRelevantSummary;
|
|
1315
|
+
const planPath = `${STATE_ROOT_NAME}/plans/${dateStamp}-${slug}-approved.md`;
|
|
1316
|
+
const summaryPath = `${STATE_ROOT_NAME}/summaries/${dateStamp}-${slug}-execution-summary.md`;
|
|
1317
|
+
const handoffPath = `${STATE_ROOT_NAME}/handoffs/${dateStamp}-${slug}-to-verification.md`;
|
|
1318
|
+
const reviewPath = `${STATE_ROOT_NAME}/reviews/${dateStamp}-${slug}-fresh-verification.md`;
|
|
1319
|
+
const guidePath = `${STATE_ROOT_NAME}/guides/${dateStamp}-${slug}-scope.md`;
|
|
1320
|
+
const runPath = lifecycleRunPath(dateStamp, slug);
|
|
1321
|
+
const sameCycle = current.latestApprovedPlan === planPath;
|
|
1322
|
+
const generatedFiles = [
|
|
1323
|
+
{ path: planPath, content: planTemplate(title) },
|
|
1324
|
+
{ path: summaryPath, content: executionSummaryTemplate(title, guidePath, planPath) },
|
|
1325
|
+
{ path: reviewPath, content: reviewTemplate(title) },
|
|
1326
|
+
{ path: guidePath, content: scopeGuideTemplate(title, planPath, previousSummary) }
|
|
1327
|
+
];
|
|
1328
|
+
const createdFiles = [];
|
|
1329
|
+
const keptFiles = [];
|
|
1330
|
+
for (const file of generatedFiles) {
|
|
1331
|
+
const result = writeFileIfMissing(path7.join(root, file.path), file.content);
|
|
1332
|
+
if (result === "created") {
|
|
1333
|
+
createdFiles.push(file.path);
|
|
1334
|
+
} else {
|
|
1335
|
+
keptFiles.push(file.path);
|
|
1336
|
+
}
|
|
1337
|
+
}
|
|
1338
|
+
const latestSummary = sameCycle ? current.latestRelevantSummary ?? summaryPath : summaryPath;
|
|
1339
|
+
const git = getGitContext(root);
|
|
1340
|
+
const handoffContent = handoffTemplate(
|
|
1341
|
+
title,
|
|
1342
|
+
guidePath,
|
|
1343
|
+
planPath,
|
|
1344
|
+
summaryPath,
|
|
1345
|
+
renderGitContextSection(runPath, git)
|
|
1346
|
+
);
|
|
1347
|
+
const handoffWrite = writeFileIfMissing(path7.join(root, handoffPath), handoffContent);
|
|
1348
|
+
if (handoffWrite === "created") {
|
|
1349
|
+
createdFiles.push(handoffPath);
|
|
1350
|
+
} else {
|
|
1351
|
+
keptFiles.push(handoffPath);
|
|
1352
|
+
}
|
|
1353
|
+
const currentGuide = sameCycle ? current.currentGuide ?? guidePath : guidePath;
|
|
1354
|
+
const currentHandoff = sameCycle ? current.currentHandoff ?? handoffPath : handoffPath;
|
|
1355
|
+
const currentReview = sameCycle ? current.currentReview ?? reviewPath : reviewPath;
|
|
1356
|
+
const latestRun = sameCycle ? current.latestRun ?? runPath : runPath;
|
|
1357
|
+
const updatedFiles = [];
|
|
1358
|
+
const runWrite = writeLifecycleRunRecord(root, {
|
|
1359
|
+
dateStamp,
|
|
1360
|
+
slug,
|
|
1361
|
+
title,
|
|
1362
|
+
phase: "begin",
|
|
1363
|
+
stage: sameCycle ? current.currentStage : "clarify",
|
|
1364
|
+
verdict: "pending",
|
|
1365
|
+
git,
|
|
1366
|
+
paths: {
|
|
1367
|
+
planPath,
|
|
1368
|
+
executionSummaryPath: summaryPath,
|
|
1369
|
+
handoffPath: currentHandoff,
|
|
1370
|
+
reviewPath: currentReview,
|
|
1371
|
+
guidePath: currentGuide,
|
|
1372
|
+
verificationSummaryPath: null,
|
|
1373
|
+
journalPath: null
|
|
1374
|
+
}
|
|
1375
|
+
});
|
|
1376
|
+
if (runWrite.writeResult === "created") {
|
|
1377
|
+
createdFiles.push(runWrite.path);
|
|
1378
|
+
} else if (runWrite.writeResult === "updated") {
|
|
1379
|
+
updatedFiles.push(runWrite.path);
|
|
1380
|
+
}
|
|
1381
|
+
const nextCurrent = normalizeCurrentIndex({
|
|
1382
|
+
...current,
|
|
1383
|
+
currentStage: sameCycle ? current.currentStage : "clarify",
|
|
1384
|
+
nextAction: sameCycle ? current.nextAction : `Fill in ${planPath} and narrow ${guidePath} before implementation starts.`,
|
|
1385
|
+
latestApprovedPlan: planPath,
|
|
1386
|
+
latestRelevantSummary: latestSummary,
|
|
1387
|
+
latestRun,
|
|
1388
|
+
currentGuide,
|
|
1389
|
+
currentHandoff,
|
|
1390
|
+
currentReview,
|
|
1391
|
+
currentFocusModules: [
|
|
1392
|
+
"AGENTS.md",
|
|
1393
|
+
`${STATE_ROOT_NAME}/guides/code-consistency.md`,
|
|
1394
|
+
latestRun,
|
|
1395
|
+
latestSummary,
|
|
1396
|
+
planPath,
|
|
1397
|
+
currentGuide,
|
|
1398
|
+
currentHandoff,
|
|
1399
|
+
currentReview
|
|
1400
|
+
],
|
|
1401
|
+
hotPath: [
|
|
1402
|
+
`${STATE_ROOT_NAME}/project.md`,
|
|
1403
|
+
`${STATE_ROOT_NAME}/state.md`,
|
|
1404
|
+
`${STATE_ROOT_NAME}/guides/read-this-first.md`,
|
|
1405
|
+
`${STATE_ROOT_NAME}/guides/code-consistency.md`,
|
|
1406
|
+
latestSummary,
|
|
1407
|
+
planPath,
|
|
1408
|
+
currentGuide
|
|
1409
|
+
],
|
|
1410
|
+
activeAgents: sameCycle ? current.activeAgents : {
|
|
1411
|
+
main: "current main session",
|
|
1412
|
+
subagents: [],
|
|
1413
|
+
verification: null,
|
|
1414
|
+
consistency: null
|
|
1415
|
+
},
|
|
1416
|
+
notes: sameCycle ? current.notes : [
|
|
1417
|
+
`Current work cycle: ${title}.`,
|
|
1418
|
+
`Fill in the approved plan and scope guide before implementation starts.`,
|
|
1419
|
+
previousSummary === null ? "No previous summary was recorded." : `Previous relevant summary: ${previousSummary}`
|
|
1420
|
+
]
|
|
1421
|
+
});
|
|
1422
|
+
const currentWrites = writeCurrentArtifacts(
|
|
1423
|
+
root,
|
|
1424
|
+
nextCurrent,
|
|
1425
|
+
buildTemplateContext(root).implementationMenuPath
|
|
1426
|
+
);
|
|
1427
|
+
if (currentWrites.index !== "unchanged") {
|
|
1428
|
+
updatedFiles.push(`${STATE_ROOT_NAME}/index/current.json`);
|
|
1429
|
+
}
|
|
1430
|
+
if (currentWrites.state !== "unchanged") {
|
|
1431
|
+
updatedFiles.push(`${STATE_ROOT_NAME}/state.md`);
|
|
1432
|
+
}
|
|
1433
|
+
if (currentWrites.readThisFirst !== "unchanged") {
|
|
1434
|
+
updatedFiles.push(`${STATE_ROOT_NAME}/guides/read-this-first.md`);
|
|
1435
|
+
}
|
|
1436
|
+
return {
|
|
1437
|
+
root,
|
|
1438
|
+
slug,
|
|
1439
|
+
title,
|
|
1440
|
+
createdFiles,
|
|
1441
|
+
keptFiles,
|
|
1442
|
+
updatedFiles,
|
|
1443
|
+
planPath,
|
|
1444
|
+
summaryPath,
|
|
1445
|
+
runPath: runWrite.path,
|
|
1446
|
+
handoffPath,
|
|
1447
|
+
reviewPath,
|
|
1448
|
+
guidePath
|
|
1449
|
+
};
|
|
1450
|
+
}
|
|
1451
|
+
|
|
1452
|
+
// src/commands/begin.ts
|
|
1453
|
+
function runBegin(args) {
|
|
1454
|
+
const [slug, ...titleParts] = args;
|
|
1455
|
+
if (slug === void 0) {
|
|
1456
|
+
console.error("Usage: sb-codex-tool begin <slug> [title words]");
|
|
1457
|
+
return 1;
|
|
1458
|
+
}
|
|
1459
|
+
const title = titleParts.length > 0 ? titleParts.join(" ") : void 0;
|
|
1460
|
+
const result = beginWorkCycle(process.cwd(), slug, title);
|
|
1461
|
+
console.log(`Project root: ${result.root}`);
|
|
1462
|
+
console.log(`Work cycle: ${result.title}`);
|
|
1463
|
+
console.log("");
|
|
1464
|
+
console.log("Artifacts:");
|
|
1465
|
+
console.log(`- plan: ${result.planPath}`);
|
|
1466
|
+
console.log(`- summary: ${result.summaryPath}`);
|
|
1467
|
+
console.log(`- run: ${result.runPath}`);
|
|
1468
|
+
console.log(`- handoff: ${result.handoffPath}`);
|
|
1469
|
+
console.log(`- review: ${result.reviewPath}`);
|
|
1470
|
+
console.log(`- guide: ${result.guidePath}`);
|
|
1471
|
+
console.log("");
|
|
1472
|
+
console.log(`Created files: ${result.createdFiles.length}`);
|
|
1473
|
+
for (const file of result.createdFiles) {
|
|
1474
|
+
console.log(`- ${file}`);
|
|
1475
|
+
}
|
|
1476
|
+
if (result.updatedFiles.length > 0) {
|
|
1477
|
+
console.log("");
|
|
1478
|
+
console.log("Updated state:");
|
|
1479
|
+
for (const file of result.updatedFiles) {
|
|
1480
|
+
console.log(`- ${file}`);
|
|
1481
|
+
}
|
|
1482
|
+
}
|
|
1483
|
+
return 0;
|
|
1484
|
+
}
|
|
1485
|
+
|
|
1486
|
+
// src/lib/close-cycle.ts
|
|
1487
|
+
import path9 from "node:path";
|
|
1488
|
+
|
|
1489
|
+
// src/lib/work-journal.ts
|
|
1490
|
+
import path8 from "node:path";
|
|
1491
|
+
function escapeRegExp(value) {
|
|
1492
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1493
|
+
}
|
|
1494
|
+
function normalizeList(items, fallback) {
|
|
1495
|
+
const unique = items.filter((item, index) => items.indexOf(item) === index);
|
|
1496
|
+
return unique.length > 0 ? unique : [fallback];
|
|
1497
|
+
}
|
|
1498
|
+
function renderBullet(item) {
|
|
1499
|
+
return item.split("\n").map((line, index) => index === 0 ? `- ${line}` : ` ${line}`).join("\n");
|
|
1500
|
+
}
|
|
1501
|
+
function renderBulletList2(items) {
|
|
1502
|
+
return normalizeList(items, "None.").map((item) => renderBullet(item)).join("\n");
|
|
1503
|
+
}
|
|
1504
|
+
function buildEntry(input) {
|
|
1505
|
+
return `## Entry: ${input.title}
|
|
1506
|
+
|
|
1507
|
+
### Date
|
|
1508
|
+
|
|
1509
|
+
- ${input.dateStamp}
|
|
1510
|
+
|
|
1511
|
+
### Summary
|
|
1512
|
+
|
|
1513
|
+
${renderBulletList2(input.summaryLines)}
|
|
1514
|
+
|
|
1515
|
+
### Completed
|
|
1516
|
+
|
|
1517
|
+
${renderBulletList2(input.completedLines)}
|
|
1518
|
+
|
|
1519
|
+
### Changed Areas
|
|
1520
|
+
|
|
1521
|
+
${renderBulletList2(input.changedAreas)}
|
|
1522
|
+
|
|
1523
|
+
### Verification
|
|
1524
|
+
|
|
1525
|
+
- Fresh verification verdict: \`${input.verdict}\`
|
|
1526
|
+
- Verification summary: \`${input.verificationSummaryPath}\`
|
|
1527
|
+
|
|
1528
|
+
### Open Issues
|
|
1529
|
+
|
|
1530
|
+
${renderBulletList2(input.openIssues)}
|
|
1531
|
+
|
|
1532
|
+
### Next
|
|
1533
|
+
|
|
1534
|
+
- ${input.nextAction}
|
|
1535
|
+
`;
|
|
1536
|
+
}
|
|
1537
|
+
function getWorkJournalPath(root, dateStamp) {
|
|
1538
|
+
return path8.join(root, STATE_ROOT_NAME, "logs", "work-journal", `${dateStamp}.md`);
|
|
1539
|
+
}
|
|
1540
|
+
function writeWorkJournalEntry(root, input) {
|
|
1541
|
+
const journalPath = getWorkJournalPath(root, input.dateStamp);
|
|
1542
|
+
const current = readTextIfPresent(journalPath);
|
|
1543
|
+
const entry = buildEntry(input).trimEnd();
|
|
1544
|
+
if (current === null) {
|
|
1545
|
+
return writeFileIfChanged(
|
|
1546
|
+
journalPath,
|
|
1547
|
+
`# ${input.dateStamp}
|
|
1548
|
+
|
|
1549
|
+
${entry}
|
|
1550
|
+
`
|
|
1551
|
+
);
|
|
1552
|
+
}
|
|
1553
|
+
const marker = `## Entry: ${input.title}`;
|
|
1554
|
+
const pattern = new RegExp(
|
|
1555
|
+
`${escapeRegExp(marker)}[\\s\\S]*?(?=\\n## Entry: |$)`,
|
|
1556
|
+
"m"
|
|
1557
|
+
);
|
|
1558
|
+
const next = pattern.test(current) ? current.replace(pattern, entry) : `${current.trimEnd()}
|
|
1559
|
+
|
|
1560
|
+
${entry}
|
|
1561
|
+
`;
|
|
1562
|
+
return writeFileIfChanged(journalPath, next.endsWith("\n") ? next : `${next}
|
|
1563
|
+
`);
|
|
1564
|
+
}
|
|
1565
|
+
|
|
1566
|
+
// src/lib/close-cycle.ts
|
|
1567
|
+
var VERDICTS = ["pass", "pass_with_concerns", "fail", "blocked"];
|
|
1568
|
+
var REVIEW_PLACEHOLDERS = {
|
|
1569
|
+
findings: [
|
|
1570
|
+
"- Add findings here in severity order.",
|
|
1571
|
+
"- None."
|
|
1572
|
+
],
|
|
1573
|
+
concerns: [
|
|
1574
|
+
"- Add non-blocking concerns here.",
|
|
1575
|
+
"- None."
|
|
1576
|
+
],
|
|
1577
|
+
missingEvidence: [
|
|
1578
|
+
"- Add any blocked evidence here.",
|
|
1579
|
+
"- None."
|
|
1580
|
+
]
|
|
1581
|
+
};
|
|
1582
|
+
function isClosingVerdict(verdict) {
|
|
1583
|
+
return verdict === "pass" || verdict === "pass_with_concerns";
|
|
1584
|
+
}
|
|
1585
|
+
function extractVerdict(content) {
|
|
1586
|
+
const verdict = extractSectionLines(content, "Verdict")[0]?.replace(/^- /, "");
|
|
1587
|
+
if (verdict === void 0 || !VERDICTS.includes(verdict)) {
|
|
1588
|
+
throw new Error("close requires the current review to contain a final verification verdict.");
|
|
1589
|
+
}
|
|
1590
|
+
return verdict;
|
|
1591
|
+
}
|
|
1592
|
+
function sectionContainsPlaceholder(lines, placeholders) {
|
|
1593
|
+
return lines.some((line) => placeholders.includes(line));
|
|
1594
|
+
}
|
|
1595
|
+
function sectionHasExplicitDetail(lines, placeholders) {
|
|
1596
|
+
return lines.some((line) => !placeholders.includes(line));
|
|
1597
|
+
}
|
|
1598
|
+
function parseReview(content) {
|
|
1599
|
+
const review = {
|
|
1600
|
+
verdict: extractVerdict(content),
|
|
1601
|
+
findings: extractSectionLines(content, "Findings"),
|
|
1602
|
+
concerns: extractSectionLines(content, "Concerns"),
|
|
1603
|
+
missingEvidence: extractSectionLines(content, "Missing Evidence"),
|
|
1604
|
+
checksRun: extractSectionLines(content, "Checks Run")
|
|
1605
|
+
};
|
|
1606
|
+
if (review.verdict === "pass_with_concerns" && (sectionContainsPlaceholder(review.concerns, REVIEW_PLACEHOLDERS.concerns) || !sectionHasExplicitDetail(review.concerns, REVIEW_PLACEHOLDERS.concerns))) {
|
|
1607
|
+
throw new Error(
|
|
1608
|
+
"close requires explicit concerns in the review before pass_with_concerns can close the cycle."
|
|
1609
|
+
);
|
|
1610
|
+
}
|
|
1611
|
+
if (review.verdict === "fail" && (sectionContainsPlaceholder(review.findings, REVIEW_PLACEHOLDERS.findings) || !sectionHasExplicitDetail(review.findings, REVIEW_PLACEHOLDERS.findings))) {
|
|
1612
|
+
throw new Error(
|
|
1613
|
+
"close requires explicit findings in the review before fail can be recorded."
|
|
1614
|
+
);
|
|
1615
|
+
}
|
|
1616
|
+
if (review.verdict === "blocked" && (sectionContainsPlaceholder(
|
|
1617
|
+
review.missingEvidence,
|
|
1618
|
+
REVIEW_PLACEHOLDERS.missingEvidence
|
|
1619
|
+
) || !sectionHasExplicitDetail(
|
|
1620
|
+
review.missingEvidence,
|
|
1621
|
+
REVIEW_PLACEHOLDERS.missingEvidence
|
|
1622
|
+
))) {
|
|
1623
|
+
throw new Error(
|
|
1624
|
+
"close requires explicit missing evidence details before blocked can be recorded."
|
|
1625
|
+
);
|
|
1626
|
+
}
|
|
1627
|
+
return review;
|
|
1628
|
+
}
|
|
1629
|
+
function updateWorkJournalDecision(content, verdict) {
|
|
1630
|
+
const decision = isClosingVerdict(verdict) ? "Verified closure is complete enough to update the work journal." : "Do not update the work journal as verified completion yet.";
|
|
1631
|
+
return content.replace(
|
|
1632
|
+
/(## Work Journal Decision\n\n)- .*?(\n)/,
|
|
1633
|
+
`$1- ${decision}$2`
|
|
1634
|
+
);
|
|
1635
|
+
}
|
|
1636
|
+
function readChecksRun(root, summaryPath) {
|
|
1637
|
+
const text = readTextIfPresent(path9.join(root, summaryPath));
|
|
1638
|
+
if (text === null) {
|
|
1639
|
+
return ["- none recorded"];
|
|
1640
|
+
}
|
|
1641
|
+
const section = text.match(/## Checks Run\s+([\s\S]*?)\n## /);
|
|
1642
|
+
if (section === null) {
|
|
1643
|
+
return ["- none recorded"];
|
|
1644
|
+
}
|
|
1645
|
+
const lines = section[1].trim().split("\n").map((line) => line.trim()).filter((line) => line.startsWith("- "));
|
|
1646
|
+
return lines.length > 0 ? lines : ["- none recorded"];
|
|
1647
|
+
}
|
|
1648
|
+
function readSummaryContext(root, summaryPath) {
|
|
1649
|
+
const text = readTextIfPresent(path9.join(root, summaryPath));
|
|
1650
|
+
if (text === null) {
|
|
1651
|
+
return {
|
|
1652
|
+
scope: [],
|
|
1653
|
+
implementedSurface: [],
|
|
1654
|
+
nextAgentGuidance: []
|
|
1655
|
+
};
|
|
1656
|
+
}
|
|
1657
|
+
return {
|
|
1658
|
+
scope: extractSectionLines(text, "Scope"),
|
|
1659
|
+
implementedSurface: extractSectionLines(text, "Implemented Surface"),
|
|
1660
|
+
nextAgentGuidance: extractSectionLines(text, "Next-Agent Guidance")
|
|
1661
|
+
};
|
|
1662
|
+
}
|
|
1663
|
+
function readAllowedFileScope2(root, guidePath) {
|
|
1664
|
+
if (guidePath === null) {
|
|
1665
|
+
return [];
|
|
1666
|
+
}
|
|
1667
|
+
const text = readTextIfPresent(path9.join(root, guidePath));
|
|
1668
|
+
if (text === null) {
|
|
1669
|
+
return [];
|
|
1670
|
+
}
|
|
1671
|
+
return extractSectionLines(text, "Allowed File Scope");
|
|
1672
|
+
}
|
|
1673
|
+
function buildVerificationSummary(title, review, planPath, executionSummaryPath, reviewPath, runPath, gitContextSection, guidePath, checksRun) {
|
|
1674
|
+
const closureComplete = isClosingVerdict(review.verdict);
|
|
1675
|
+
const effectiveChecks = review.checksRun.length > 0 ? review.checksRun : checksRun;
|
|
1676
|
+
return `# Verification Summary: ${title}
|
|
1677
|
+
|
|
1678
|
+
## Verdict
|
|
1679
|
+
|
|
1680
|
+
- ${review.verdict}
|
|
1681
|
+
|
|
1682
|
+
## Verification Scope
|
|
1683
|
+
|
|
1684
|
+
- ${title}
|
|
1685
|
+
- ${planPath}
|
|
1686
|
+
- ${executionSummaryPath}
|
|
1687
|
+
- ${reviewPath}
|
|
1688
|
+
${guidePath === null ? "" : `- ${guidePath}
|
|
1689
|
+
`}
|
|
1690
|
+
${gitContextSection}
|
|
1691
|
+
## Checks Run
|
|
1692
|
+
|
|
1693
|
+
${effectiveChecks.join("\n")}
|
|
1694
|
+
|
|
1695
|
+
## Evidence
|
|
1696
|
+
|
|
1697
|
+
- The fresh verification verdict is recorded in \`${reviewPath}\`.
|
|
1698
|
+
- Current-state artifacts can be updated from one shared writer after the
|
|
1699
|
+
review result is already present.
|
|
1700
|
+
- The latest verification summary is available for the next-agent hot path.
|
|
1701
|
+
${closureComplete ? "- Verified closure is complete enough to update the work journal." : "- Verified closure is not complete enough to update the work journal yet."}
|
|
1702
|
+
|
|
1703
|
+
## Plan vs Actual
|
|
1704
|
+
|
|
1705
|
+
- Planned: record the fresh verification result for the current cycle
|
|
1706
|
+
- Actual: the close flow reads the review artifact and records the result
|
|
1707
|
+
- Planned: keep current-state artifacts aligned after closure
|
|
1708
|
+
- Actual: the close flow updates the latest summary and hot-path references together
|
|
1709
|
+
|
|
1710
|
+
## Findings
|
|
1711
|
+
|
|
1712
|
+
${review.findings.length > 0 ? review.findings.join("\n") : "- None."}
|
|
1713
|
+
|
|
1714
|
+
## Concerns
|
|
1715
|
+
|
|
1716
|
+
${review.concerns.length > 0 ? review.concerns.join("\n") : "- None."}
|
|
1717
|
+
|
|
1718
|
+
## Missing Evidence
|
|
1719
|
+
|
|
1720
|
+
${review.missingEvidence.length > 0 ? review.missingEvidence.join("\n") : "- None."}
|
|
1721
|
+
|
|
1722
|
+
## Closure Decision
|
|
1723
|
+
|
|
1724
|
+
- ${closureComplete ? "Verified closure is complete." : "Verified closure is not complete; keep the cycle in verify."}
|
|
1725
|
+
|
|
1726
|
+
## Related Review Artifact
|
|
1727
|
+
|
|
1728
|
+
- ${reviewPath}
|
|
1729
|
+
`;
|
|
1730
|
+
}
|
|
1731
|
+
function closeCurrentCycle(start) {
|
|
1732
|
+
const root = resolveProjectRoot(start);
|
|
1733
|
+
const current = normalizeCurrentIndex(readCurrentIndex(root));
|
|
1734
|
+
if (current.currentStage !== "verify") {
|
|
1735
|
+
throw new Error("close requires the current stage to be verify.");
|
|
1736
|
+
}
|
|
1737
|
+
if (current.latestApprovedPlan === null) {
|
|
1738
|
+
throw new Error("close requires a current approved plan.");
|
|
1739
|
+
}
|
|
1740
|
+
if (current.latestRelevantSummary === null) {
|
|
1741
|
+
throw new Error("close requires a current execution summary.");
|
|
1742
|
+
}
|
|
1743
|
+
if (current.currentReview === null) {
|
|
1744
|
+
throw new Error("close requires a current review artifact.");
|
|
1745
|
+
}
|
|
1746
|
+
const { dateStamp, slug } = parseCycleDescriptor(current.latestApprovedPlan);
|
|
1747
|
+
const title = readCycleTitle(root, current.latestApprovedPlan, slug);
|
|
1748
|
+
const runPath = lifecycleRunPath(dateStamp, slug);
|
|
1749
|
+
const verificationSummaryPath = `${STATE_ROOT_NAME}/summaries/${dateStamp}-${slug}-verification-summary.md`;
|
|
1750
|
+
const reviewAbsolutePath = path9.join(root, current.currentReview);
|
|
1751
|
+
const reviewBefore = readTextIfPresent(reviewAbsolutePath);
|
|
1752
|
+
if (reviewBefore === null) {
|
|
1753
|
+
throw new Error(`close requires the review artifact ${current.currentReview}.`);
|
|
1754
|
+
}
|
|
1755
|
+
const review = parseReview(reviewBefore);
|
|
1756
|
+
const createdFiles = [];
|
|
1757
|
+
const updatedFiles = [];
|
|
1758
|
+
const closingSummaryContext = isClosingVerdict(review.verdict) ? readSummaryContext(root, current.latestRelevantSummary) : null;
|
|
1759
|
+
const git = getGitContext(root);
|
|
1760
|
+
if (closingSummaryContext !== null && closingSummaryContext.nextAgentGuidance.length === 0) {
|
|
1761
|
+
throw new Error(
|
|
1762
|
+
"close requires the current execution summary to include explicit Next-Agent Guidance before verified closure."
|
|
1763
|
+
);
|
|
1764
|
+
}
|
|
1765
|
+
const reviewWrite = writeFileIfChanged(
|
|
1766
|
+
reviewAbsolutePath,
|
|
1767
|
+
updateWorkJournalDecision(reviewBefore, review.verdict)
|
|
1768
|
+
);
|
|
1769
|
+
if (reviewWrite === "updated") {
|
|
1770
|
+
updatedFiles.push(current.currentReview);
|
|
1771
|
+
}
|
|
1772
|
+
const checksRun = readChecksRun(root, current.latestRelevantSummary);
|
|
1773
|
+
const journalPath = isClosingVerdict(review.verdict) ? `${STATE_ROOT_NAME}/logs/work-journal/${dateStamp}.md` : null;
|
|
1774
|
+
const nextStage = isClosingVerdict(review.verdict) ? "clarify" : "verify";
|
|
1775
|
+
const runWrite = writeLifecycleRunRecord(root, {
|
|
1776
|
+
dateStamp,
|
|
1777
|
+
slug,
|
|
1778
|
+
title,
|
|
1779
|
+
phase: "close",
|
|
1780
|
+
stage: nextStage,
|
|
1781
|
+
verdict: review.verdict,
|
|
1782
|
+
git,
|
|
1783
|
+
paths: {
|
|
1784
|
+
planPath: current.latestApprovedPlan,
|
|
1785
|
+
executionSummaryPath: current.latestRelevantSummary,
|
|
1786
|
+
handoffPath: current.currentHandoff,
|
|
1787
|
+
reviewPath: current.currentReview,
|
|
1788
|
+
guidePath: current.currentGuide,
|
|
1789
|
+
verificationSummaryPath,
|
|
1790
|
+
journalPath
|
|
1791
|
+
}
|
|
1792
|
+
});
|
|
1793
|
+
if (runWrite.writeResult === "created") {
|
|
1794
|
+
createdFiles.push(runWrite.path);
|
|
1795
|
+
} else if (runWrite.writeResult === "updated") {
|
|
1796
|
+
updatedFiles.push(runWrite.path);
|
|
1797
|
+
}
|
|
1798
|
+
const verificationSummary = buildVerificationSummary(
|
|
1799
|
+
title,
|
|
1800
|
+
review,
|
|
1801
|
+
current.latestApprovedPlan,
|
|
1802
|
+
current.latestRelevantSummary,
|
|
1803
|
+
current.currentReview,
|
|
1804
|
+
runWrite.path,
|
|
1805
|
+
renderGitContextSection(runWrite.path, git),
|
|
1806
|
+
current.currentGuide,
|
|
1807
|
+
checksRun
|
|
1808
|
+
);
|
|
1809
|
+
const verificationWrite = writeFileIfChanged(
|
|
1810
|
+
path9.join(root, verificationSummaryPath),
|
|
1811
|
+
verificationSummary
|
|
1812
|
+
);
|
|
1813
|
+
if (verificationWrite === "created") {
|
|
1814
|
+
createdFiles.push(verificationSummaryPath);
|
|
1815
|
+
} else if (verificationWrite === "updated") {
|
|
1816
|
+
updatedFiles.push(verificationSummaryPath);
|
|
1817
|
+
}
|
|
1818
|
+
const nextAction = isClosingVerdict(review.verdict) ? "Start the next implementation increment from a new approved plan." : `Address the latest ${review.verdict} verification result for ${title} and rerun fresh verification.`;
|
|
1819
|
+
if (isClosingVerdict(review.verdict)) {
|
|
1820
|
+
const allowedFileScope = readAllowedFileScope2(root, current.currentGuide);
|
|
1821
|
+
const journalWrite = writeWorkJournalEntry(root, {
|
|
1822
|
+
dateStamp,
|
|
1823
|
+
title,
|
|
1824
|
+
verdict: review.verdict,
|
|
1825
|
+
summaryLines: [
|
|
1826
|
+
`Closed the ${title} increment with verdict \`${review.verdict}\`.`,
|
|
1827
|
+
...stripBulletPrefix(closingSummaryContext.scope)
|
|
1828
|
+
],
|
|
1829
|
+
completedLines: [
|
|
1830
|
+
...stripBulletPrefix(closingSummaryContext.implementedSurface),
|
|
1831
|
+
`Recorded the fresh verification verdict in \`${current.currentReview}\`.`,
|
|
1832
|
+
`Wrote \`${verificationSummaryPath}\`.`,
|
|
1833
|
+
"Updated current-state artifacts for the next step."
|
|
1834
|
+
],
|
|
1835
|
+
reviewPath: current.currentReview,
|
|
1836
|
+
verificationSummaryPath,
|
|
1837
|
+
changedAreas: stripBulletPrefix(allowedFileScope).length > 0 ? stripBulletPrefix(allowedFileScope) : [
|
|
1838
|
+
current.currentReview,
|
|
1839
|
+
verificationSummaryPath,
|
|
1840
|
+
current.latestApprovedPlan,
|
|
1841
|
+
current.currentGuide ?? "no current guide recorded",
|
|
1842
|
+
`${STATE_ROOT_NAME}/state.md`,
|
|
1843
|
+
`${STATE_ROOT_NAME}/index/current.json`,
|
|
1844
|
+
`${STATE_ROOT_NAME}/guides/read-this-first.md`
|
|
1845
|
+
],
|
|
1846
|
+
openIssues: review.verdict === "pass" ? ["None."] : review.concerns.map((line) => line.replace(/^- /, "")),
|
|
1847
|
+
nextAction
|
|
1848
|
+
});
|
|
1849
|
+
if (journalWrite === "created") {
|
|
1850
|
+
createdFiles.push(journalPath);
|
|
1851
|
+
} else if (journalWrite === "updated") {
|
|
1852
|
+
updatedFiles.push(journalPath);
|
|
1853
|
+
}
|
|
1854
|
+
}
|
|
1855
|
+
const nextCurrent = normalizeCurrentIndex({
|
|
1856
|
+
...current,
|
|
1857
|
+
currentStage: nextStage,
|
|
1858
|
+
nextAction,
|
|
1859
|
+
latestRelevantSummary: verificationSummaryPath,
|
|
1860
|
+
latestRun: runWrite.path,
|
|
1861
|
+
currentFocusModules: [
|
|
1862
|
+
"AGENTS.md",
|
|
1863
|
+
`${STATE_ROOT_NAME}/guides/code-consistency.md`,
|
|
1864
|
+
verificationSummaryPath,
|
|
1865
|
+
runWrite.path,
|
|
1866
|
+
current.currentReview,
|
|
1867
|
+
current.latestApprovedPlan,
|
|
1868
|
+
current.currentGuide ?? `${STATE_ROOT_NAME}/guides/read-this-first.md`
|
|
1869
|
+
],
|
|
1870
|
+
hotPath: [
|
|
1871
|
+
`${STATE_ROOT_NAME}/project.md`,
|
|
1872
|
+
`${STATE_ROOT_NAME}/state.md`,
|
|
1873
|
+
`${STATE_ROOT_NAME}/guides/read-this-first.md`,
|
|
1874
|
+
`${STATE_ROOT_NAME}/guides/code-consistency.md`,
|
|
1875
|
+
verificationSummaryPath,
|
|
1876
|
+
current.latestApprovedPlan,
|
|
1877
|
+
current.currentGuide ?? `${STATE_ROOT_NAME}/guides/read-this-first.md`
|
|
1878
|
+
],
|
|
1879
|
+
activeAgents: {
|
|
1880
|
+
...current.activeAgents,
|
|
1881
|
+
verification: null
|
|
1882
|
+
},
|
|
1883
|
+
notes: isClosingVerdict(review.verdict) ? [
|
|
1884
|
+
`Current work cycle: ${title}.`,
|
|
1885
|
+
`The increment is closed with verdict ${review.verdict}.`,
|
|
1886
|
+
"The next increment can start from a new approved plan.",
|
|
1887
|
+
`Previous relevant summary: ${current.latestRelevantSummary}`
|
|
1888
|
+
] : [
|
|
1889
|
+
`Current work cycle: ${title}.`,
|
|
1890
|
+
`The latest fresh verification verdict is ${review.verdict}.`,
|
|
1891
|
+
"Address the verification result before attempting closure again.",
|
|
1892
|
+
`Previous relevant summary: ${current.latestRelevantSummary}`
|
|
1893
|
+
]
|
|
1894
|
+
});
|
|
1895
|
+
const currentWrites = writeCurrentArtifacts(
|
|
1896
|
+
root,
|
|
1897
|
+
nextCurrent,
|
|
1898
|
+
buildTemplateContext(root).implementationMenuPath
|
|
1899
|
+
);
|
|
1900
|
+
if (currentWrites.index !== "unchanged") {
|
|
1901
|
+
updatedFiles.push(`${STATE_ROOT_NAME}/index/current.json`);
|
|
1902
|
+
}
|
|
1903
|
+
if (currentWrites.state !== "unchanged") {
|
|
1904
|
+
updatedFiles.push(`${STATE_ROOT_NAME}/state.md`);
|
|
1905
|
+
}
|
|
1906
|
+
if (currentWrites.readThisFirst !== "unchanged") {
|
|
1907
|
+
updatedFiles.push(`${STATE_ROOT_NAME}/guides/read-this-first.md`);
|
|
1908
|
+
}
|
|
1909
|
+
return {
|
|
1910
|
+
root,
|
|
1911
|
+
title,
|
|
1912
|
+
verdict: review.verdict,
|
|
1913
|
+
reviewPath: current.currentReview,
|
|
1914
|
+
verificationSummaryPath,
|
|
1915
|
+
runPath: runWrite.path,
|
|
1916
|
+
journalPath,
|
|
1917
|
+
createdFiles,
|
|
1918
|
+
updatedFiles
|
|
1919
|
+
};
|
|
1920
|
+
}
|
|
1921
|
+
|
|
1922
|
+
// src/commands/close.ts
|
|
1923
|
+
function runClose(args) {
|
|
1924
|
+
if (args.length > 0) {
|
|
1925
|
+
console.error("Usage: sb-codex-tool close");
|
|
1926
|
+
return 1;
|
|
1927
|
+
}
|
|
1928
|
+
const result = closeCurrentCycle(process.cwd());
|
|
1929
|
+
console.log(`Project root: ${result.root}`);
|
|
1930
|
+
console.log(`Work cycle: ${result.title}`);
|
|
1931
|
+
console.log(`Verdict: ${result.verdict}`);
|
|
1932
|
+
console.log("");
|
|
1933
|
+
console.log("Closure artifacts:");
|
|
1934
|
+
console.log(`- review: ${result.reviewPath}`);
|
|
1935
|
+
console.log(`- verification summary: ${result.verificationSummaryPath}`);
|
|
1936
|
+
console.log(`- run: ${result.runPath}`);
|
|
1937
|
+
console.log(`- work journal: ${result.journalPath ?? "not updated for this verdict"}`);
|
|
1938
|
+
console.log("");
|
|
1939
|
+
console.log(`Created files: ${result.createdFiles.length}`);
|
|
1940
|
+
for (const file of result.createdFiles) {
|
|
1941
|
+
console.log(`- ${file}`);
|
|
1942
|
+
}
|
|
1943
|
+
if (result.updatedFiles.length > 0) {
|
|
1944
|
+
console.log("");
|
|
1945
|
+
console.log("Updated files:");
|
|
1946
|
+
for (const file of result.updatedFiles) {
|
|
1947
|
+
console.log(`- ${file}`);
|
|
1948
|
+
}
|
|
1949
|
+
}
|
|
1950
|
+
return 0;
|
|
1951
|
+
}
|
|
1952
|
+
|
|
1953
|
+
// src/lib/assignment-lifecycle.ts
|
|
1954
|
+
import path10 from "node:path";
|
|
1955
|
+
var ASSIGNMENT_LIFECYCLE_ACTIONS = ["close", "clear", "replace"];
|
|
1956
|
+
function buildLifecycleArtifact(agentName, title, action, assignmentPath, replacementAssignmentPath) {
|
|
1957
|
+
return `# Assignment Lifecycle: ${title}
|
|
1958
|
+
|
|
1959
|
+
## Agent
|
|
1960
|
+
|
|
1961
|
+
- ${agentName}
|
|
1962
|
+
|
|
1963
|
+
## Decision
|
|
1964
|
+
|
|
1965
|
+
- ${action}
|
|
1966
|
+
|
|
1967
|
+
## Completed Assignment Guide
|
|
1968
|
+
|
|
1969
|
+
- ${assignmentPath}
|
|
1970
|
+
|
|
1971
|
+
## Lifecycle Rule Applied
|
|
1972
|
+
|
|
1973
|
+
- Default rule is close and replace.
|
|
1974
|
+
- Clear is allowed only for the same narrow role after context reset.
|
|
1975
|
+
- Final verification remains fresh-agent-only.
|
|
1976
|
+
|
|
1977
|
+
## Replacement Assignment
|
|
1978
|
+
|
|
1979
|
+
- ${replacementAssignmentPath ?? "none"}
|
|
1980
|
+
|
|
1981
|
+
## Next Main-Agent Action
|
|
1982
|
+
|
|
1983
|
+
- ${replacementAssignmentPath === null ? "Keep the agent inactive until a new bounded assignment is created." : `Continue with the replacement assignment guide at \`${replacementAssignmentPath}\`.`}
|
|
1984
|
+
`;
|
|
1985
|
+
}
|
|
1986
|
+
function findAssignmentGuide(current, agentName) {
|
|
1987
|
+
const fromMap = current.assignmentGuides[agentName];
|
|
1988
|
+
if (fromMap !== void 0) {
|
|
1989
|
+
return fromMap;
|
|
1990
|
+
}
|
|
1991
|
+
if (current.latestApprovedPlan === null) {
|
|
1992
|
+
return null;
|
|
1993
|
+
}
|
|
1994
|
+
const { dateStamp } = parseCycleDescriptor(current.latestApprovedPlan);
|
|
1995
|
+
const agentSlug = normalizeSlug(agentName);
|
|
1996
|
+
return current.currentFocusModules.find(
|
|
1997
|
+
(entry) => entry.startsWith(`${STATE_ROOT_NAME}/guides/${dateStamp}-${agentSlug}-`) && entry.endsWith("-assignment.md")
|
|
1998
|
+
) ?? null;
|
|
1999
|
+
}
|
|
2000
|
+
function removeAssignmentGuide(assignmentGuides, agentName) {
|
|
2001
|
+
const next = { ...assignmentGuides };
|
|
2002
|
+
delete next[agentName];
|
|
2003
|
+
return next;
|
|
2004
|
+
}
|
|
2005
|
+
function withoutItem(items, target) {
|
|
2006
|
+
return items.filter((item) => item !== target);
|
|
2007
|
+
}
|
|
2008
|
+
function ensureFocusModules(currentFocusModules, items) {
|
|
2009
|
+
const next = [...currentFocusModules];
|
|
2010
|
+
for (const item of items) {
|
|
2011
|
+
if (item !== null && !next.includes(item)) {
|
|
2012
|
+
next.push(item);
|
|
2013
|
+
}
|
|
2014
|
+
}
|
|
2015
|
+
return next;
|
|
2016
|
+
}
|
|
2017
|
+
function readAssignmentTitle(root, assignmentPath) {
|
|
2018
|
+
const text = readTextIfPresent(path10.join(root, assignmentPath));
|
|
2019
|
+
const match = text?.match(/^# Assignment Guide: (.+)$/m);
|
|
2020
|
+
return match?.[1]?.trim() ?? humanizeSlug(normalizeSlug(agentNameFromPath(assignmentPath)));
|
|
2021
|
+
}
|
|
2022
|
+
function agentNameFromPath(assignmentPath) {
|
|
2023
|
+
const fileName = path10.basename(assignmentPath, ".md");
|
|
2024
|
+
return fileName;
|
|
2025
|
+
}
|
|
2026
|
+
function completeAssignment(start, agentName, action, replacement) {
|
|
2027
|
+
const root = resolveProjectRoot(start);
|
|
2028
|
+
const current = normalizeCurrentIndex(readCurrentIndex(root));
|
|
2029
|
+
if (current.latestApprovedPlan === null) {
|
|
2030
|
+
throw new Error("complete-assignment requires a current approved plan.");
|
|
2031
|
+
}
|
|
2032
|
+
if (!current.activeAgents.subagents.includes(agentName)) {
|
|
2033
|
+
throw new Error(`complete-assignment requires ${agentName} to be an active subagent.`);
|
|
2034
|
+
}
|
|
2035
|
+
const assignmentPath = findAssignmentGuide(current, agentName);
|
|
2036
|
+
if (assignmentPath === null) {
|
|
2037
|
+
throw new Error(`complete-assignment requires an active assignment guide for ${agentName}.`);
|
|
2038
|
+
}
|
|
2039
|
+
if (action === "replace" && replacement === void 0) {
|
|
2040
|
+
throw new Error("complete-assignment replace requires a replacement agent and slug.");
|
|
2041
|
+
}
|
|
2042
|
+
const createdFiles = [];
|
|
2043
|
+
const updatedFiles = [];
|
|
2044
|
+
let replacementAssignmentPath = null;
|
|
2045
|
+
let latestCurrent = current;
|
|
2046
|
+
if (action === "replace" && replacement !== void 0) {
|
|
2047
|
+
const replacementResult = createAssignmentGuide(
|
|
2048
|
+
root,
|
|
2049
|
+
replacement.agentName,
|
|
2050
|
+
replacement.slug,
|
|
2051
|
+
replacement.title
|
|
2052
|
+
);
|
|
2053
|
+
replacementAssignmentPath = replacementResult.assignmentPath;
|
|
2054
|
+
createdFiles.push(...replacementResult.createdFiles);
|
|
2055
|
+
updatedFiles.push(...replacementResult.updatedFiles);
|
|
2056
|
+
latestCurrent = normalizeCurrentIndex(readCurrentIndex(root));
|
|
2057
|
+
}
|
|
2058
|
+
const { dateStamp, slug } = parseCycleDescriptor(latestCurrent.latestApprovedPlan ?? current.latestApprovedPlan);
|
|
2059
|
+
const agentSlug = normalizeSlug(agentName);
|
|
2060
|
+
const lifecyclePath = `${STATE_ROOT_NAME}/handoffs/${dateStamp}-${slug}-${agentSlug}-assignment-lifecycle.md`;
|
|
2061
|
+
const assignmentTitle = readAssignmentTitle(root, assignmentPath);
|
|
2062
|
+
const lifecycleWrite = writeFileIfChanged(
|
|
2063
|
+
path10.join(root, lifecyclePath),
|
|
2064
|
+
buildLifecycleArtifact(
|
|
2065
|
+
agentName,
|
|
2066
|
+
assignmentTitle,
|
|
2067
|
+
action,
|
|
2068
|
+
assignmentPath,
|
|
2069
|
+
replacementAssignmentPath
|
|
2070
|
+
)
|
|
2071
|
+
);
|
|
2072
|
+
if (lifecycleWrite === "created") {
|
|
2073
|
+
createdFiles.push(lifecyclePath);
|
|
2074
|
+
} else if (lifecycleWrite === "updated") {
|
|
2075
|
+
updatedFiles.push(lifecyclePath);
|
|
2076
|
+
}
|
|
2077
|
+
const nextSubagents = withoutItem(latestCurrent.activeAgents.subagents, agentName);
|
|
2078
|
+
const nextAssignmentGuides = removeAssignmentGuide(latestCurrent.assignmentGuides, agentName);
|
|
2079
|
+
const nextFocusModules = ensureFocusModules(
|
|
2080
|
+
withoutItem(latestCurrent.currentFocusModules, assignmentPath),
|
|
2081
|
+
[lifecyclePath, replacementAssignmentPath]
|
|
2082
|
+
);
|
|
2083
|
+
const nextCurrent = normalizeCurrentIndex({
|
|
2084
|
+
...latestCurrent,
|
|
2085
|
+
latestAssignmentLifecycle: lifecyclePath,
|
|
2086
|
+
currentFocusModules: nextFocusModules,
|
|
2087
|
+
activeAgents: {
|
|
2088
|
+
...latestCurrent.activeAgents,
|
|
2089
|
+
subagents: nextSubagents
|
|
2090
|
+
},
|
|
2091
|
+
assignmentGuides: nextAssignmentGuides,
|
|
2092
|
+
notes: [
|
|
2093
|
+
`Current work cycle: ${humanizeSlug(parseCycleDescriptor(latestCurrent.latestApprovedPlan ?? current.latestApprovedPlan).slug)}.`,
|
|
2094
|
+
`Recorded ${action} lifecycle decision for ${agentName}: ${lifecyclePath}.`,
|
|
2095
|
+
replacementAssignmentPath === null ? `The completed assignment guide was ${assignmentPath}.` : `Replacement assignment guide: ${replacementAssignmentPath}.`,
|
|
2096
|
+
`Previous relevant summary: ${latestCurrent.latestRelevantSummary}`
|
|
2097
|
+
]
|
|
2098
|
+
});
|
|
2099
|
+
const currentWrites = writeCurrentArtifacts(
|
|
2100
|
+
root,
|
|
2101
|
+
nextCurrent,
|
|
2102
|
+
buildTemplateContext(root).implementationMenuPath
|
|
2103
|
+
);
|
|
2104
|
+
if (currentWrites.index !== "unchanged") {
|
|
2105
|
+
updatedFiles.push(`${STATE_ROOT_NAME}/index/current.json`);
|
|
2106
|
+
}
|
|
2107
|
+
if (currentWrites.state !== "unchanged") {
|
|
2108
|
+
updatedFiles.push(`${STATE_ROOT_NAME}/state.md`);
|
|
2109
|
+
}
|
|
2110
|
+
if (currentWrites.readThisFirst !== "unchanged") {
|
|
2111
|
+
updatedFiles.push(`${STATE_ROOT_NAME}/guides/read-this-first.md`);
|
|
2112
|
+
}
|
|
2113
|
+
return {
|
|
2114
|
+
root,
|
|
2115
|
+
agentName,
|
|
2116
|
+
action,
|
|
2117
|
+
lifecyclePath,
|
|
2118
|
+
replacementAssignmentPath,
|
|
2119
|
+
createdFiles,
|
|
2120
|
+
updatedFiles
|
|
2121
|
+
};
|
|
2122
|
+
}
|
|
2123
|
+
|
|
2124
|
+
// src/commands/complete-assignment.ts
|
|
2125
|
+
function runCompleteAssignment(args) {
|
|
2126
|
+
const [agentName, action, third, ...rest] = args;
|
|
2127
|
+
if (agentName === void 0 || action === void 0 || !ASSIGNMENT_LIFECYCLE_ACTIONS.includes(action)) {
|
|
2128
|
+
console.error(
|
|
2129
|
+
"Usage: sb-codex-tool complete-assignment <agent-name> <close|clear|replace> [replacement-agent] [replacement-slug] [title words]"
|
|
2130
|
+
);
|
|
2131
|
+
return 1;
|
|
2132
|
+
}
|
|
2133
|
+
const replacement = action === "replace" ? {
|
|
2134
|
+
agentName: third,
|
|
2135
|
+
slug: rest[0],
|
|
2136
|
+
title: rest.slice(1).length > 0 ? rest.slice(1).join(" ") : void 0
|
|
2137
|
+
} : void 0;
|
|
2138
|
+
if (action === "replace" && (replacement?.agentName === void 0 || replacement.slug === void 0)) {
|
|
2139
|
+
console.error(
|
|
2140
|
+
"Usage: sb-codex-tool complete-assignment <agent-name> replace <replacement-agent> <replacement-slug> [title words]"
|
|
2141
|
+
);
|
|
2142
|
+
return 1;
|
|
2143
|
+
}
|
|
2144
|
+
const result = completeAssignment(process.cwd(), agentName, action, replacement);
|
|
2145
|
+
console.log(`Project root: ${result.root}`);
|
|
2146
|
+
console.log(`Completed assignment for: ${result.agentName}`);
|
|
2147
|
+
console.log(`Lifecycle action: ${result.action}`);
|
|
2148
|
+
console.log(`Lifecycle artifact: ${result.lifecyclePath}`);
|
|
2149
|
+
console.log(`Replacement assignment: ${result.replacementAssignmentPath ?? "none"}`);
|
|
2150
|
+
console.log("");
|
|
2151
|
+
console.log(`Created files: ${result.createdFiles.length}`);
|
|
2152
|
+
for (const file of result.createdFiles) {
|
|
2153
|
+
console.log(`- ${file}`);
|
|
2154
|
+
}
|
|
2155
|
+
if (result.updatedFiles.length > 0) {
|
|
2156
|
+
console.log("");
|
|
2157
|
+
console.log("Updated files:");
|
|
2158
|
+
for (const file of result.updatedFiles) {
|
|
2159
|
+
console.log(`- ${file}`);
|
|
2160
|
+
}
|
|
2161
|
+
}
|
|
2162
|
+
return 0;
|
|
2163
|
+
}
|
|
2164
|
+
|
|
2165
|
+
// src/lib/doctor.ts
|
|
2166
|
+
import { existsSync as existsSync5 } from "node:fs";
|
|
2167
|
+
import path12 from "node:path";
|
|
2168
|
+
|
|
2169
|
+
// src/lib/state-coherence.ts
|
|
2170
|
+
import { existsSync as existsSync4 } from "node:fs";
|
|
2171
|
+
import path11 from "node:path";
|
|
2172
|
+
function createIssue(label, detail, level = "fail") {
|
|
2173
|
+
return { level, label, detail };
|
|
2174
|
+
}
|
|
2175
|
+
function validateLatestRunCoherence(current, latestRunRecord) {
|
|
2176
|
+
if (current.latestRun === null) {
|
|
2177
|
+
return [];
|
|
2178
|
+
}
|
|
2179
|
+
if (latestRunRecord === null) {
|
|
2180
|
+
return [
|
|
2181
|
+
createIssue(
|
|
2182
|
+
"latest run coherence",
|
|
2183
|
+
"current state references a latest run that could not be read"
|
|
2184
|
+
)
|
|
2185
|
+
];
|
|
2186
|
+
}
|
|
2187
|
+
const issues = [];
|
|
2188
|
+
if (latestRunRecord.stage !== current.currentStage) {
|
|
2189
|
+
issues.push(
|
|
2190
|
+
createIssue(
|
|
2191
|
+
"latest run coherence",
|
|
2192
|
+
`current stage ${current.currentStage} does not match latest run stage ${latestRunRecord.stage}`
|
|
2193
|
+
)
|
|
2194
|
+
);
|
|
2195
|
+
}
|
|
2196
|
+
if (current.latestApprovedPlan !== latestRunRecord.paths.planPath) {
|
|
2197
|
+
issues.push(
|
|
2198
|
+
createIssue(
|
|
2199
|
+
"latest run coherence",
|
|
2200
|
+
"current approved plan does not match the latest run plan path"
|
|
2201
|
+
)
|
|
2202
|
+
);
|
|
2203
|
+
}
|
|
2204
|
+
const allowedSummaryPaths = [
|
|
2205
|
+
latestRunRecord.paths.executionSummaryPath,
|
|
2206
|
+
latestRunRecord.paths.verificationSummaryPath
|
|
2207
|
+
].filter((value) => value !== null);
|
|
2208
|
+
if (current.latestRelevantSummary !== null && !allowedSummaryPaths.includes(current.latestRelevantSummary)) {
|
|
2209
|
+
issues.push(
|
|
2210
|
+
createIssue(
|
|
2211
|
+
"latest run coherence",
|
|
2212
|
+
"current latest summary does not match the latest run summary paths"
|
|
2213
|
+
)
|
|
2214
|
+
);
|
|
2215
|
+
}
|
|
2216
|
+
if (current.currentGuide !== latestRunRecord.paths.guidePath) {
|
|
2217
|
+
issues.push(
|
|
2218
|
+
createIssue(
|
|
2219
|
+
"latest run coherence",
|
|
2220
|
+
"current guide does not match the latest run guide path"
|
|
2221
|
+
)
|
|
2222
|
+
);
|
|
2223
|
+
}
|
|
2224
|
+
if (current.currentHandoff !== latestRunRecord.paths.handoffPath) {
|
|
2225
|
+
issues.push(
|
|
2226
|
+
createIssue(
|
|
2227
|
+
"latest run coherence",
|
|
2228
|
+
"current handoff does not match the latest run handoff path"
|
|
2229
|
+
)
|
|
2230
|
+
);
|
|
2231
|
+
}
|
|
2232
|
+
if (current.currentReview !== latestRunRecord.paths.reviewPath) {
|
|
2233
|
+
issues.push(
|
|
2234
|
+
createIssue(
|
|
2235
|
+
"latest run coherence",
|
|
2236
|
+
"current review does not match the latest run review path"
|
|
2237
|
+
)
|
|
2238
|
+
);
|
|
2239
|
+
}
|
|
2240
|
+
return issues;
|
|
2241
|
+
}
|
|
2242
|
+
function validateVerifyStage(current) {
|
|
2243
|
+
const issues = [];
|
|
2244
|
+
if (current.currentStage !== "verify") {
|
|
2245
|
+
if (current.activeAgents.verification !== null) {
|
|
2246
|
+
issues.push(
|
|
2247
|
+
createIssue(
|
|
2248
|
+
"verification-agent coherence",
|
|
2249
|
+
"verification agent is still assigned outside the verify stage"
|
|
2250
|
+
)
|
|
2251
|
+
);
|
|
2252
|
+
}
|
|
2253
|
+
return issues;
|
|
2254
|
+
}
|
|
2255
|
+
if (current.activeAgents.verification === null) {
|
|
2256
|
+
issues.push(
|
|
2257
|
+
createIssue(
|
|
2258
|
+
"verification-agent coherence",
|
|
2259
|
+
"verify stage requires a pending or assigned verification agent"
|
|
2260
|
+
)
|
|
2261
|
+
);
|
|
2262
|
+
}
|
|
2263
|
+
const missingArtifacts = [];
|
|
2264
|
+
if (current.currentGuide === null) {
|
|
2265
|
+
missingArtifacts.push("current guide");
|
|
2266
|
+
}
|
|
2267
|
+
if (current.currentHandoff === null) {
|
|
2268
|
+
missingArtifacts.push("current handoff");
|
|
2269
|
+
}
|
|
2270
|
+
if (current.currentReview === null) {
|
|
2271
|
+
missingArtifacts.push("current review");
|
|
2272
|
+
}
|
|
2273
|
+
if (current.latestRelevantSummary === null) {
|
|
2274
|
+
missingArtifacts.push("latest relevant summary");
|
|
2275
|
+
}
|
|
2276
|
+
if (current.latestApprovedPlan === null) {
|
|
2277
|
+
missingArtifacts.push("latest approved plan");
|
|
2278
|
+
}
|
|
2279
|
+
if (missingArtifacts.length > 0) {
|
|
2280
|
+
issues.push(
|
|
2281
|
+
createIssue(
|
|
2282
|
+
"verify-stage readiness",
|
|
2283
|
+
`verify stage is missing required references: ${missingArtifacts.join(", ")}`
|
|
2284
|
+
)
|
|
2285
|
+
);
|
|
2286
|
+
}
|
|
2287
|
+
return issues;
|
|
2288
|
+
}
|
|
2289
|
+
function validateAssignmentGuides(root, current) {
|
|
2290
|
+
const issues = [];
|
|
2291
|
+
const activeSubagents = new Set(current.activeAgents.subagents);
|
|
2292
|
+
const guideEntries = Object.entries(current.assignmentGuides);
|
|
2293
|
+
for (const agentName of activeSubagents) {
|
|
2294
|
+
if (current.assignmentGuides[agentName] === void 0) {
|
|
2295
|
+
issues.push(
|
|
2296
|
+
createIssue(
|
|
2297
|
+
"assignment-guide coherence",
|
|
2298
|
+
`active subagent ${agentName} is missing an assignment guide`
|
|
2299
|
+
)
|
|
2300
|
+
);
|
|
2301
|
+
}
|
|
2302
|
+
}
|
|
2303
|
+
for (const [agentName, guidePath] of guideEntries) {
|
|
2304
|
+
if (!activeSubagents.has(agentName)) {
|
|
2305
|
+
issues.push(
|
|
2306
|
+
createIssue(
|
|
2307
|
+
"assignment-guide coherence",
|
|
2308
|
+
`assignment guide for ${agentName} is still registered without an active subagent`
|
|
2309
|
+
)
|
|
2310
|
+
);
|
|
2311
|
+
continue;
|
|
2312
|
+
}
|
|
2313
|
+
if (!existsSync4(path11.join(root, guidePath))) {
|
|
2314
|
+
issues.push(
|
|
2315
|
+
createIssue(
|
|
2316
|
+
"assignment-guide coherence",
|
|
2317
|
+
`assignment guide for ${agentName} is missing: ${guidePath}`
|
|
2318
|
+
)
|
|
2319
|
+
);
|
|
2320
|
+
}
|
|
2321
|
+
}
|
|
2322
|
+
return issues;
|
|
2323
|
+
}
|
|
2324
|
+
function collectStateCoherenceIssues(root, current, latestRunRecord) {
|
|
2325
|
+
if (current === null) {
|
|
2326
|
+
return [];
|
|
2327
|
+
}
|
|
2328
|
+
return [
|
|
2329
|
+
...validateLatestRunCoherence(current, latestRunRecord),
|
|
2330
|
+
...validateVerifyStage(current),
|
|
2331
|
+
...validateAssignmentGuides(root, current)
|
|
2332
|
+
];
|
|
2333
|
+
}
|
|
2334
|
+
|
|
2335
|
+
// src/lib/doctor.ts
|
|
2336
|
+
var PLAN_PLACEHOLDERS = [
|
|
2337
|
+
"Replace with the concrete objective for this work cycle.",
|
|
2338
|
+
"Replace with the acceptance criteria that define completion.",
|
|
2339
|
+
"Replace with in-scope and out-of-scope notes.",
|
|
2340
|
+
"fill-in-file-scope",
|
|
2341
|
+
"describe the concrete implementation task",
|
|
2342
|
+
"describe how this task will be checked"
|
|
2343
|
+
];
|
|
2344
|
+
var GUIDE_PLACEHOLDERS = [
|
|
2345
|
+
"Replace with the concrete goal for this cycle.",
|
|
2346
|
+
"Replace with the files or modules that should be touched.",
|
|
2347
|
+
"Replace with the checks and verdict expectations for this cycle."
|
|
2348
|
+
];
|
|
2349
|
+
var EXECUTION_SUMMARY_PLACEHOLDERS = [
|
|
2350
|
+
"Replace with the actual implementation scope.",
|
|
2351
|
+
"not started yet",
|
|
2352
|
+
"none yet",
|
|
2353
|
+
"Update this section as implementation progresses.",
|
|
2354
|
+
"Update this section after refactor.",
|
|
2355
|
+
"Add deferred issues if they exist."
|
|
2356
|
+
];
|
|
2357
|
+
var HANDOFF_PLACEHOLDERS = [
|
|
2358
|
+
"Replace with the current implementation status.",
|
|
2359
|
+
"Replace with the checks relevant to this work cycle.",
|
|
2360
|
+
"Replace with the current blocker or risk list."
|
|
2361
|
+
];
|
|
2362
|
+
var CONSISTENCY_REVIEW_PLACEHOLDERS = [
|
|
2363
|
+
"Add findings here in severity order.",
|
|
2364
|
+
"Add follow-up recommendations here.",
|
|
2365
|
+
`Record whether ${STATE_ROOT_NAME}/guides/code-consistency.md needs an update.`
|
|
2366
|
+
];
|
|
2367
|
+
function findPlaceholderMatches(text, placeholders) {
|
|
2368
|
+
return placeholders.filter((placeholder) => text.includes(placeholder));
|
|
2369
|
+
}
|
|
2370
|
+
function validatePlaceholderArtifact(root, check) {
|
|
2371
|
+
if (check.relativePath === null) {
|
|
2372
|
+
return null;
|
|
2373
|
+
}
|
|
2374
|
+
const text = readTextIfPresent(path12.join(root, check.relativePath));
|
|
2375
|
+
if (text === null) {
|
|
2376
|
+
return {
|
|
2377
|
+
level: "fail",
|
|
2378
|
+
label: check.label,
|
|
2379
|
+
detail: `missing artifact: ${check.relativePath}`
|
|
2380
|
+
};
|
|
2381
|
+
}
|
|
2382
|
+
const matches = findPlaceholderMatches(text, check.placeholders);
|
|
2383
|
+
if (matches.length === 0) {
|
|
2384
|
+
return {
|
|
2385
|
+
level: "ok",
|
|
2386
|
+
label: check.label,
|
|
2387
|
+
detail: "no scaffold placeholders detected"
|
|
2388
|
+
};
|
|
2389
|
+
}
|
|
2390
|
+
return {
|
|
2391
|
+
level: "fail",
|
|
2392
|
+
label: check.label,
|
|
2393
|
+
detail: `contains placeholder content: ${matches.join("; ")}`
|
|
2394
|
+
};
|
|
2395
|
+
}
|
|
2396
|
+
function pushExistenceChecks(root, items, labelPrefix, results) {
|
|
2397
|
+
for (const item of items) {
|
|
2398
|
+
results.push(
|
|
2399
|
+
existsSync5(path12.join(root, item)) ? {
|
|
2400
|
+
level: "ok",
|
|
2401
|
+
label: `${labelPrefix}: ${item}`,
|
|
2402
|
+
detail: "present"
|
|
2403
|
+
} : {
|
|
2404
|
+
level: "fail",
|
|
2405
|
+
label: `${labelPrefix}: ${item}`,
|
|
2406
|
+
detail: "missing"
|
|
2407
|
+
}
|
|
2408
|
+
);
|
|
2409
|
+
}
|
|
2410
|
+
}
|
|
2411
|
+
function validateAgents(root) {
|
|
2412
|
+
const file = path12.join(root, "AGENTS.md");
|
|
2413
|
+
const text = readTextIfPresent(file);
|
|
2414
|
+
if (text === null) {
|
|
2415
|
+
return [
|
|
2416
|
+
{
|
|
2417
|
+
level: "fail",
|
|
2418
|
+
label: "AGENTS.md",
|
|
2419
|
+
detail: "missing"
|
|
2420
|
+
}
|
|
2421
|
+
];
|
|
2422
|
+
}
|
|
2423
|
+
const snippets = [
|
|
2424
|
+
"fresh agent",
|
|
2425
|
+
"Korean",
|
|
2426
|
+
"code-consistency.md",
|
|
2427
|
+
"work journal",
|
|
2428
|
+
"refactor"
|
|
2429
|
+
];
|
|
2430
|
+
const missing = snippets.filter((snippet) => !text.includes(snippet));
|
|
2431
|
+
if (missing.length === 0) {
|
|
2432
|
+
return [
|
|
2433
|
+
{
|
|
2434
|
+
level: "ok",
|
|
2435
|
+
label: "AGENTS.md",
|
|
2436
|
+
detail: "contains required operational guidance"
|
|
2437
|
+
}
|
|
2438
|
+
];
|
|
2439
|
+
}
|
|
2440
|
+
return [
|
|
2441
|
+
{
|
|
2442
|
+
level: "warn",
|
|
2443
|
+
label: "AGENTS.md",
|
|
2444
|
+
detail: `missing guidance snippets: ${missing.join(", ")}`
|
|
2445
|
+
}
|
|
2446
|
+
];
|
|
2447
|
+
}
|
|
2448
|
+
function validateIndex(root) {
|
|
2449
|
+
const file = path12.join(root, ".sb-codex-tool/index/current.json");
|
|
2450
|
+
const current = readJsonIfPresent(file);
|
|
2451
|
+
if (current === null) {
|
|
2452
|
+
return {
|
|
2453
|
+
level: "fail",
|
|
2454
|
+
label: "index/current.json",
|
|
2455
|
+
detail: "missing or unreadable"
|
|
2456
|
+
};
|
|
2457
|
+
}
|
|
2458
|
+
const requiredKeys = [
|
|
2459
|
+
"currentStage",
|
|
2460
|
+
"nextAction",
|
|
2461
|
+
"hotPath",
|
|
2462
|
+
"activeAgents"
|
|
2463
|
+
];
|
|
2464
|
+
const missing = requiredKeys.filter((key) => !(key in current));
|
|
2465
|
+
if (missing.length > 0) {
|
|
2466
|
+
return {
|
|
2467
|
+
level: "warn",
|
|
2468
|
+
label: "index/current.json",
|
|
2469
|
+
detail: `missing keys: ${missing.join(", ")}`
|
|
2470
|
+
};
|
|
2471
|
+
}
|
|
2472
|
+
return {
|
|
2473
|
+
level: "ok",
|
|
2474
|
+
label: "index/current.json",
|
|
2475
|
+
detail: "structured current state is present"
|
|
2476
|
+
};
|
|
2477
|
+
}
|
|
2478
|
+
function validateConsistencyGuide(root) {
|
|
2479
|
+
const file = path12.join(root, ".sb-codex-tool/guides/code-consistency.md");
|
|
2480
|
+
const text = readTextIfPresent(file);
|
|
2481
|
+
if (text === null) {
|
|
2482
|
+
return {
|
|
2483
|
+
level: "fail",
|
|
2484
|
+
label: "code-consistency.md",
|
|
2485
|
+
detail: "missing"
|
|
2486
|
+
};
|
|
2487
|
+
}
|
|
2488
|
+
const requiredSections = [
|
|
2489
|
+
"Naming Rules",
|
|
2490
|
+
"Module Boundary Rules",
|
|
2491
|
+
"Reuse Rules",
|
|
2492
|
+
"Readability Rules",
|
|
2493
|
+
"Anti-Patterns"
|
|
2494
|
+
];
|
|
2495
|
+
const missing = requiredSections.filter((section) => !text.includes(section));
|
|
2496
|
+
if (missing.length === 0) {
|
|
2497
|
+
return {
|
|
2498
|
+
level: "ok",
|
|
2499
|
+
label: "code-consistency.md",
|
|
2500
|
+
detail: "contains the expected guidance sections"
|
|
2501
|
+
};
|
|
2502
|
+
}
|
|
2503
|
+
return {
|
|
2504
|
+
level: "warn",
|
|
2505
|
+
label: "code-consistency.md",
|
|
2506
|
+
detail: `missing sections: ${missing.join(", ")}`
|
|
2507
|
+
};
|
|
2508
|
+
}
|
|
2509
|
+
function validateIgnoreStrategy(root) {
|
|
2510
|
+
const files = [".ignore", ".rgignore"];
|
|
2511
|
+
const requiredPattern = ".sb-codex-tool/logs/work-journal/";
|
|
2512
|
+
const missing = files.filter((file) => {
|
|
2513
|
+
const text = readTextIfPresent(path12.join(root, file));
|
|
2514
|
+
return text === null || !text.includes(requiredPattern);
|
|
2515
|
+
});
|
|
2516
|
+
if (missing.length === 0) {
|
|
2517
|
+
return {
|
|
2518
|
+
level: "ok",
|
|
2519
|
+
label: "ignore strategy",
|
|
2520
|
+
detail: "default search hot path excludes work-journal noise"
|
|
2521
|
+
};
|
|
2522
|
+
}
|
|
2523
|
+
return {
|
|
2524
|
+
level: "warn",
|
|
2525
|
+
label: "ignore strategy",
|
|
2526
|
+
detail: `missing work-journal exclusion in: ${missing.join(", ")}`
|
|
2527
|
+
};
|
|
2528
|
+
}
|
|
2529
|
+
function validateCurrentReferences(root) {
|
|
2530
|
+
const current = readCurrentIndex(root);
|
|
2531
|
+
if (current === null) {
|
|
2532
|
+
return [];
|
|
2533
|
+
}
|
|
2534
|
+
const referencedPaths = [
|
|
2535
|
+
current.latestApprovedPlan,
|
|
2536
|
+
current.latestRelevantSummary,
|
|
2537
|
+
current.latestRun,
|
|
2538
|
+
current.latestConsistencyReview,
|
|
2539
|
+
current.currentGuide,
|
|
2540
|
+
current.currentHandoff,
|
|
2541
|
+
current.currentReview,
|
|
2542
|
+
...current.hotPath
|
|
2543
|
+
].filter((value) => value !== null);
|
|
2544
|
+
const uniquePaths = referencedPaths.filter(
|
|
2545
|
+
(value, index, array) => array.indexOf(value) === index
|
|
2546
|
+
);
|
|
2547
|
+
return uniquePaths.map(
|
|
2548
|
+
(relativePath) => existsSync5(path12.join(root, relativePath)) ? {
|
|
2549
|
+
level: "ok",
|
|
2550
|
+
label: `current reference: ${relativePath}`,
|
|
2551
|
+
detail: "present"
|
|
2552
|
+
} : {
|
|
2553
|
+
level: "fail",
|
|
2554
|
+
label: `current reference: ${relativePath}`,
|
|
2555
|
+
detail: "missing"
|
|
2556
|
+
}
|
|
2557
|
+
);
|
|
2558
|
+
}
|
|
2559
|
+
function validateReadThisFirst(root) {
|
|
2560
|
+
const current = readCurrentIndex(root);
|
|
2561
|
+
const text = readTextIfPresent(path12.join(root, ".sb-codex-tool/guides/read-this-first.md"));
|
|
2562
|
+
if (current === null || text === null) {
|
|
2563
|
+
return {
|
|
2564
|
+
level: "fail",
|
|
2565
|
+
label: "read-this-first hot path",
|
|
2566
|
+
detail: "missing current index or read-this-first guide"
|
|
2567
|
+
};
|
|
2568
|
+
}
|
|
2569
|
+
const hotPathSection = text.match(
|
|
2570
|
+
/## Hot Path\s+Read in this order before implementation or verification:\s+([\s\S]*?)\n## Additional Repo Docs/
|
|
2571
|
+
);
|
|
2572
|
+
if (hotPathSection === null) {
|
|
2573
|
+
return {
|
|
2574
|
+
level: "fail",
|
|
2575
|
+
label: "read-this-first hot path",
|
|
2576
|
+
detail: "missing hot-path section structure"
|
|
2577
|
+
};
|
|
2578
|
+
}
|
|
2579
|
+
const actualHotPath = hotPathSection[1].trim().split("\n").map((line) => line.trim()).filter((line) => /^\d+\.\s+/.test(line)).map((line) => line.replace(/^\d+\.\s+/, ""));
|
|
2580
|
+
const missing = current.hotPath.filter((relativePath) => !text.includes(relativePath));
|
|
2581
|
+
if (missing.length === 0) {
|
|
2582
|
+
const orderMatches = actualHotPath.length === current.hotPath.length && actualHotPath.every((value, index) => value === current.hotPath[index]);
|
|
2583
|
+
if (!orderMatches) {
|
|
2584
|
+
return {
|
|
2585
|
+
level: "fail",
|
|
2586
|
+
label: "read-this-first hot path",
|
|
2587
|
+
detail: "hot-path order does not match current state"
|
|
2588
|
+
};
|
|
2589
|
+
}
|
|
2590
|
+
const mismatches = [];
|
|
2591
|
+
const cycleChecks = [
|
|
2592
|
+
[`Current stage: ${current.currentStage}`, "current stage"],
|
|
2593
|
+
[
|
|
2594
|
+
`Latest approved plan: ${current.latestApprovedPlan ?? "none yet"}`,
|
|
2595
|
+
"latest approved plan"
|
|
2596
|
+
],
|
|
2597
|
+
[
|
|
2598
|
+
`Latest relevant summary: ${current.latestRelevantSummary ?? "none yet"}`,
|
|
2599
|
+
"latest relevant summary"
|
|
2600
|
+
],
|
|
2601
|
+
[
|
|
2602
|
+
`Latest lifecycle run: ${current.latestRun ?? "none yet"}`,
|
|
2603
|
+
"latest lifecycle run"
|
|
2604
|
+
],
|
|
2605
|
+
[
|
|
2606
|
+
`Current task guide: ${current.currentGuide ?? "none yet"}`,
|
|
2607
|
+
"current task guide"
|
|
2608
|
+
],
|
|
2609
|
+
[
|
|
2610
|
+
`Current handoff: ${current.currentHandoff ?? "none yet"}`,
|
|
2611
|
+
"current handoff"
|
|
2612
|
+
],
|
|
2613
|
+
[
|
|
2614
|
+
`Current review: ${current.currentReview ?? "none yet"}`,
|
|
2615
|
+
"current review"
|
|
2616
|
+
]
|
|
2617
|
+
];
|
|
2618
|
+
for (const [expected, label] of cycleChecks) {
|
|
2619
|
+
if (!text.includes(expected)) {
|
|
2620
|
+
mismatches.push(label);
|
|
2621
|
+
}
|
|
2622
|
+
}
|
|
2623
|
+
if (mismatches.length === 0) {
|
|
2624
|
+
return {
|
|
2625
|
+
level: "ok",
|
|
2626
|
+
label: "read-this-first hot path",
|
|
2627
|
+
detail: "matches the current hot-path references and cycle metadata"
|
|
2628
|
+
};
|
|
2629
|
+
}
|
|
2630
|
+
return {
|
|
2631
|
+
level: "fail",
|
|
2632
|
+
label: "read-this-first hot path",
|
|
2633
|
+
detail: `stale current-cycle metadata: ${mismatches.join(", ")}`
|
|
2634
|
+
};
|
|
2635
|
+
}
|
|
2636
|
+
return {
|
|
2637
|
+
level: "fail",
|
|
2638
|
+
label: "read-this-first hot path",
|
|
2639
|
+
detail: `missing current hot-path references: ${missing.join(", ")}`
|
|
2640
|
+
};
|
|
2641
|
+
}
|
|
2642
|
+
function validateCurrentArtifactSemantics(root) {
|
|
2643
|
+
const current = readCurrentIndex(root);
|
|
2644
|
+
if (current === null) {
|
|
2645
|
+
return [];
|
|
2646
|
+
}
|
|
2647
|
+
const checks = [
|
|
2648
|
+
{
|
|
2649
|
+
label: "approved plan readiness",
|
|
2650
|
+
relativePath: current.latestApprovedPlan,
|
|
2651
|
+
placeholders: PLAN_PLACEHOLDERS
|
|
2652
|
+
},
|
|
2653
|
+
{
|
|
2654
|
+
label: "current task guide readiness",
|
|
2655
|
+
relativePath: current.currentGuide,
|
|
2656
|
+
placeholders: GUIDE_PLACEHOLDERS
|
|
2657
|
+
},
|
|
2658
|
+
{
|
|
2659
|
+
label: "current handoff readiness",
|
|
2660
|
+
relativePath: current.currentHandoff,
|
|
2661
|
+
placeholders: HANDOFF_PLACEHOLDERS
|
|
2662
|
+
},
|
|
2663
|
+
{
|
|
2664
|
+
label: "latest consistency review readiness",
|
|
2665
|
+
relativePath: current.latestConsistencyReview,
|
|
2666
|
+
placeholders: CONSISTENCY_REVIEW_PLACEHOLDERS
|
|
2667
|
+
}
|
|
2668
|
+
];
|
|
2669
|
+
if (current.latestRelevantSummary !== null && current.latestRelevantSummary.endsWith("-execution-summary.md")) {
|
|
2670
|
+
checks.push({
|
|
2671
|
+
label: "execution summary readiness",
|
|
2672
|
+
relativePath: current.latestRelevantSummary,
|
|
2673
|
+
placeholders: EXECUTION_SUMMARY_PLACEHOLDERS
|
|
2674
|
+
});
|
|
2675
|
+
}
|
|
2676
|
+
return checks.map((check) => validatePlaceholderArtifact(root, check)).filter((result) => result !== null);
|
|
2677
|
+
}
|
|
2678
|
+
function runDoctor(start = process.cwd()) {
|
|
2679
|
+
const root = resolveProjectRoot(start);
|
|
2680
|
+
const results = [];
|
|
2681
|
+
const current = readCurrentIndex(root);
|
|
2682
|
+
const latestRunRecord = readLifecycleRunRecord(root, current?.latestRun ?? null);
|
|
2683
|
+
pushExistenceChecks(root, REQUIRED_DIRECTORIES, "directory", results);
|
|
2684
|
+
pushExistenceChecks(root, REQUIRED_FILES, "file", results);
|
|
2685
|
+
pushExistenceChecks(root, REQUIRED_WORKFLOW_FILES, "workflow", results);
|
|
2686
|
+
results.push(...validateAgents(root));
|
|
2687
|
+
results.push(validateIndex(root));
|
|
2688
|
+
results.push(validateConsistencyGuide(root));
|
|
2689
|
+
results.push(validateIgnoreStrategy(root));
|
|
2690
|
+
results.push(...validateCurrentReferences(root));
|
|
2691
|
+
results.push(validateReadThisFirst(root));
|
|
2692
|
+
results.push(...validateCurrentArtifactSemantics(root));
|
|
2693
|
+
results.push(...collectStateCoherenceIssues(root, current, latestRunRecord));
|
|
2694
|
+
return { root, results };
|
|
2695
|
+
}
|
|
2696
|
+
|
|
2697
|
+
// src/commands/doctor.ts
|
|
2698
|
+
function formatResult(level) {
|
|
2699
|
+
if (level === "ok") {
|
|
2700
|
+
return "[ok]";
|
|
2701
|
+
}
|
|
2702
|
+
if (level === "warn") {
|
|
2703
|
+
return "[warn]";
|
|
2704
|
+
}
|
|
2705
|
+
return "[fail]";
|
|
2706
|
+
}
|
|
2707
|
+
function runDoctorCommand() {
|
|
2708
|
+
const report = runDoctor();
|
|
2709
|
+
let hasFailure = false;
|
|
2710
|
+
console.log(`Project root: ${report.root}`);
|
|
2711
|
+
console.log("");
|
|
2712
|
+
for (const result of report.results) {
|
|
2713
|
+
console.log(`${formatResult(result.level)} ${result.label}: ${result.detail}`);
|
|
2714
|
+
if (result.level === "fail") {
|
|
2715
|
+
hasFailure = true;
|
|
2716
|
+
}
|
|
2717
|
+
}
|
|
2718
|
+
return hasFailure ? 1 : 0;
|
|
2719
|
+
}
|
|
2720
|
+
|
|
2721
|
+
// src/lib/launch.ts
|
|
2722
|
+
import { spawnSync as spawnSync2 } from "node:child_process";
|
|
2723
|
+
import { existsSync as existsSync7, writeFileSync as writeFileSync2 } from "node:fs";
|
|
2724
|
+
import path14 from "node:path";
|
|
2725
|
+
|
|
2726
|
+
// src/lib/codex.ts
|
|
2727
|
+
import { existsSync as existsSync6 } from "node:fs";
|
|
2728
|
+
import path13 from "node:path";
|
|
2729
|
+
function findExecutableInPath(binaryName, searchPath) {
|
|
2730
|
+
for (const entry of searchPath.split(path13.delimiter)) {
|
|
2731
|
+
if (entry.trim().length === 0) {
|
|
2732
|
+
continue;
|
|
2733
|
+
}
|
|
2734
|
+
const candidate = path13.join(entry, binaryName);
|
|
2735
|
+
if (existsSync6(candidate)) {
|
|
2736
|
+
return candidate;
|
|
2737
|
+
}
|
|
2738
|
+
}
|
|
2739
|
+
return null;
|
|
2740
|
+
}
|
|
2741
|
+
function resolveCodexBinary(root = process.cwd(), searchPath = process.env.PATH ?? "") {
|
|
2742
|
+
const localBinary = path13.join(root, "node_modules", ".bin", "codex");
|
|
2743
|
+
if (existsSync6(localBinary)) {
|
|
2744
|
+
return localBinary;
|
|
2745
|
+
}
|
|
2746
|
+
return findExecutableInPath("codex", searchPath);
|
|
2747
|
+
}
|
|
2748
|
+
|
|
2749
|
+
// src/lib/launch.ts
|
|
2750
|
+
function getInstructionSurface(hotPath) {
|
|
2751
|
+
return ["AGENTS.md", ...hotPath].filter(
|
|
2752
|
+
(value, index, array) => array.indexOf(value) === index
|
|
2753
|
+
);
|
|
2754
|
+
}
|
|
2755
|
+
function writeInstructionSurfaceFile(root, instructionSurface) {
|
|
2756
|
+
const relativePath = ".sb-codex-tool/index/current-launch-instructions.txt";
|
|
2757
|
+
const absolutePath = path14.join(root, relativePath);
|
|
2758
|
+
ensureDir(path14.dirname(absolutePath));
|
|
2759
|
+
writeFileSync2(absolutePath, instructionSurface.join("\n") + "\n", "utf8");
|
|
2760
|
+
return relativePath;
|
|
2761
|
+
}
|
|
2762
|
+
function writeLaunchMetadata(root, relativePath, metadata) {
|
|
2763
|
+
const absolutePath = path14.join(root, relativePath);
|
|
2764
|
+
ensureDir(path14.dirname(absolutePath));
|
|
2765
|
+
writeFileSync2(absolutePath, JSON.stringify(metadata, null, 2) + "\n", "utf8");
|
|
2766
|
+
}
|
|
2767
|
+
function recordLaunchStart(root, args, current, codexBinary, instructionSurface, instructionSurfaceFile, missingHotPath) {
|
|
2768
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:]/g, "-");
|
|
2769
|
+
const relativePath = `.sb-codex-tool/runs/launch-${timestamp}.json`;
|
|
2770
|
+
const metadata = {
|
|
2771
|
+
version: 2,
|
|
2772
|
+
launchedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2773
|
+
completedAt: null,
|
|
2774
|
+
cwd: root,
|
|
2775
|
+
args,
|
|
2776
|
+
codexBinary,
|
|
2777
|
+
instructionSurface,
|
|
2778
|
+
instructionSurfaceFile,
|
|
2779
|
+
currentStage: current?.currentStage ?? "unknown",
|
|
2780
|
+
latestPlan: current?.latestApprovedPlan ?? null,
|
|
2781
|
+
latestSummary: current?.latestRelevantSummary ?? null,
|
|
2782
|
+
latestRun: current?.latestRun ?? null,
|
|
2783
|
+
preflight: {
|
|
2784
|
+
missingHotPath,
|
|
2785
|
+
scaffoldPresent: true
|
|
2786
|
+
},
|
|
2787
|
+
exitStatus: null,
|
|
2788
|
+
failedReason: null
|
|
2789
|
+
};
|
|
2790
|
+
writeLaunchMetadata(root, relativePath, metadata);
|
|
2791
|
+
return { relativePath, metadata };
|
|
2792
|
+
}
|
|
2793
|
+
function finalizeLaunchMetadata(root, relativePath, metadata, exitStatus, failedReason) {
|
|
2794
|
+
writeLaunchMetadata(root, relativePath, {
|
|
2795
|
+
...metadata,
|
|
2796
|
+
completedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2797
|
+
exitStatus,
|
|
2798
|
+
failedReason
|
|
2799
|
+
});
|
|
2800
|
+
}
|
|
2801
|
+
function launchCodex(start, args, env = process.env, spawnImpl = spawnSync2) {
|
|
2802
|
+
const root = resolveProjectRoot(start);
|
|
2803
|
+
const stateRoot = statePath(root);
|
|
2804
|
+
if (!existsSync7(stateRoot)) {
|
|
2805
|
+
throw new Error("Missing .sb-codex-tool scaffold. Run `sb-codex-tool setup` first.");
|
|
2806
|
+
}
|
|
2807
|
+
const current = readJsonIfPresent(statePath(root, "index", "current.json"));
|
|
2808
|
+
const instructionSurface = getInstructionSurface(current?.hotPath ?? []);
|
|
2809
|
+
const missingHotPath = instructionSurface.filter((file) => !existsSync7(path14.join(root, file)));
|
|
2810
|
+
const instructionSurfaceFile = writeInstructionSurfaceFile(root, instructionSurface);
|
|
2811
|
+
const codexBinary = resolveCodexBinary(root, env.PATH ?? "");
|
|
2812
|
+
const launch = recordLaunchStart(
|
|
2813
|
+
root,
|
|
2814
|
+
args,
|
|
2815
|
+
current,
|
|
2816
|
+
codexBinary,
|
|
2817
|
+
instructionSurface,
|
|
2818
|
+
instructionSurfaceFile,
|
|
2819
|
+
missingHotPath
|
|
2820
|
+
);
|
|
2821
|
+
if (missingHotPath.length > 0) {
|
|
2822
|
+
finalizeLaunchMetadata(
|
|
2823
|
+
root,
|
|
2824
|
+
launch.relativePath,
|
|
2825
|
+
launch.metadata,
|
|
2826
|
+
1,
|
|
2827
|
+
`missing hot-path files: ${missingHotPath.join(", ")}`
|
|
2828
|
+
);
|
|
2829
|
+
throw new Error(
|
|
2830
|
+
`Launch preflight failed because hot-path files are missing: ${missingHotPath.join(", ")}.`
|
|
2831
|
+
);
|
|
2832
|
+
}
|
|
2833
|
+
if (codexBinary === null) {
|
|
2834
|
+
finalizeLaunchMetadata(
|
|
2835
|
+
root,
|
|
2836
|
+
launch.relativePath,
|
|
2837
|
+
launch.metadata,
|
|
2838
|
+
1,
|
|
2839
|
+
"Codex binary not found in PATH or node_modules/.bin."
|
|
2840
|
+
);
|
|
2841
|
+
throw new Error("Codex binary not found in PATH. Install or expose `codex` first.");
|
|
2842
|
+
}
|
|
2843
|
+
const result = spawnImpl(codexBinary, args, {
|
|
2844
|
+
cwd: root,
|
|
2845
|
+
env: {
|
|
2846
|
+
...env,
|
|
2847
|
+
SB_CODEX_TOOL_PROJECT_INSTRUCTIONS: instructionSurface.join("\n"),
|
|
2848
|
+
SB_CODEX_TOOL_PROJECT_INSTRUCTIONS_FILE: instructionSurfaceFile,
|
|
2849
|
+
SB_CODEX_TOOL_CURRENT_STAGE: current?.currentStage ?? "unknown",
|
|
2850
|
+
SB_CODEX_TOOL_LATEST_PLAN: current?.latestApprovedPlan ?? "",
|
|
2851
|
+
SB_CODEX_TOOL_LATEST_SUMMARY: current?.latestRelevantSummary ?? "",
|
|
2852
|
+
SB_CODEX_TOOL_LATEST_RUN: current?.latestRun ?? ""
|
|
2853
|
+
},
|
|
2854
|
+
stdio: "inherit"
|
|
2855
|
+
});
|
|
2856
|
+
finalizeLaunchMetadata(
|
|
2857
|
+
root,
|
|
2858
|
+
launch.relativePath,
|
|
2859
|
+
launch.metadata,
|
|
2860
|
+
result.status ?? 1,
|
|
2861
|
+
result.error?.message ?? null
|
|
2862
|
+
);
|
|
2863
|
+
return {
|
|
2864
|
+
root,
|
|
2865
|
+
launchFile: launch.relativePath,
|
|
2866
|
+
instructionSurface,
|
|
2867
|
+
instructionSurfaceFile,
|
|
2868
|
+
exitStatus: result.status ?? 1
|
|
2869
|
+
};
|
|
2870
|
+
}
|
|
2871
|
+
|
|
2872
|
+
// src/commands/launch.ts
|
|
2873
|
+
function runLaunch(args) {
|
|
2874
|
+
try {
|
|
2875
|
+
const result = launchCodex(process.cwd(), args);
|
|
2876
|
+
console.log(`Launch metadata: ${result.launchFile}`);
|
|
2877
|
+
console.log(`Instruction surface file: ${result.instructionSurfaceFile}`);
|
|
2878
|
+
console.log("Instruction surface for this session:");
|
|
2879
|
+
for (const file of result.instructionSurface) {
|
|
2880
|
+
console.log(`- ${file}`);
|
|
2881
|
+
}
|
|
2882
|
+
return result.exitStatus;
|
|
2883
|
+
} catch (error) {
|
|
2884
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
2885
|
+
return 1;
|
|
2886
|
+
}
|
|
2887
|
+
}
|
|
2888
|
+
|
|
2889
|
+
// src/lib/prepare-verify.ts
|
|
2890
|
+
import path15 from "node:path";
|
|
2891
|
+
function readSummaryContext2(root, summaryPath) {
|
|
2892
|
+
const text = readTextIfPresent(path15.join(root, summaryPath));
|
|
2893
|
+
if (text === null) {
|
|
2894
|
+
throw new Error(`prepare-verify requires the execution summary ${summaryPath}.`);
|
|
2895
|
+
}
|
|
2896
|
+
return {
|
|
2897
|
+
scope: extractSectionLines(text, "Scope"),
|
|
2898
|
+
implementedSurface: extractSectionLines(text, "Implemented Surface"),
|
|
2899
|
+
deferredIssues: extractSectionLines(text, "Deferred Issues"),
|
|
2900
|
+
nextAgentGuidance: extractSectionLines(text, "Next-Agent Guidance")
|
|
2901
|
+
};
|
|
2902
|
+
}
|
|
2903
|
+
function readVerificationExpectations(root, guidePath) {
|
|
2904
|
+
const text = readTextIfPresent(path15.join(root, guidePath));
|
|
2905
|
+
if (text === null) {
|
|
2906
|
+
throw new Error(`prepare-verify requires the current guide ${guidePath}.`);
|
|
2907
|
+
}
|
|
2908
|
+
return extractSectionLines(text, "Verification Expectations");
|
|
2909
|
+
}
|
|
2910
|
+
function ensureExplicitLines(lines, label) {
|
|
2911
|
+
const values = stripBulletPrefix(lines).map((line) => line.trim()).filter(
|
|
2912
|
+
(line) => line.length > 0 && line !== "none" && line !== "none." && line !== "not started yet" && line !== "none yet"
|
|
2913
|
+
);
|
|
2914
|
+
if (values.length === 0) {
|
|
2915
|
+
throw new Error(`prepare-verify requires explicit ${label} before verify can start.`);
|
|
2916
|
+
}
|
|
2917
|
+
return values;
|
|
2918
|
+
}
|
|
2919
|
+
function renderBulletList3(items) {
|
|
2920
|
+
return items.map((item) => `- ${item}`).join("\n");
|
|
2921
|
+
}
|
|
2922
|
+
function buildHandoff(title, summaryContext, verificationExpectations, summaryPath, planPath, guidePath, runPath, gitContextSection) {
|
|
2923
|
+
const implementedSurface = ensureExplicitLines(
|
|
2924
|
+
summaryContext.implementedSurface,
|
|
2925
|
+
"implemented surface details"
|
|
2926
|
+
);
|
|
2927
|
+
const nextAgentGuidance = ensureExplicitLines(
|
|
2928
|
+
summaryContext.nextAgentGuidance,
|
|
2929
|
+
"next-agent guidance"
|
|
2930
|
+
);
|
|
2931
|
+
const checks = ensureExplicitLines(
|
|
2932
|
+
verificationExpectations,
|
|
2933
|
+
"verification expectations"
|
|
2934
|
+
);
|
|
2935
|
+
const scope = stripBulletPrefix(summaryContext.scope).filter((line) => line.trim().length > 0);
|
|
2936
|
+
const risks = stripBulletPrefix(summaryContext.deferredIssues).filter((line) => line.trim().length > 0 && line.trim().toLowerCase() !== "none.");
|
|
2937
|
+
return `# Handoff: ${title}
|
|
2938
|
+
|
|
2939
|
+
## Goal
|
|
2940
|
+
|
|
2941
|
+
- Enable the next fresh agent to continue this work cycle without hidden context.
|
|
2942
|
+
|
|
2943
|
+
## Read In This Order
|
|
2944
|
+
|
|
2945
|
+
1. AGENTS.md
|
|
2946
|
+
2. ${STATE_ROOT_NAME}/project.md
|
|
2947
|
+
3. ${STATE_ROOT_NAME}/state.md
|
|
2948
|
+
4. ${STATE_ROOT_NAME}/guides/read-this-first.md
|
|
2949
|
+
5. ${STATE_ROOT_NAME}/guides/code-consistency.md
|
|
2950
|
+
6. ${summaryPath}
|
|
2951
|
+
7. ${planPath}
|
|
2952
|
+
8. ${guidePath}
|
|
2953
|
+
|
|
2954
|
+
## Current Status
|
|
2955
|
+
|
|
2956
|
+
${renderBulletList3([
|
|
2957
|
+
...scope,
|
|
2958
|
+
...implementedSurface,
|
|
2959
|
+
"Implementation and refactor are ready for fresh verification."
|
|
2960
|
+
])}
|
|
2961
|
+
|
|
2962
|
+
${gitContextSection}
|
|
2963
|
+
## Expected Verification Checks
|
|
2964
|
+
|
|
2965
|
+
${renderBulletList3(checks)}
|
|
2966
|
+
|
|
2967
|
+
## Open Risks
|
|
2968
|
+
|
|
2969
|
+
${renderBulletList3(risks.length > 0 ? risks : ["None."])}
|
|
2970
|
+
|
|
2971
|
+
## Next-Agent Guidance
|
|
2972
|
+
|
|
2973
|
+
${renderBulletList3(nextAgentGuidance)}
|
|
2974
|
+
`;
|
|
2975
|
+
}
|
|
2976
|
+
function prepareVerify(start) {
|
|
2977
|
+
const root = resolveProjectRoot(start);
|
|
2978
|
+
const current = normalizeCurrentIndex(readCurrentIndex(root));
|
|
2979
|
+
if (current.latestApprovedPlan === null) {
|
|
2980
|
+
throw new Error("prepare-verify requires a current approved plan.");
|
|
2981
|
+
}
|
|
2982
|
+
if (current.latestRelevantSummary === null) {
|
|
2983
|
+
throw new Error("prepare-verify requires a current execution summary.");
|
|
2984
|
+
}
|
|
2985
|
+
if (current.currentGuide === null) {
|
|
2986
|
+
throw new Error("prepare-verify requires a current task guide.");
|
|
2987
|
+
}
|
|
2988
|
+
if (current.currentHandoff === null) {
|
|
2989
|
+
throw new Error("prepare-verify requires a current handoff artifact.");
|
|
2990
|
+
}
|
|
2991
|
+
if (current.currentReview === null) {
|
|
2992
|
+
throw new Error("prepare-verify requires a current review artifact.");
|
|
2993
|
+
}
|
|
2994
|
+
const { dateStamp, slug } = parseCycleDescriptor(current.latestApprovedPlan);
|
|
2995
|
+
const title = readCycleTitle(root, current.latestApprovedPlan, slug);
|
|
2996
|
+
const summaryContext = readSummaryContext2(root, current.latestRelevantSummary);
|
|
2997
|
+
const verificationExpectations = readVerificationExpectations(root, current.currentGuide);
|
|
2998
|
+
const git = getGitContext(root);
|
|
2999
|
+
const runWrite = writeLifecycleRunRecord(root, {
|
|
3000
|
+
dateStamp,
|
|
3001
|
+
slug,
|
|
3002
|
+
title,
|
|
3003
|
+
phase: "prepare-verify",
|
|
3004
|
+
stage: "verify",
|
|
3005
|
+
verdict: "pending",
|
|
3006
|
+
git,
|
|
3007
|
+
paths: {
|
|
3008
|
+
planPath: current.latestApprovedPlan,
|
|
3009
|
+
executionSummaryPath: current.latestRelevantSummary,
|
|
3010
|
+
handoffPath: current.currentHandoff,
|
|
3011
|
+
reviewPath: current.currentReview,
|
|
3012
|
+
guidePath: current.currentGuide,
|
|
3013
|
+
verificationSummaryPath: null,
|
|
3014
|
+
journalPath: null
|
|
3015
|
+
}
|
|
3016
|
+
});
|
|
3017
|
+
const handoffWrite = writeFileIfChanged(
|
|
3018
|
+
path15.join(root, current.currentHandoff),
|
|
3019
|
+
buildHandoff(
|
|
3020
|
+
title,
|
|
3021
|
+
summaryContext,
|
|
3022
|
+
verificationExpectations,
|
|
3023
|
+
current.latestRelevantSummary,
|
|
3024
|
+
current.latestApprovedPlan,
|
|
3025
|
+
current.currentGuide,
|
|
3026
|
+
runWrite.path,
|
|
3027
|
+
renderGitContextSection(runWrite.path, git)
|
|
3028
|
+
)
|
|
3029
|
+
);
|
|
3030
|
+
const nextCurrent = normalizeCurrentIndex({
|
|
3031
|
+
...current,
|
|
3032
|
+
currentStage: "verify",
|
|
3033
|
+
nextAction: `Run fresh verification for the ${title} increment.`,
|
|
3034
|
+
latestRun: runWrite.path,
|
|
3035
|
+
currentFocusModules: [
|
|
3036
|
+
"AGENTS.md",
|
|
3037
|
+
`${STATE_ROOT_NAME}/guides/code-consistency.md`,
|
|
3038
|
+
current.latestRelevantSummary,
|
|
3039
|
+
current.latestApprovedPlan,
|
|
3040
|
+
current.currentGuide,
|
|
3041
|
+
current.currentHandoff,
|
|
3042
|
+
current.currentReview,
|
|
3043
|
+
runWrite.path
|
|
3044
|
+
],
|
|
3045
|
+
hotPath: [
|
|
3046
|
+
`${STATE_ROOT_NAME}/project.md`,
|
|
3047
|
+
`${STATE_ROOT_NAME}/state.md`,
|
|
3048
|
+
`${STATE_ROOT_NAME}/guides/read-this-first.md`,
|
|
3049
|
+
`${STATE_ROOT_NAME}/guides/code-consistency.md`,
|
|
3050
|
+
current.latestRelevantSummary,
|
|
3051
|
+
current.latestApprovedPlan,
|
|
3052
|
+
current.currentGuide
|
|
3053
|
+
],
|
|
3054
|
+
activeAgents: {
|
|
3055
|
+
...current.activeAgents,
|
|
3056
|
+
verification: "pending assignment"
|
|
3057
|
+
},
|
|
3058
|
+
notes: [
|
|
3059
|
+
`Current work cycle: ${title}.`,
|
|
3060
|
+
"Execution and refactor are complete.",
|
|
3061
|
+
"Handoff and current-state artifacts are aligned for fresh verification.",
|
|
3062
|
+
`Previous relevant summary: ${current.latestRelevantSummary}`
|
|
3063
|
+
]
|
|
3064
|
+
});
|
|
3065
|
+
const updatedFiles = [];
|
|
3066
|
+
if (runWrite.writeResult === "created" || runWrite.writeResult === "updated") {
|
|
3067
|
+
updatedFiles.push(runWrite.path);
|
|
3068
|
+
}
|
|
3069
|
+
if (handoffWrite === "created" || handoffWrite === "updated") {
|
|
3070
|
+
updatedFiles.push(current.currentHandoff);
|
|
3071
|
+
}
|
|
3072
|
+
const currentWrites = writeCurrentArtifacts(
|
|
3073
|
+
root,
|
|
3074
|
+
nextCurrent,
|
|
3075
|
+
buildTemplateContext(root).implementationMenuPath
|
|
3076
|
+
);
|
|
3077
|
+
if (currentWrites.index !== "unchanged") {
|
|
3078
|
+
updatedFiles.push(`${STATE_ROOT_NAME}/index/current.json`);
|
|
3079
|
+
}
|
|
3080
|
+
if (currentWrites.state !== "unchanged") {
|
|
3081
|
+
updatedFiles.push(`${STATE_ROOT_NAME}/state.md`);
|
|
3082
|
+
}
|
|
3083
|
+
if (currentWrites.readThisFirst !== "unchanged") {
|
|
3084
|
+
updatedFiles.push(`${STATE_ROOT_NAME}/guides/read-this-first.md`);
|
|
3085
|
+
}
|
|
3086
|
+
return {
|
|
3087
|
+
root,
|
|
3088
|
+
title,
|
|
3089
|
+
runPath: runWrite.path,
|
|
3090
|
+
handoffPath: current.currentHandoff,
|
|
3091
|
+
updatedFiles
|
|
3092
|
+
};
|
|
3093
|
+
}
|
|
3094
|
+
|
|
3095
|
+
// src/commands/prepare-verify.ts
|
|
3096
|
+
function runPrepareVerify(args) {
|
|
3097
|
+
if (args.length > 0) {
|
|
3098
|
+
console.error("Usage: sb-codex-tool prepare-verify");
|
|
3099
|
+
return 1;
|
|
3100
|
+
}
|
|
3101
|
+
const result = prepareVerify(process.cwd());
|
|
3102
|
+
console.log(`Project root: ${result.root}`);
|
|
3103
|
+
console.log(`Work cycle: ${result.title}`);
|
|
3104
|
+
console.log(`Updated run: ${result.runPath}`);
|
|
3105
|
+
console.log(`Updated handoff: ${result.handoffPath}`);
|
|
3106
|
+
console.log("");
|
|
3107
|
+
console.log(`Updated files: ${result.updatedFiles.length}`);
|
|
3108
|
+
for (const file of result.updatedFiles) {
|
|
3109
|
+
console.log(`- ${file}`);
|
|
3110
|
+
}
|
|
3111
|
+
return 0;
|
|
3112
|
+
}
|
|
3113
|
+
|
|
3114
|
+
// src/lib/consistency-review.ts
|
|
3115
|
+
import path16 from "node:path";
|
|
3116
|
+
function readAllowedFileScope3(root, guidePath) {
|
|
3117
|
+
if (guidePath === null) {
|
|
3118
|
+
return [];
|
|
3119
|
+
}
|
|
3120
|
+
const text = readTextIfPresent(path16.join(root, guidePath));
|
|
3121
|
+
if (text === null) {
|
|
3122
|
+
return [];
|
|
3123
|
+
}
|
|
3124
|
+
return extractSectionLines(text, "Allowed File Scope");
|
|
3125
|
+
}
|
|
3126
|
+
function buildConsistencyReview(agentName, title, current, allowedFileScope) {
|
|
3127
|
+
const fileScope = allowedFileScope.length > 0 ? allowedFileScope.join("\n") : "- Replace with the bounded file scope for this review.";
|
|
3128
|
+
return `# Code Consistency Review: ${title}
|
|
3129
|
+
|
|
3130
|
+
## Assigned Consistency Agent
|
|
3131
|
+
|
|
3132
|
+
- ${agentName}
|
|
3133
|
+
|
|
3134
|
+
## Review Target
|
|
3135
|
+
|
|
3136
|
+
- Current plan: ${current.latestApprovedPlan ?? "none yet"}
|
|
3137
|
+
- Latest summary: ${current.latestRelevantSummary ?? "none yet"}
|
|
3138
|
+
- Current guide: ${current.currentGuide ?? "none yet"}
|
|
3139
|
+
- Latest lifecycle run: ${current.latestRun ?? "none yet"}
|
|
3140
|
+
|
|
3141
|
+
## Review Scope
|
|
3142
|
+
|
|
3143
|
+
${fileScope}
|
|
3144
|
+
|
|
3145
|
+
## Required References
|
|
3146
|
+
|
|
3147
|
+
- AGENTS.md
|
|
3148
|
+
- ${STATE_ROOT_NAME}/guides/code-consistency.md
|
|
3149
|
+
- ${current.latestApprovedPlan ?? `${STATE_ROOT_NAME}/plans/README.md`}
|
|
3150
|
+
- ${current.latestRelevantSummary ?? `${STATE_ROOT_NAME}/summaries/README.md`}
|
|
3151
|
+
- ${current.currentGuide ?? `${STATE_ROOT_NAME}/guides/read-this-first.md`}
|
|
3152
|
+
|
|
3153
|
+
## Review Questions
|
|
3154
|
+
|
|
3155
|
+
- Do naming and module boundaries still match the consistency guide?
|
|
3156
|
+
- Did the latest increment preserve reuse, readability, and low complexity?
|
|
3157
|
+
- Are there new anti-patterns or consistency drifts that should be recorded?
|
|
3158
|
+
- Is the code still easy for a fresh agent to read and modify?
|
|
3159
|
+
|
|
3160
|
+
## Findings
|
|
3161
|
+
|
|
3162
|
+
- Add findings here in severity order.
|
|
3163
|
+
|
|
3164
|
+
## Recommendations
|
|
3165
|
+
|
|
3166
|
+
- Add follow-up recommendations here.
|
|
3167
|
+
|
|
3168
|
+
## Guide Update Decision
|
|
3169
|
+
|
|
3170
|
+
- Record whether ${STATE_ROOT_NAME}/guides/code-consistency.md needs an update.
|
|
3171
|
+
`;
|
|
3172
|
+
}
|
|
3173
|
+
function createConsistencyReview(start, agentName, requestedTitle) {
|
|
3174
|
+
const root = resolveProjectRoot(start);
|
|
3175
|
+
const current = normalizeCurrentIndex(readCurrentIndex(root));
|
|
3176
|
+
if (current.latestApprovedPlan === null) {
|
|
3177
|
+
throw new Error("review-consistency requires a current approved plan.");
|
|
3178
|
+
}
|
|
3179
|
+
if (current.latestRelevantSummary === null) {
|
|
3180
|
+
throw new Error("review-consistency requires a current summary.");
|
|
3181
|
+
}
|
|
3182
|
+
if (current.currentGuide === null) {
|
|
3183
|
+
throw new Error("review-consistency requires a current task guide.");
|
|
3184
|
+
}
|
|
3185
|
+
const agentSlug = normalizeSlug(agentName);
|
|
3186
|
+
if (agentSlug.length === 0) {
|
|
3187
|
+
throw new Error("Agent name must contain at least one alphanumeric character.");
|
|
3188
|
+
}
|
|
3189
|
+
const { dateStamp, slug } = parseCycleDescriptor(current.latestApprovedPlan);
|
|
3190
|
+
const cycleTitle = readCycleTitle(root, current.latestApprovedPlan, slug);
|
|
3191
|
+
const title = requestedTitle?.trim().length ? requestedTitle.trim() : `${cycleTitle} Consistency Review`;
|
|
3192
|
+
const reviewPath = `${STATE_ROOT_NAME}/reviews/${dateStamp}-${slug}-${agentSlug}-consistency-review.md`;
|
|
3193
|
+
const allowedFileScope = readAllowedFileScope3(root, current.currentGuide);
|
|
3194
|
+
const writeResult = writeFileIfMissing(
|
|
3195
|
+
path16.join(root, reviewPath),
|
|
3196
|
+
buildConsistencyReview(agentName, title, current, allowedFileScope)
|
|
3197
|
+
);
|
|
3198
|
+
const createdFiles = [];
|
|
3199
|
+
const keptFiles = [];
|
|
3200
|
+
if (writeResult === "created") {
|
|
3201
|
+
createdFiles.push(reviewPath);
|
|
3202
|
+
} else {
|
|
3203
|
+
keptFiles.push(reviewPath);
|
|
3204
|
+
}
|
|
3205
|
+
const nextFocusModules = current.currentFocusModules.includes(reviewPath) ? current.currentFocusModules : [...current.currentFocusModules, reviewPath];
|
|
3206
|
+
const nextCurrent = normalizeCurrentIndex({
|
|
3207
|
+
...current,
|
|
3208
|
+
latestConsistencyReview: reviewPath,
|
|
3209
|
+
currentFocusModules: nextFocusModules,
|
|
3210
|
+
activeAgents: {
|
|
3211
|
+
...current.activeAgents,
|
|
3212
|
+
consistency: agentName
|
|
3213
|
+
},
|
|
3214
|
+
notes: [
|
|
3215
|
+
`Current work cycle: ${humanizeSlug(slug)}.`,
|
|
3216
|
+
`Prepared consistency review for ${agentName}: ${reviewPath}.`,
|
|
3217
|
+
`Previous relevant summary: ${current.latestRelevantSummary}`
|
|
3218
|
+
]
|
|
3219
|
+
});
|
|
3220
|
+
const updatedFiles = [];
|
|
3221
|
+
const currentWrites = writeCurrentArtifacts(
|
|
3222
|
+
root,
|
|
3223
|
+
nextCurrent,
|
|
3224
|
+
buildTemplateContext(root).implementationMenuPath
|
|
3225
|
+
);
|
|
3226
|
+
if (currentWrites.index !== "unchanged") {
|
|
3227
|
+
updatedFiles.push(`${STATE_ROOT_NAME}/index/current.json`);
|
|
3228
|
+
}
|
|
3229
|
+
if (currentWrites.state !== "unchanged") {
|
|
3230
|
+
updatedFiles.push(`${STATE_ROOT_NAME}/state.md`);
|
|
3231
|
+
}
|
|
3232
|
+
if (currentWrites.readThisFirst !== "unchanged") {
|
|
3233
|
+
updatedFiles.push(`${STATE_ROOT_NAME}/guides/read-this-first.md`);
|
|
3234
|
+
}
|
|
3235
|
+
return {
|
|
3236
|
+
root,
|
|
3237
|
+
agentName,
|
|
3238
|
+
title,
|
|
3239
|
+
reviewPath,
|
|
3240
|
+
createdFiles,
|
|
3241
|
+
keptFiles,
|
|
3242
|
+
updatedFiles
|
|
3243
|
+
};
|
|
3244
|
+
}
|
|
3245
|
+
|
|
3246
|
+
// src/commands/review-consistency.ts
|
|
3247
|
+
function runReviewConsistency(args) {
|
|
3248
|
+
const [agentName, ...titleParts] = args;
|
|
3249
|
+
if (agentName === void 0) {
|
|
3250
|
+
console.error("Usage: sb-codex-tool review-consistency <agent-name> [title words]");
|
|
3251
|
+
return 1;
|
|
3252
|
+
}
|
|
3253
|
+
const title = titleParts.length > 0 ? titleParts.join(" ") : void 0;
|
|
3254
|
+
const result = createConsistencyReview(process.cwd(), agentName, title);
|
|
3255
|
+
console.log(`Project root: ${result.root}`);
|
|
3256
|
+
console.log(`Consistency agent: ${result.agentName}`);
|
|
3257
|
+
console.log(`Review: ${result.title}`);
|
|
3258
|
+
console.log("");
|
|
3259
|
+
console.log("Artifacts:");
|
|
3260
|
+
console.log(`- consistency review: ${result.reviewPath}`);
|
|
3261
|
+
console.log("");
|
|
3262
|
+
console.log(`Created files: ${result.createdFiles.length}`);
|
|
3263
|
+
for (const file of result.createdFiles) {
|
|
3264
|
+
console.log(`- ${file}`);
|
|
3265
|
+
}
|
|
3266
|
+
if (result.updatedFiles.length > 0) {
|
|
3267
|
+
console.log("");
|
|
3268
|
+
console.log("Updated state:");
|
|
3269
|
+
for (const file of result.updatedFiles) {
|
|
3270
|
+
console.log(`- ${file}`);
|
|
3271
|
+
}
|
|
3272
|
+
}
|
|
3273
|
+
return 0;
|
|
3274
|
+
}
|
|
3275
|
+
|
|
3276
|
+
// src/lib/scaffold.ts
|
|
3277
|
+
import { existsSync as existsSync8 } from "node:fs";
|
|
3278
|
+
import path17 from "node:path";
|
|
3279
|
+
function ensureDirectories(root) {
|
|
3280
|
+
const created = [];
|
|
3281
|
+
for (const relativeDir of REQUIRED_DIRECTORIES) {
|
|
3282
|
+
const absoluteDir = path17.join(root, relativeDir);
|
|
3283
|
+
const existed = existsSync8(absoluteDir);
|
|
3284
|
+
ensureDir(absoluteDir);
|
|
3285
|
+
if (!existed) {
|
|
3286
|
+
created.push(relativeDir);
|
|
3287
|
+
}
|
|
3288
|
+
}
|
|
3289
|
+
return created;
|
|
3290
|
+
}
|
|
3291
|
+
function scaffoldProject(start = process.cwd()) {
|
|
3292
|
+
const root = resolveProjectRoot(start);
|
|
3293
|
+
const context = buildTemplateContext(root);
|
|
3294
|
+
const createdDirectories = ensureDirectories(root);
|
|
3295
|
+
const createdFiles = [];
|
|
3296
|
+
const keptFiles = [];
|
|
3297
|
+
const updatedFiles = [];
|
|
3298
|
+
for (const file of buildGeneratedFiles(context)) {
|
|
3299
|
+
const result = writeFileIfMissing(path17.join(root, file.path), file.content);
|
|
3300
|
+
if (result === "created") {
|
|
3301
|
+
createdFiles.push(file.path);
|
|
3302
|
+
} else {
|
|
3303
|
+
keptFiles.push(file.path);
|
|
3304
|
+
}
|
|
3305
|
+
}
|
|
3306
|
+
const gitIgnore = ensureLines(
|
|
3307
|
+
path17.join(root, ".gitignore"),
|
|
3308
|
+
getGitIgnoreLines(),
|
|
3309
|
+
"# Added by sb-codex-tool"
|
|
3310
|
+
);
|
|
3311
|
+
if (gitIgnore.created || gitIgnore.added.length > 0) {
|
|
3312
|
+
updatedFiles.push(".gitignore");
|
|
3313
|
+
}
|
|
3314
|
+
const ignore = ensureLines(
|
|
3315
|
+
path17.join(root, ".ignore"),
|
|
3316
|
+
getSearchIgnoreLines(),
|
|
3317
|
+
"# Added by sb-codex-tool"
|
|
3318
|
+
);
|
|
3319
|
+
if (ignore.created || ignore.added.length > 0) {
|
|
3320
|
+
updatedFiles.push(".ignore");
|
|
3321
|
+
}
|
|
3322
|
+
const rgIgnore = ensureLines(
|
|
3323
|
+
path17.join(root, ".rgignore"),
|
|
3324
|
+
getSearchIgnoreLines(),
|
|
3325
|
+
"# Added by sb-codex-tool"
|
|
3326
|
+
);
|
|
3327
|
+
if (rgIgnore.created || rgIgnore.added.length > 0) {
|
|
3328
|
+
updatedFiles.push(".rgignore");
|
|
3329
|
+
}
|
|
3330
|
+
const workflowReadme = writeFileIfMissing(
|
|
3331
|
+
statePath(root, "workflows", "README.md"),
|
|
3332
|
+
"# Workflows\n\nThis directory stores the canonical workflow stage assets.\n"
|
|
3333
|
+
);
|
|
3334
|
+
if (workflowReadme === "created") {
|
|
3335
|
+
createdFiles.push(`${statePath(".", "workflows", "README.md")}`);
|
|
3336
|
+
}
|
|
3337
|
+
return {
|
|
3338
|
+
root,
|
|
3339
|
+
createdFiles,
|
|
3340
|
+
keptFiles,
|
|
3341
|
+
createdDirectories,
|
|
3342
|
+
updatedFiles
|
|
3343
|
+
};
|
|
3344
|
+
}
|
|
3345
|
+
|
|
3346
|
+
// src/commands/setup.ts
|
|
3347
|
+
function runSetup() {
|
|
3348
|
+
const summary = scaffoldProject();
|
|
3349
|
+
const codexBinary = resolveCodexBinary();
|
|
3350
|
+
console.log(`Project root: ${summary.root}`);
|
|
3351
|
+
console.log("");
|
|
3352
|
+
console.log(`Created directories: ${summary.createdDirectories.length}`);
|
|
3353
|
+
for (const directory of summary.createdDirectories) {
|
|
3354
|
+
console.log(`- ${directory}`);
|
|
3355
|
+
}
|
|
3356
|
+
console.log("");
|
|
3357
|
+
console.log(`Created files: ${summary.createdFiles.length}`);
|
|
3358
|
+
for (const file of summary.createdFiles) {
|
|
3359
|
+
console.log(`- ${file}`);
|
|
3360
|
+
}
|
|
3361
|
+
if (summary.updatedFiles.length > 0) {
|
|
3362
|
+
console.log("");
|
|
3363
|
+
console.log("Updated files:");
|
|
3364
|
+
for (const file of summary.updatedFiles) {
|
|
3365
|
+
console.log(`- ${file}`);
|
|
3366
|
+
}
|
|
3367
|
+
}
|
|
3368
|
+
console.log("");
|
|
3369
|
+
if (codexBinary === null) {
|
|
3370
|
+
console.log("Codex binary: missing");
|
|
3371
|
+
console.log("Remediation: install Codex or expose `codex` in PATH before using the launch wrapper.");
|
|
3372
|
+
} else {
|
|
3373
|
+
console.log(`Codex binary: ${codexBinary}`);
|
|
3374
|
+
}
|
|
3375
|
+
return 0;
|
|
3376
|
+
}
|
|
3377
|
+
|
|
3378
|
+
// src/lib/status.ts
|
|
3379
|
+
function getStatus(start = process.cwd()) {
|
|
3380
|
+
const root = resolveProjectRoot(start);
|
|
3381
|
+
const current = readCurrentIndex(root);
|
|
3382
|
+
const git = getGitContext(root);
|
|
3383
|
+
const latestRun = current?.latestRun ?? null;
|
|
3384
|
+
const latestRunRecord = readLifecycleRunRecord(root, latestRun);
|
|
3385
|
+
const coherenceIssues = collectStateCoherenceIssues(root, current, latestRunRecord);
|
|
3386
|
+
return {
|
|
3387
|
+
root,
|
|
3388
|
+
stage: current?.currentStage ?? "unknown",
|
|
3389
|
+
nextAction: current?.nextAction ?? "No next action recorded.",
|
|
3390
|
+
latestPlan: current?.latestApprovedPlan ?? null,
|
|
3391
|
+
latestSummary: current?.latestRelevantSummary ?? null,
|
|
3392
|
+
latestRun,
|
|
3393
|
+
latestRunRecord,
|
|
3394
|
+
latestConsistencyReview: current?.latestConsistencyReview ?? null,
|
|
3395
|
+
latestAssignmentLifecycle: current?.latestAssignmentLifecycle ?? null,
|
|
3396
|
+
currentGuide: current?.currentGuide ?? null,
|
|
3397
|
+
currentReview: current?.currentReview ?? null,
|
|
3398
|
+
currentHandoff: current?.currentHandoff ?? null,
|
|
3399
|
+
hotPath: current?.hotPath ?? [
|
|
3400
|
+
".sb-codex-tool/project.md",
|
|
3401
|
+
".sb-codex-tool/state.md",
|
|
3402
|
+
".sb-codex-tool/guides/read-this-first.md",
|
|
3403
|
+
".sb-codex-tool/guides/code-consistency.md"
|
|
3404
|
+
],
|
|
3405
|
+
agents: current?.activeAgents,
|
|
3406
|
+
assignmentGuides: current?.assignmentGuides ?? {},
|
|
3407
|
+
coherenceIssues,
|
|
3408
|
+
git
|
|
3409
|
+
};
|
|
3410
|
+
}
|
|
3411
|
+
|
|
3412
|
+
// src/commands/status.ts
|
|
3413
|
+
function runStatus() {
|
|
3414
|
+
const status = getStatus();
|
|
3415
|
+
console.log(`Project root: ${status.root}`);
|
|
3416
|
+
console.log(`Current stage: ${status.stage}`);
|
|
3417
|
+
console.log(`Next action: ${status.nextAction}`);
|
|
3418
|
+
console.log(`Latest approved plan: ${status.latestPlan ?? "none"}`);
|
|
3419
|
+
console.log(`Latest summary: ${status.latestSummary ?? "none"}`);
|
|
3420
|
+
console.log(`Latest run: ${status.latestRun ?? "none"}`);
|
|
3421
|
+
console.log(`Latest consistency review: ${status.latestConsistencyReview ?? "none"}`);
|
|
3422
|
+
console.log(`Latest assignment lifecycle: ${status.latestAssignmentLifecycle ?? "none"}`);
|
|
3423
|
+
console.log(`Current guide: ${status.currentGuide ?? "none"}`);
|
|
3424
|
+
console.log(`Current handoff: ${status.currentHandoff ?? "none"}`);
|
|
3425
|
+
console.log(`Current review: ${status.currentReview ?? "none"}`);
|
|
3426
|
+
if (status.latestRunRecord !== null) {
|
|
3427
|
+
console.log("");
|
|
3428
|
+
console.log("Latest run details:");
|
|
3429
|
+
console.log(`- Title: ${status.latestRunRecord.title}`);
|
|
3430
|
+
console.log(`- Phase: ${status.latestRunRecord.phase}`);
|
|
3431
|
+
console.log(`- Stage: ${status.latestRunRecord.stage}`);
|
|
3432
|
+
console.log(`- Verdict: ${status.latestRunRecord.verdict}`);
|
|
3433
|
+
console.log("Run-linked artifacts:");
|
|
3434
|
+
console.log(`- Plan: ${status.latestRunRecord.paths.planPath}`);
|
|
3435
|
+
console.log(`- Execution summary: ${status.latestRunRecord.paths.executionSummaryPath}`);
|
|
3436
|
+
console.log(`- Guide: ${status.latestRunRecord.paths.guidePath ?? "none"}`);
|
|
3437
|
+
console.log(`- Handoff: ${status.latestRunRecord.paths.handoffPath ?? "none"}`);
|
|
3438
|
+
console.log(`- Review: ${status.latestRunRecord.paths.reviewPath ?? "none"}`);
|
|
3439
|
+
console.log(
|
|
3440
|
+
`- Verification summary: ${status.latestRunRecord.paths.verificationSummaryPath ?? "none"}`
|
|
3441
|
+
);
|
|
3442
|
+
console.log(`- Work journal: ${status.latestRunRecord.paths.journalPath ?? "none"}`);
|
|
3443
|
+
console.log("Recorded run git context:");
|
|
3444
|
+
if (status.latestRunRecord.git.available) {
|
|
3445
|
+
console.log(`- Branch: ${status.latestRunRecord.git.branch ?? "detached"}`);
|
|
3446
|
+
console.log(`- Dirty: ${status.latestRunRecord.git.dirty ? "yes" : "no"}`);
|
|
3447
|
+
console.log("- Changed files:");
|
|
3448
|
+
if (status.latestRunRecord.git.changedFiles.length === 0) {
|
|
3449
|
+
console.log(" - none");
|
|
3450
|
+
} else {
|
|
3451
|
+
for (const file of status.latestRunRecord.git.changedFiles) {
|
|
3452
|
+
console.log(` - ${file}`);
|
|
3453
|
+
}
|
|
3454
|
+
}
|
|
3455
|
+
} else {
|
|
3456
|
+
console.log("- Branch: unavailable");
|
|
3457
|
+
console.log("- Dirty: unavailable");
|
|
3458
|
+
console.log("- Changed files: unavailable");
|
|
3459
|
+
}
|
|
3460
|
+
}
|
|
3461
|
+
console.log("");
|
|
3462
|
+
console.log("Hot path:");
|
|
3463
|
+
for (const file of status.hotPath) {
|
|
3464
|
+
console.log(`- ${file}`);
|
|
3465
|
+
}
|
|
3466
|
+
console.log("");
|
|
3467
|
+
console.log("Active agents:");
|
|
3468
|
+
console.log(`- Main: ${status.agents?.main ?? "unassigned"}`);
|
|
3469
|
+
console.log(
|
|
3470
|
+
`- Subagents: ${status.agents?.subagents && status.agents.subagents.length > 0 ? status.agents.subagents.join(", ") : "none"}`
|
|
3471
|
+
);
|
|
3472
|
+
console.log(`- Verification: ${status.agents?.verification ?? "none"}`);
|
|
3473
|
+
console.log(`- Consistency: ${status.agents?.consistency ?? "none"}`);
|
|
3474
|
+
console.log("");
|
|
3475
|
+
console.log("Assignment guides:");
|
|
3476
|
+
const assignmentEntries = Object.entries(status.assignmentGuides);
|
|
3477
|
+
if (assignmentEntries.length === 0) {
|
|
3478
|
+
console.log("- none");
|
|
3479
|
+
} else {
|
|
3480
|
+
for (const [agent, guidePath] of assignmentEntries.sort(([left], [right]) => left.localeCompare(right))) {
|
|
3481
|
+
console.log(`- ${agent}: ${guidePath}`);
|
|
3482
|
+
}
|
|
3483
|
+
}
|
|
3484
|
+
console.log("");
|
|
3485
|
+
console.log("Semantic issues:");
|
|
3486
|
+
if (status.coherenceIssues.length === 0) {
|
|
3487
|
+
console.log("- none");
|
|
3488
|
+
} else {
|
|
3489
|
+
for (const issue of status.coherenceIssues) {
|
|
3490
|
+
console.log(`- [${issue.level}] ${issue.label}: ${issue.detail}`);
|
|
3491
|
+
}
|
|
3492
|
+
}
|
|
3493
|
+
console.log("");
|
|
3494
|
+
if (status.git.available) {
|
|
3495
|
+
console.log(`Git branch: ${status.git.branch ?? "detached"}`);
|
|
3496
|
+
console.log(`Git dirty: ${status.git.dirty ? "yes" : "no"}`);
|
|
3497
|
+
console.log("Changed files:");
|
|
3498
|
+
if (status.git.changedFiles.length === 0) {
|
|
3499
|
+
console.log("- none");
|
|
3500
|
+
} else {
|
|
3501
|
+
for (const file of status.git.changedFiles) {
|
|
3502
|
+
console.log(`- ${file}`);
|
|
3503
|
+
}
|
|
3504
|
+
}
|
|
3505
|
+
} else {
|
|
3506
|
+
console.log("Git branch: unavailable");
|
|
3507
|
+
console.log("Git dirty: unavailable");
|
|
3508
|
+
}
|
|
3509
|
+
return 0;
|
|
3510
|
+
}
|
|
3511
|
+
|
|
3512
|
+
// src/cli.ts
|
|
3513
|
+
function printHelp() {
|
|
3514
|
+
console.log("sb-codex-tool");
|
|
3515
|
+
console.log("");
|
|
3516
|
+
console.log("Usage:");
|
|
3517
|
+
console.log(" sb-codex-tool assign <agent-name> <slug> [title words]");
|
|
3518
|
+
console.log(" sb-codex-tool begin <slug> [title words]");
|
|
3519
|
+
console.log(" sb-codex-tool close");
|
|
3520
|
+
console.log(" sb-codex-tool complete-assignment <agent-name> <close|clear|replace> [replacement-agent] [replacement-slug] [title words]");
|
|
3521
|
+
console.log(" sb-codex-tool prepare-verify");
|
|
3522
|
+
console.log(" sb-codex-tool review-consistency <agent-name> [title words]");
|
|
3523
|
+
console.log(" sb-codex-tool setup");
|
|
3524
|
+
console.log(" sb-codex-tool doctor");
|
|
3525
|
+
console.log(" sb-codex-tool status");
|
|
3526
|
+
console.log(" sb-codex-tool [codex args]");
|
|
3527
|
+
console.log("");
|
|
3528
|
+
console.log("Commands:");
|
|
3529
|
+
console.log(" assign Create a bounded assignment guide and update active subagents.");
|
|
3530
|
+
console.log(" begin Create the next work-cycle artifacts and update current state.");
|
|
3531
|
+
console.log(" close Finalize the current cycle from the recorded review verdict.");
|
|
3532
|
+
console.log(" complete-assignment Record close/clear/replace lifecycle handling for a bounded subagent task.");
|
|
3533
|
+
console.log(" prepare-verify Align handoff, run state, and current state for fresh verification.");
|
|
3534
|
+
console.log(" review-consistency Create a consistency review artifact and update visible review state.");
|
|
3535
|
+
console.log(" setup Create the sb-codex-tool scaffold in the current project.");
|
|
3536
|
+
console.log(" doctor Validate the scaffold and required operational files.");
|
|
3537
|
+
console.log(" status Show current stage, next action, hot path, and git context.");
|
|
3538
|
+
console.log(" help Show this message.");
|
|
3539
|
+
console.log("");
|
|
3540
|
+
console.log("When no command is provided, sb-codex-tool launches Codex through the wrapper.");
|
|
3541
|
+
}
|
|
3542
|
+
function main(argv) {
|
|
3543
|
+
const [command, ...rest] = argv;
|
|
3544
|
+
if (command === void 0) {
|
|
3545
|
+
return runLaunch([]);
|
|
3546
|
+
}
|
|
3547
|
+
if (command === "setup") {
|
|
3548
|
+
return runSetup();
|
|
3549
|
+
}
|
|
3550
|
+
if (command === "begin") {
|
|
3551
|
+
return runBegin(rest);
|
|
3552
|
+
}
|
|
3553
|
+
if (command === "assign") {
|
|
3554
|
+
return runAssign(rest);
|
|
3555
|
+
}
|
|
3556
|
+
if (command === "close") {
|
|
3557
|
+
return runClose(rest);
|
|
3558
|
+
}
|
|
3559
|
+
if (command === "complete-assignment") {
|
|
3560
|
+
return runCompleteAssignment(rest);
|
|
3561
|
+
}
|
|
3562
|
+
if (command === "review-consistency") {
|
|
3563
|
+
return runReviewConsistency(rest);
|
|
3564
|
+
}
|
|
3565
|
+
if (command === "prepare-verify") {
|
|
3566
|
+
return runPrepareVerify(rest);
|
|
3567
|
+
}
|
|
3568
|
+
if (command === "doctor") {
|
|
3569
|
+
return runDoctorCommand();
|
|
3570
|
+
}
|
|
3571
|
+
if (command === "status") {
|
|
3572
|
+
return runStatus();
|
|
3573
|
+
}
|
|
3574
|
+
if (command === "help" || command === "--help" || command === "-h") {
|
|
3575
|
+
printHelp();
|
|
3576
|
+
return 0;
|
|
3577
|
+
}
|
|
3578
|
+
if (command.startsWith("-")) {
|
|
3579
|
+
return runLaunch(argv);
|
|
3580
|
+
}
|
|
3581
|
+
return runLaunch(argv);
|
|
3582
|
+
}
|
|
3583
|
+
process.exit(main(process.argv.slice(2)));
|