superlab 0.1.12 → 0.1.13
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.md +11 -2
- package/README.zh-CN.md +11 -2
- package/bin/superlab.cjs +43 -1
- package/lib/auto.cjs +14 -972
- package/lib/auto_common.cjs +129 -0
- package/lib/auto_contracts.cjs +387 -0
- package/lib/auto_runner.cjs +830 -0
- package/lib/auto_state.cjs +227 -0
- package/lib/context.cjs +94 -0
- package/lib/eval_protocol.cjs +236 -0
- package/lib/i18n.cjs +125 -11
- package/lib/install.cjs +26 -6
- package/package-assets/claude/commands/lab/auto.md +1 -1
- package/package-assets/claude/commands/lab.md +2 -1
- package/package-assets/codex/prompts/lab-auto.md +1 -1
- package/package-assets/codex/prompts/lab.md +2 -1
- package/package-assets/shared/lab/context/auto-mode.md +7 -0
- package/package-assets/shared/lab/context/auto-outcome.md +28 -0
- package/package-assets/shared/lab/context/auto-status.md +3 -0
- package/package-assets/shared/lab/context/eval-protocol.md +46 -0
- package/package-assets/shared/skills/lab/SKILL.md +12 -1
- package/package-assets/shared/skills/lab/stages/auto.md +31 -7
- package/package-assets/shared/skills/lab/stages/iterate.md +4 -0
- package/package-assets/shared/skills/lab/stages/report.md +4 -0
- package/package-assets/shared/skills/lab/stages/run.md +4 -1
- package/package.json +1 -1
package/lib/auto.cjs
CHANGED
|
@@ -1,975 +1,17 @@
|
|
|
1
|
-
const
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
const
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
path.join(".lab", "writing", "framing.md"),
|
|
16
|
-
],
|
|
17
|
-
claims: [path.join(".lab", "context", "terminology-lock.md")],
|
|
18
|
-
"terminology-lock": [path.join(".lab", "context", "terminology-lock.md")],
|
|
19
|
-
};
|
|
20
|
-
const REVIEW_CONTEXT_FILES = [
|
|
21
|
-
path.join(".lab", "context", "decisions.md"),
|
|
22
|
-
path.join(".lab", "context", "state.md"),
|
|
23
|
-
path.join(".lab", "context", "open-questions.md"),
|
|
24
|
-
path.join(".lab", "context", "evidence-index.md"),
|
|
25
|
-
];
|
|
26
|
-
const PROMOTION_CANONICAL_FILES = [
|
|
27
|
-
path.join(".lab", "context", "data-decisions.md"),
|
|
28
|
-
path.join(".lab", "context", "decisions.md"),
|
|
29
|
-
path.join(".lab", "context", "state.md"),
|
|
30
|
-
path.join(".lab", "context", "session-brief.md"),
|
|
31
|
-
];
|
|
32
|
-
|
|
33
|
-
function contextFile(targetDir, name) {
|
|
34
|
-
return path.join(targetDir, ".lab", "context", name);
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
function readFileIfExists(filePath) {
|
|
38
|
-
if (!fs.existsSync(filePath)) {
|
|
39
|
-
return "";
|
|
40
|
-
}
|
|
41
|
-
return fs.readFileSync(filePath, "utf8");
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
function extractValue(text, labels) {
|
|
45
|
-
for (const label of labels) {
|
|
46
|
-
const escaped = label.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
47
|
-
const regex = new RegExp(`^\\s*-\\s*${escaped}:[ \\t]*([^\\n\\r]+?)[ \\t]*$`, "im");
|
|
48
|
-
const match = text.match(regex);
|
|
49
|
-
if (match && match[1]) {
|
|
50
|
-
return match[1].trim();
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
return "";
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
function isMeaningful(value) {
|
|
57
|
-
return !PLACEHOLDER_VALUES.has((value || "").trim().toLowerCase());
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
function normalizeAllowedStages(rawValue) {
|
|
61
|
-
return (rawValue || "")
|
|
62
|
-
.split(",")
|
|
63
|
-
.map((value) => value.trim().toLowerCase())
|
|
64
|
-
.filter(Boolean);
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
function parseAutoMode(targetDir) {
|
|
68
|
-
const text = readFileIfExists(contextFile(targetDir, "auto-mode.md"));
|
|
69
|
-
const allowedStages = normalizeAllowedStages(extractValue(text, ["Allowed stages", "允许阶段"]));
|
|
70
|
-
return {
|
|
71
|
-
path: contextFile(targetDir, "auto-mode.md"),
|
|
72
|
-
text,
|
|
73
|
-
objective: extractValue(text, ["Objective", "目标"]),
|
|
74
|
-
allowedStages,
|
|
75
|
-
successCriteria: extractValue(text, ["Success criteria", "成功标准"]),
|
|
76
|
-
maxIterations: extractValue(text, ["Max iterations", "最大迭代轮次"]),
|
|
77
|
-
maxWallClockTime: extractValue(text, ["Max wall-clock time", "最大运行时长"]),
|
|
78
|
-
maxFailures: extractValue(text, ["Max failures", "最大失败次数"]),
|
|
79
|
-
pollInterval: extractValue(text, ["Poll interval", "轮询间隔"]),
|
|
80
|
-
stageCommands: {
|
|
81
|
-
run: extractValue(text, ["Run command", "运行命令"]),
|
|
82
|
-
iterate: extractValue(text, ["Iterate command", "迭代命令"]),
|
|
83
|
-
review: extractValue(text, ["Review command", "审查命令"]),
|
|
84
|
-
report: extractValue(text, ["Report command", "报告命令"]),
|
|
85
|
-
write: extractValue(text, ["Write command", "写作命令"]),
|
|
86
|
-
},
|
|
87
|
-
successCheckCommand: extractValue(text, ["Success check command", "成功检查命令"]),
|
|
88
|
-
stopCheckCommand: extractValue(text, ["Stop check command", "停止检查命令"]),
|
|
89
|
-
promotionCheckCommand: extractValue(text, ["Promotion check command", "升格检查命令"]),
|
|
90
|
-
promotionCommand: extractValue(text, ["Promotion command", "升格命令"]),
|
|
91
|
-
promotionPolicy: extractValue(text, ["Promotion policy", "升格策略"]),
|
|
92
|
-
frozenCore: extractValue(text, ["Frozen core", "冻结核心"]),
|
|
93
|
-
explorationEnvelope: extractValue(text, ["Exploration envelope", "探索边界"]),
|
|
94
|
-
stopConditions: extractValue(text, ["Stop conditions", "停止条件"]),
|
|
95
|
-
escalationConditions: extractValue(text, ["Escalation conditions", "升级条件"]),
|
|
96
|
-
};
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
function parseInteger(value, fallback = null) {
|
|
100
|
-
const normalized = (value || "").trim();
|
|
101
|
-
if (!normalized) {
|
|
102
|
-
return fallback;
|
|
103
|
-
}
|
|
104
|
-
const parsed = Number.parseInt(normalized, 10);
|
|
105
|
-
return Number.isFinite(parsed) ? parsed : fallback;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
function parseDurationMs(value, fallbackMs) {
|
|
109
|
-
const normalized = (value || "").trim().toLowerCase();
|
|
110
|
-
if (!normalized) {
|
|
111
|
-
return fallbackMs;
|
|
112
|
-
}
|
|
113
|
-
const match = normalized.match(/^(\d+(?:\.\d+)?)(ms|s|m|h)?$/);
|
|
114
|
-
if (!match) {
|
|
115
|
-
return fallbackMs;
|
|
116
|
-
}
|
|
117
|
-
const amount = Number.parseFloat(match[1]);
|
|
118
|
-
const unit = match[2] || "s";
|
|
119
|
-
const multipliers = {
|
|
120
|
-
ms: 1,
|
|
121
|
-
s: 1000,
|
|
122
|
-
m: 60_000,
|
|
123
|
-
h: 3_600_000,
|
|
124
|
-
};
|
|
125
|
-
return Math.max(1, Math.round(amount * multipliers[unit]));
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
function sleep(ms) {
|
|
129
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
function normalizeList(rawValue) {
|
|
133
|
-
return (rawValue || "")
|
|
134
|
-
.split(",")
|
|
135
|
-
.map((value) => value.trim())
|
|
136
|
-
.filter(Boolean);
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
function resolveFrozenCoreEntries(rawValue) {
|
|
140
|
-
const normalized = normalizeList(rawValue);
|
|
141
|
-
const paths = new Set();
|
|
142
|
-
const invalidEntries = [];
|
|
143
|
-
|
|
144
|
-
for (const entry of normalized) {
|
|
145
|
-
const key = entry.trim().toLowerCase();
|
|
146
|
-
if (FROZEN_CORE_ALIASES[key]) {
|
|
147
|
-
for (const aliasedPath of FROZEN_CORE_ALIASES[key]) {
|
|
148
|
-
paths.add(aliasedPath);
|
|
149
|
-
}
|
|
150
|
-
continue;
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
if (entry.includes("/") || entry.includes(path.sep) || entry.endsWith(".md")) {
|
|
154
|
-
paths.add(entry);
|
|
155
|
-
continue;
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
invalidEntries.push(entry);
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
return {
|
|
162
|
-
entries: normalized,
|
|
163
|
-
relativePaths: Array.from(paths),
|
|
164
|
-
invalidEntries,
|
|
165
|
-
};
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
function hashPathState(filePath) {
|
|
169
|
-
if (!fs.existsSync(filePath)) {
|
|
170
|
-
return "__missing__";
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
const stat = fs.statSync(filePath);
|
|
174
|
-
if (stat.isDirectory()) {
|
|
175
|
-
const entries = fs
|
|
176
|
-
.readdirSync(filePath)
|
|
177
|
-
.sort((left, right) => left.localeCompare(right))
|
|
178
|
-
.map((entry) => {
|
|
179
|
-
const childPath = path.join(filePath, entry);
|
|
180
|
-
return `${entry}:${hashPathState(childPath)}`;
|
|
181
|
-
})
|
|
182
|
-
.join("|");
|
|
183
|
-
return crypto.createHash("sha256").update(entries).digest("hex");
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
return crypto.createHash("sha256").update(fs.readFileSync(filePath)).digest("hex");
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
function snapshotFrozenCore(targetDir, rawValue) {
|
|
190
|
-
const { relativePaths } = resolveFrozenCoreEntries(rawValue);
|
|
191
|
-
const snapshot = new Map();
|
|
192
|
-
|
|
193
|
-
for (const relativePath of relativePaths) {
|
|
194
|
-
const absolutePath = path.resolve(targetDir, relativePath);
|
|
195
|
-
snapshot.set(absolutePath, hashPathState(absolutePath));
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
return snapshot;
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
function detectFrozenCoreChanges(snapshot) {
|
|
202
|
-
const changedPaths = [];
|
|
203
|
-
for (const [absolutePath, initialHash] of snapshot.entries()) {
|
|
204
|
-
const currentHash = hashPathState(absolutePath);
|
|
205
|
-
if (currentHash !== initialHash) {
|
|
206
|
-
changedPaths.push(absolutePath);
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
return changedPaths;
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
function parseAutoStatus(targetDir) {
|
|
213
|
-
const text = readFileIfExists(contextFile(targetDir, "auto-status.md"));
|
|
214
|
-
return {
|
|
215
|
-
path: contextFile(targetDir, "auto-status.md"),
|
|
216
|
-
text,
|
|
217
|
-
status: extractValue(text, ["Status", "状态"]),
|
|
218
|
-
currentStage: extractValue(text, ["Current stage", "当前阶段"]),
|
|
219
|
-
currentCommand: extractValue(text, ["Current command", "当前命令"]),
|
|
220
|
-
activeRunId: extractValue(text, ["Active run id", "当前活跃 run id"]),
|
|
221
|
-
iterationCount: extractValue(text, ["Iteration count", "迭代计数"]),
|
|
222
|
-
startedAt: extractValue(text, ["Started at", "开始时间"]),
|
|
223
|
-
lastHeartbeat: extractValue(text, ["Last heartbeat", "最近心跳"]),
|
|
224
|
-
lastCheckpoint: extractValue(text, ["Last checkpoint", "最近 checkpoint"]),
|
|
225
|
-
lastSummary: extractValue(text, ["Last summary", "最近 summary"]),
|
|
226
|
-
decision: extractValue(text, ["Current decision", "当前决策"]),
|
|
227
|
-
};
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
function validateAutoMode(mode, status = null) {
|
|
231
|
-
const issues = [];
|
|
232
|
-
const activated =
|
|
233
|
-
isMeaningful(mode.objective) ||
|
|
234
|
-
(status && ["running", "stopped", "failed", "completed"].includes((status.status || "").toLowerCase()));
|
|
235
|
-
|
|
236
|
-
if (!activated) {
|
|
237
|
-
return issues;
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
const requiredFields = [
|
|
241
|
-
["objective", mode.objective],
|
|
242
|
-
["allowed stages", mode.allowedStages.join(", ")],
|
|
243
|
-
["success criteria", mode.successCriteria],
|
|
244
|
-
["max iterations", mode.maxIterations],
|
|
245
|
-
["max wall-clock time", mode.maxWallClockTime],
|
|
246
|
-
["max failures", mode.maxFailures],
|
|
247
|
-
["poll interval", mode.pollInterval],
|
|
248
|
-
["success check command", mode.successCheckCommand],
|
|
249
|
-
["stop check command", mode.stopCheckCommand],
|
|
250
|
-
["promotion policy", mode.promotionPolicy],
|
|
251
|
-
["promotion check command", mode.promotionCheckCommand],
|
|
252
|
-
["promotion command", mode.promotionCommand],
|
|
253
|
-
["frozen core", mode.frozenCore],
|
|
254
|
-
["exploration envelope", mode.explorationEnvelope],
|
|
255
|
-
["stop conditions", mode.stopConditions],
|
|
256
|
-
];
|
|
257
|
-
|
|
258
|
-
const missing = requiredFields
|
|
259
|
-
.filter(([, value]) => !isMeaningful(value))
|
|
260
|
-
.map(([name]) => name);
|
|
261
|
-
if (missing.length > 0) {
|
|
262
|
-
issues.push(`missing auto mode fields: ${missing.join(", ")}`);
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
const invalidStages = mode.allowedStages.filter((stage) => !ALLOWED_AUTO_STAGES.has(stage));
|
|
266
|
-
if (invalidStages.length > 0) {
|
|
267
|
-
issues.push(`unsupported auto stages: ${invalidStages.join(", ")}`);
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
const missingCommands = mode.allowedStages.filter((stage) => !isMeaningful(mode.stageCommands?.[stage]));
|
|
271
|
-
if (missingCommands.length > 0) {
|
|
272
|
-
issues.push(`missing auto commands for stages: ${missingCommands.join(", ")}`);
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
const invalidFrozenCoreEntries = resolveFrozenCoreEntries(mode.frozenCore).invalidEntries;
|
|
276
|
-
if (invalidFrozenCoreEntries.length > 0) {
|
|
277
|
-
issues.push(`unsupported frozen core entries: ${invalidFrozenCoreEntries.join(", ")}`);
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
return issues;
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
function validateAutoStatus(status, mode = null) {
|
|
284
|
-
const issues = [];
|
|
285
|
-
const normalizedStatus = (status.status || "").trim().toLowerCase();
|
|
286
|
-
if (!normalizedStatus || normalizedStatus === "idle") {
|
|
287
|
-
return issues;
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
if (!["running", "stopped", "failed", "completed"].includes(normalizedStatus)) {
|
|
291
|
-
issues.push(`invalid auto status: ${status.status}`);
|
|
292
|
-
return issues;
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
if (normalizedStatus === "running") {
|
|
296
|
-
if (!isMeaningful(status.currentStage)) {
|
|
297
|
-
issues.push("auto status is missing current stage");
|
|
298
|
-
}
|
|
299
|
-
if (!isMeaningful(status.startedAt)) {
|
|
300
|
-
issues.push("auto status is missing started at");
|
|
301
|
-
}
|
|
302
|
-
if (!isMeaningful(status.lastHeartbeat)) {
|
|
303
|
-
issues.push("auto status is missing last heartbeat");
|
|
304
|
-
}
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
if (mode && mode.allowedStages.length > 0 && isMeaningful(status.currentStage)) {
|
|
308
|
-
const currentStage = status.currentStage.trim().toLowerCase();
|
|
309
|
-
if (!mode.allowedStages.includes(currentStage)) {
|
|
310
|
-
issues.push(`auto status stage is outside allowed stages: ${status.currentStage}`);
|
|
311
|
-
}
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
return issues;
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
function renderAutoStatus(status, { lang = "en" } = {}) {
|
|
318
|
-
if (lang === "zh") {
|
|
319
|
-
return `# 自动模式状态
|
|
320
|
-
|
|
321
|
-
## 运行状态
|
|
322
|
-
|
|
323
|
-
- 状态: ${status.status || "idle"}
|
|
324
|
-
- 当前阶段: ${status.currentStage || ""}
|
|
325
|
-
- 当前命令: ${status.currentCommand || ""}
|
|
326
|
-
- 当前活跃 run id: ${status.activeRunId || ""}
|
|
327
|
-
- 迭代计数: ${status.iterationCount || "0"}
|
|
328
|
-
|
|
329
|
-
## 时间
|
|
330
|
-
|
|
331
|
-
- 开始时间: ${status.startedAt || ""}
|
|
332
|
-
- 最近心跳: ${status.lastHeartbeat || ""}
|
|
333
|
-
|
|
334
|
-
## 工件
|
|
335
|
-
|
|
336
|
-
- 最近 checkpoint: ${status.lastCheckpoint || ""}
|
|
337
|
-
- 最近 summary: ${status.lastSummary || ""}
|
|
338
|
-
|
|
339
|
-
## 决策
|
|
340
|
-
|
|
341
|
-
- 当前决策: ${status.decision || ""}
|
|
342
|
-
`;
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
return `# Auto Mode Status
|
|
346
|
-
|
|
347
|
-
## Runtime State
|
|
348
|
-
|
|
349
|
-
- Status: ${status.status || "idle"}
|
|
350
|
-
- Current stage: ${status.currentStage || ""}
|
|
351
|
-
- Current command: ${status.currentCommand || ""}
|
|
352
|
-
- Active run id: ${status.activeRunId || ""}
|
|
353
|
-
- Iteration count: ${status.iterationCount || "0"}
|
|
354
|
-
|
|
355
|
-
## Timing
|
|
356
|
-
|
|
357
|
-
- Started at: ${status.startedAt || ""}
|
|
358
|
-
- Last heartbeat: ${status.lastHeartbeat || ""}
|
|
359
|
-
|
|
360
|
-
## Artifacts
|
|
361
|
-
|
|
362
|
-
- Last checkpoint: ${status.lastCheckpoint || ""}
|
|
363
|
-
- Last summary: ${status.lastSummary || ""}
|
|
364
|
-
|
|
365
|
-
## Decision
|
|
366
|
-
|
|
367
|
-
- Current decision: ${status.decision || ""}
|
|
368
|
-
`;
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
function writeAutoStatus(targetDir, status, { lang = "en" } = {}) {
|
|
372
|
-
const filePath = contextFile(targetDir, "auto-status.md");
|
|
373
|
-
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
374
|
-
fs.writeFileSync(filePath, renderAutoStatus(status, { lang }).trimEnd() + "\n");
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
function readWorkflowLanguage(targetDir) {
|
|
378
|
-
const configPath = path.join(targetDir, ".lab", "config", "workflow.json");
|
|
379
|
-
if (!fs.existsSync(configPath)) {
|
|
380
|
-
return "en";
|
|
381
|
-
}
|
|
382
|
-
try {
|
|
383
|
-
const config = JSON.parse(fs.readFileSync(configPath, "utf8"));
|
|
384
|
-
return config.workflow_language === "zh" ? "zh" : "en";
|
|
385
|
-
} catch {
|
|
386
|
-
return "en";
|
|
387
|
-
}
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
function readWorkflowConfig(targetDir) {
|
|
391
|
-
const configPath = path.join(targetDir, ".lab", "config", "workflow.json");
|
|
392
|
-
try {
|
|
393
|
-
return JSON.parse(fs.readFileSync(configPath, "utf8"));
|
|
394
|
-
} catch {
|
|
395
|
-
return {};
|
|
396
|
-
}
|
|
397
|
-
}
|
|
398
|
-
|
|
399
|
-
function resolveProjectPath(targetDir, configuredPath, fallbackRelativePath) {
|
|
400
|
-
if (typeof configuredPath !== "string" || configuredPath.trim() === "") {
|
|
401
|
-
return path.join(targetDir, fallbackRelativePath);
|
|
402
|
-
}
|
|
403
|
-
return path.isAbsolute(configuredPath)
|
|
404
|
-
? configuredPath
|
|
405
|
-
: path.resolve(targetDir, configuredPath);
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
function snapshotPaths(targetDir, relativePaths) {
|
|
409
|
-
const snapshot = new Map();
|
|
410
|
-
for (const relativePath of relativePaths) {
|
|
411
|
-
const absolutePath = path.resolve(targetDir, relativePath);
|
|
412
|
-
snapshot.set(absolutePath, hashPathState(absolutePath));
|
|
413
|
-
}
|
|
414
|
-
return snapshot;
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
function changedSnapshotPaths(snapshot) {
|
|
418
|
-
const changed = [];
|
|
419
|
-
for (const [absolutePath, previousHash] of snapshot.entries()) {
|
|
420
|
-
if (hashPathState(absolutePath) !== previousHash) {
|
|
421
|
-
changed.push(absolutePath);
|
|
422
|
-
}
|
|
423
|
-
}
|
|
424
|
-
return changed;
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
function stageContractSnapshot(targetDir, stage) {
|
|
428
|
-
const workflowConfig = readWorkflowConfig(targetDir);
|
|
429
|
-
const resultsRoot = resolveProjectPath(targetDir, workflowConfig.results_root, "results");
|
|
430
|
-
const deliverablesRoot = resolveProjectPath(targetDir, workflowConfig.deliverables_root, path.join("docs", "research"));
|
|
431
|
-
const trackedPathsByStage = {
|
|
432
|
-
run: [resultsRoot],
|
|
433
|
-
iterate: [resultsRoot],
|
|
434
|
-
review: REVIEW_CONTEXT_FILES.map((relativePath) => path.resolve(targetDir, relativePath)),
|
|
435
|
-
report: [path.join(deliverablesRoot, "report.md")],
|
|
436
|
-
write: [
|
|
437
|
-
path.join(deliverablesRoot, "paper", "main.tex"),
|
|
438
|
-
path.join(deliverablesRoot, "paper", "sections"),
|
|
439
|
-
],
|
|
440
|
-
};
|
|
441
|
-
|
|
442
|
-
const absolutePaths = trackedPathsByStage[stage] || [];
|
|
443
|
-
const snapshot = new Map();
|
|
444
|
-
for (const absolutePath of absolutePaths) {
|
|
445
|
-
snapshot.set(absolutePath, hashPathState(absolutePath));
|
|
446
|
-
}
|
|
447
|
-
return {
|
|
448
|
-
stage,
|
|
449
|
-
absolutePaths,
|
|
450
|
-
snapshot,
|
|
451
|
-
};
|
|
452
|
-
}
|
|
453
|
-
|
|
454
|
-
function verifyStageContract({ stage, snapshot }) {
|
|
455
|
-
const changedPaths = [];
|
|
456
|
-
for (const [absolutePath, previousHash] of snapshot.entries()) {
|
|
457
|
-
if (hashPathState(absolutePath) !== previousHash) {
|
|
458
|
-
changedPaths.push(absolutePath);
|
|
459
|
-
}
|
|
460
|
-
}
|
|
461
|
-
|
|
462
|
-
if (stage === "review") {
|
|
463
|
-
if (changedPaths.length === 0) {
|
|
464
|
-
throw new Error(
|
|
465
|
-
"review stage did not update canonical review context (.lab/context/decisions.md, state.md, open-questions.md, or evidence-index.md)"
|
|
466
|
-
);
|
|
467
|
-
}
|
|
468
|
-
return;
|
|
469
|
-
}
|
|
470
|
-
|
|
471
|
-
if (stage === "report") {
|
|
472
|
-
if (changedPaths.length === 0) {
|
|
473
|
-
throw new Error("report stage did not produce the deliverable report.md under deliverables_root");
|
|
474
|
-
}
|
|
475
|
-
return;
|
|
476
|
-
}
|
|
477
|
-
|
|
478
|
-
if (stage === "write") {
|
|
479
|
-
if (changedPaths.length === 0) {
|
|
480
|
-
throw new Error("write stage did not produce LaTeX output under deliverables_root/paper");
|
|
481
|
-
}
|
|
482
|
-
return;
|
|
483
|
-
}
|
|
484
|
-
|
|
485
|
-
if ((stage === "run" || stage === "iterate") && changedPaths.length === 0) {
|
|
486
|
-
throw new Error(`${stage} stage did not produce persistent outputs under results_root`);
|
|
487
|
-
}
|
|
488
|
-
}
|
|
489
|
-
|
|
490
|
-
function verifyPromotionWriteback(targetDir, snapshot) {
|
|
491
|
-
const changedPaths = changedSnapshotPaths(snapshot);
|
|
492
|
-
if (changedPaths.length !== PROMOTION_CANONICAL_FILES.length) {
|
|
493
|
-
throw new Error(
|
|
494
|
-
`promotion did not update canonical context: ${PROMOTION_CANONICAL_FILES.filter(
|
|
495
|
-
(relativePath) => !changedPaths.includes(path.resolve(targetDir, relativePath))
|
|
496
|
-
).join(", ")}`
|
|
497
|
-
);
|
|
498
|
-
}
|
|
499
|
-
}
|
|
500
|
-
|
|
501
|
-
async function runCommandWithPolling({ targetDir, stage, command, pollIntervalMs, deadlineMs, startedAt, status, lang }) {
|
|
502
|
-
const child = spawn(command, {
|
|
503
|
-
cwd: targetDir,
|
|
504
|
-
shell: true,
|
|
505
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
506
|
-
});
|
|
507
|
-
|
|
508
|
-
let stdout = "";
|
|
509
|
-
let stderr = "";
|
|
510
|
-
let exitCode = null;
|
|
511
|
-
let signalCode = null;
|
|
512
|
-
|
|
513
|
-
child.stdout.on("data", (chunk) => {
|
|
514
|
-
if (stdout.length < 4000) {
|
|
515
|
-
stdout += chunk.toString("utf8");
|
|
516
|
-
}
|
|
517
|
-
});
|
|
518
|
-
child.stderr.on("data", (chunk) => {
|
|
519
|
-
if (stderr.length < 4000) {
|
|
520
|
-
stderr += chunk.toString("utf8");
|
|
521
|
-
}
|
|
522
|
-
});
|
|
523
|
-
child.on("exit", (code, signal) => {
|
|
524
|
-
exitCode = code;
|
|
525
|
-
signalCode = signal;
|
|
526
|
-
});
|
|
527
|
-
|
|
528
|
-
while (exitCode === null && signalCode === null) {
|
|
529
|
-
if (Date.now() > deadlineMs) {
|
|
530
|
-
child.kill("SIGTERM");
|
|
531
|
-
await sleep(50);
|
|
532
|
-
if (child.exitCode === null && child.signalCode === null) {
|
|
533
|
-
child.kill("SIGKILL");
|
|
534
|
-
}
|
|
535
|
-
throw new Error(`auto stage timed out: ${stage}`);
|
|
536
|
-
}
|
|
537
|
-
|
|
538
|
-
writeAutoStatus(
|
|
539
|
-
targetDir,
|
|
540
|
-
{
|
|
541
|
-
...status,
|
|
542
|
-
status: "running",
|
|
543
|
-
currentStage: stage,
|
|
544
|
-
currentCommand: command,
|
|
545
|
-
startedAt,
|
|
546
|
-
lastHeartbeat: new Date().toISOString(),
|
|
547
|
-
decision: `running stage ${stage}`,
|
|
548
|
-
},
|
|
549
|
-
{ lang }
|
|
550
|
-
);
|
|
551
|
-
await sleep(pollIntervalMs);
|
|
552
|
-
}
|
|
553
|
-
|
|
554
|
-
if ((exitCode ?? 1) !== 0) {
|
|
555
|
-
throw new Error(`auto stage failed: ${stage}${stderr ? ` | ${stderr.trim()}` : stdout ? ` | ${stdout.trim()}` : ""}`);
|
|
556
|
-
}
|
|
557
|
-
|
|
558
|
-
return {
|
|
559
|
-
stdout: stdout.trim(),
|
|
560
|
-
stderr: stderr.trim(),
|
|
561
|
-
};
|
|
562
|
-
}
|
|
563
|
-
|
|
564
|
-
function runCheckCommand({ targetDir, label, command, deadlineMs }) {
|
|
565
|
-
const remainingMs = Math.max(1000, deadlineMs - Date.now());
|
|
566
|
-
const result = spawn(command, {
|
|
567
|
-
cwd: targetDir,
|
|
568
|
-
shell: true,
|
|
569
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
570
|
-
});
|
|
571
|
-
|
|
572
|
-
return new Promise((resolve, reject) => {
|
|
573
|
-
let stdout = "";
|
|
574
|
-
let stderr = "";
|
|
575
|
-
let settled = false;
|
|
576
|
-
const timeout = setTimeout(() => {
|
|
577
|
-
if (settled) {
|
|
578
|
-
return;
|
|
579
|
-
}
|
|
580
|
-
settled = true;
|
|
581
|
-
result.kill("SIGTERM");
|
|
582
|
-
reject(new Error(`${label} timed out`));
|
|
583
|
-
}, remainingMs);
|
|
584
|
-
|
|
585
|
-
result.stdout.on("data", (chunk) => {
|
|
586
|
-
if (stdout.length < 4000) {
|
|
587
|
-
stdout += chunk.toString("utf8");
|
|
588
|
-
}
|
|
589
|
-
});
|
|
590
|
-
result.stderr.on("data", (chunk) => {
|
|
591
|
-
if (stderr.length < 4000) {
|
|
592
|
-
stderr += chunk.toString("utf8");
|
|
593
|
-
}
|
|
594
|
-
});
|
|
595
|
-
result.on("error", (error) => {
|
|
596
|
-
if (settled) {
|
|
597
|
-
return;
|
|
598
|
-
}
|
|
599
|
-
settled = true;
|
|
600
|
-
clearTimeout(timeout);
|
|
601
|
-
reject(error);
|
|
602
|
-
});
|
|
603
|
-
result.on("exit", (code, signal) => {
|
|
604
|
-
if (settled) {
|
|
605
|
-
return;
|
|
606
|
-
}
|
|
607
|
-
settled = true;
|
|
608
|
-
clearTimeout(timeout);
|
|
609
|
-
if (signal) {
|
|
610
|
-
reject(new Error(`${label} exited with signal ${signal}`));
|
|
611
|
-
return;
|
|
612
|
-
}
|
|
613
|
-
if (code === 0) {
|
|
614
|
-
resolve({ matched: true, stdout: stdout.trim(), stderr: stderr.trim() });
|
|
615
|
-
return;
|
|
616
|
-
}
|
|
617
|
-
if (code === 1) {
|
|
618
|
-
resolve({ matched: false, stdout: stdout.trim(), stderr: stderr.trim() });
|
|
619
|
-
return;
|
|
620
|
-
}
|
|
621
|
-
reject(
|
|
622
|
-
new Error(
|
|
623
|
-
`${label} failed with exit code ${code}${
|
|
624
|
-
stderr ? ` | ${stderr.trim()}` : stdout ? ` | ${stdout.trim()}` : ""
|
|
625
|
-
}`
|
|
626
|
-
)
|
|
627
|
-
);
|
|
628
|
-
});
|
|
629
|
-
});
|
|
630
|
-
}
|
|
631
|
-
|
|
632
|
-
function splitAutoStages(allowedStages) {
|
|
633
|
-
const loopStages = allowedStages.filter((stage) => LOOP_AUTO_STAGES.has(stage));
|
|
634
|
-
const finalStages = allowedStages.filter((stage) => FINAL_AUTO_STAGES.has(stage));
|
|
635
|
-
return { loopStages, finalStages };
|
|
636
|
-
}
|
|
637
|
-
|
|
638
|
-
async function startAutoMode({ targetDir, now = new Date() }) {
|
|
639
|
-
const mode = parseAutoMode(targetDir);
|
|
640
|
-
const issues = validateAutoMode(mode);
|
|
641
|
-
if (issues.length > 0) {
|
|
642
|
-
throw new Error(issues.join(" | "));
|
|
643
|
-
}
|
|
644
|
-
|
|
645
|
-
const lang = readWorkflowLanguage(targetDir);
|
|
646
|
-
const timestamp = now.toISOString();
|
|
647
|
-
const status = {
|
|
648
|
-
status: "running",
|
|
649
|
-
currentStage: mode.allowedStages[0] || "run",
|
|
650
|
-
currentCommand: "",
|
|
651
|
-
activeRunId: "",
|
|
652
|
-
iterationCount: "0",
|
|
653
|
-
startedAt: timestamp,
|
|
654
|
-
lastHeartbeat: timestamp,
|
|
655
|
-
lastCheckpoint: "",
|
|
656
|
-
lastSummary: "",
|
|
657
|
-
decision: "armed for bounded auto orchestration",
|
|
658
|
-
};
|
|
659
|
-
writeAutoStatus(targetDir, status, { lang });
|
|
660
|
-
|
|
661
|
-
const startedAt = status.startedAt;
|
|
662
|
-
const pollIntervalMs = parseDurationMs(mode.pollInterval, 1000);
|
|
663
|
-
const maxWallClockMs = parseDurationMs(mode.maxWallClockTime, 60 * 60 * 1000);
|
|
664
|
-
const deadlineMs = Date.now() + maxWallClockMs;
|
|
665
|
-
const maxFailures = parseInteger(mode.maxFailures, 0);
|
|
666
|
-
const maxIterations = parseInteger(mode.maxIterations, 1);
|
|
667
|
-
const frozenCoreSnapshot = snapshotFrozenCore(targetDir, mode.frozenCore);
|
|
668
|
-
const { loopStages, finalStages } = splitAutoStages(mode.allowedStages);
|
|
669
|
-
const executedStages = [];
|
|
670
|
-
let failureCount = 0;
|
|
671
|
-
let currentStatus = { ...status };
|
|
672
|
-
let successReached = false;
|
|
673
|
-
let stopMatched = false;
|
|
674
|
-
let promotionApplied = false;
|
|
675
|
-
let stopReason = "";
|
|
676
|
-
|
|
677
|
-
const writeRunningStatus = (overrides = {}) => {
|
|
678
|
-
currentStatus = {
|
|
679
|
-
...currentStatus,
|
|
680
|
-
status: "running",
|
|
681
|
-
lastHeartbeat: new Date().toISOString(),
|
|
682
|
-
...overrides,
|
|
683
|
-
};
|
|
684
|
-
writeAutoStatus(targetDir, currentStatus, { lang });
|
|
685
|
-
};
|
|
686
|
-
|
|
687
|
-
const failAutoMode = (message) => {
|
|
688
|
-
currentStatus = {
|
|
689
|
-
...currentStatus,
|
|
690
|
-
status: "failed",
|
|
691
|
-
lastHeartbeat: new Date().toISOString(),
|
|
692
|
-
decision: message,
|
|
693
|
-
};
|
|
694
|
-
writeAutoStatus(targetDir, currentStatus, { lang });
|
|
695
|
-
throw new Error(message);
|
|
696
|
-
};
|
|
697
|
-
|
|
698
|
-
const stageExecutors = {
|
|
699
|
-
run: async () => {
|
|
700
|
-
const contract = stageContractSnapshot(targetDir, "run");
|
|
701
|
-
await runCommandWithPolling({
|
|
702
|
-
targetDir,
|
|
703
|
-
stage: "run",
|
|
704
|
-
command: mode.stageCommands.run,
|
|
705
|
-
pollIntervalMs,
|
|
706
|
-
deadlineMs,
|
|
707
|
-
startedAt,
|
|
708
|
-
status: currentStatus,
|
|
709
|
-
lang,
|
|
710
|
-
});
|
|
711
|
-
verifyStageContract({ stage: "run", snapshot: contract.snapshot });
|
|
712
|
-
},
|
|
713
|
-
iterate: async () => {
|
|
714
|
-
const contract = stageContractSnapshot(targetDir, "iterate");
|
|
715
|
-
await runCommandWithPolling({
|
|
716
|
-
targetDir,
|
|
717
|
-
stage: "iterate",
|
|
718
|
-
command: mode.stageCommands.iterate,
|
|
719
|
-
pollIntervalMs,
|
|
720
|
-
deadlineMs,
|
|
721
|
-
startedAt,
|
|
722
|
-
status: currentStatus,
|
|
723
|
-
lang,
|
|
724
|
-
});
|
|
725
|
-
verifyStageContract({ stage: "iterate", snapshot: contract.snapshot });
|
|
726
|
-
},
|
|
727
|
-
review: async () => {
|
|
728
|
-
const contract = stageContractSnapshot(targetDir, "review");
|
|
729
|
-
await runCommandWithPolling({
|
|
730
|
-
targetDir,
|
|
731
|
-
stage: "review",
|
|
732
|
-
command: mode.stageCommands.review,
|
|
733
|
-
pollIntervalMs,
|
|
734
|
-
deadlineMs,
|
|
735
|
-
startedAt,
|
|
736
|
-
status: currentStatus,
|
|
737
|
-
lang,
|
|
738
|
-
});
|
|
739
|
-
verifyStageContract({ stage: "review", snapshot: contract.snapshot });
|
|
740
|
-
},
|
|
741
|
-
report: async () => {
|
|
742
|
-
const contract = stageContractSnapshot(targetDir, "report");
|
|
743
|
-
await runCommandWithPolling({
|
|
744
|
-
targetDir,
|
|
745
|
-
stage: "report",
|
|
746
|
-
command: mode.stageCommands.report,
|
|
747
|
-
pollIntervalMs,
|
|
748
|
-
deadlineMs,
|
|
749
|
-
startedAt,
|
|
750
|
-
status: currentStatus,
|
|
751
|
-
lang,
|
|
752
|
-
});
|
|
753
|
-
verifyStageContract({ stage: "report", snapshot: contract.snapshot });
|
|
754
|
-
},
|
|
755
|
-
write: async () => {
|
|
756
|
-
const contract = stageContractSnapshot(targetDir, "write");
|
|
757
|
-
await runCommandWithPolling({
|
|
758
|
-
targetDir,
|
|
759
|
-
stage: "write",
|
|
760
|
-
command: mode.stageCommands.write,
|
|
761
|
-
pollIntervalMs,
|
|
762
|
-
deadlineMs,
|
|
763
|
-
startedAt,
|
|
764
|
-
status: currentStatus,
|
|
765
|
-
lang,
|
|
766
|
-
});
|
|
767
|
-
verifyStageContract({ stage: "write", snapshot: contract.snapshot });
|
|
768
|
-
},
|
|
769
|
-
};
|
|
770
|
-
|
|
771
|
-
const executeStage = async (stage) => {
|
|
772
|
-
const command = mode.stageCommands[stage];
|
|
773
|
-
if (!isMeaningful(command)) {
|
|
774
|
-
return;
|
|
775
|
-
}
|
|
776
|
-
|
|
777
|
-
let stageCompleted = false;
|
|
778
|
-
while (!stageCompleted) {
|
|
779
|
-
try {
|
|
780
|
-
const executor = stageExecutors[stage];
|
|
781
|
-
if (!executor) {
|
|
782
|
-
throw new Error(`unsupported auto stage executor: ${stage}`);
|
|
783
|
-
}
|
|
784
|
-
await executor();
|
|
785
|
-
executedStages.push(stage);
|
|
786
|
-
writeRunningStatus({
|
|
787
|
-
currentStage: stage,
|
|
788
|
-
currentCommand: command,
|
|
789
|
-
decision: `completed stage ${stage}`,
|
|
790
|
-
});
|
|
791
|
-
refreshContext({ targetDir });
|
|
792
|
-
|
|
793
|
-
const frozenCoreChanges = detectFrozenCoreChanges(frozenCoreSnapshot);
|
|
794
|
-
if (frozenCoreChanges.length > 0) {
|
|
795
|
-
failAutoMode(`frozen core changed: ${frozenCoreChanges.join(", ")}`);
|
|
796
|
-
}
|
|
797
|
-
|
|
798
|
-
const stopCheck = await runCheckCommand({
|
|
799
|
-
targetDir,
|
|
800
|
-
label: `stop check after ${stage}`,
|
|
801
|
-
command: mode.stopCheckCommand,
|
|
802
|
-
deadlineMs,
|
|
803
|
-
});
|
|
804
|
-
if (stopCheck.matched) {
|
|
805
|
-
stopMatched = true;
|
|
806
|
-
stopReason = stopCheck.stdout || stopCheck.stderr || `stop condition matched after ${stage}`;
|
|
807
|
-
stageCompleted = true;
|
|
808
|
-
return;
|
|
809
|
-
}
|
|
810
|
-
|
|
811
|
-
stageCompleted = true;
|
|
812
|
-
} catch (error) {
|
|
813
|
-
failureCount += 1;
|
|
814
|
-
if (failureCount > maxFailures) {
|
|
815
|
-
failAutoMode(error.message);
|
|
816
|
-
}
|
|
817
|
-
writeRunningStatus({
|
|
818
|
-
currentStage: stage,
|
|
819
|
-
currentCommand: command,
|
|
820
|
-
decision: `retrying stage ${stage} after failure ${failureCount}`,
|
|
821
|
-
});
|
|
822
|
-
}
|
|
823
|
-
}
|
|
824
|
-
};
|
|
825
|
-
|
|
826
|
-
const stagesPerIteration = loopStages.length > 0 ? loopStages : mode.allowedStages;
|
|
827
|
-
|
|
828
|
-
for (let iteration = 1; iteration <= Math.max(1, maxIterations); iteration += 1) {
|
|
829
|
-
writeRunningStatus({
|
|
830
|
-
currentStage: stagesPerIteration[0] || currentStatus.currentStage,
|
|
831
|
-
currentCommand: mode.stageCommands[stagesPerIteration[0]] || "",
|
|
832
|
-
iterationCount: String(iteration),
|
|
833
|
-
decision: `starting iteration ${iteration}`,
|
|
834
|
-
});
|
|
835
|
-
|
|
836
|
-
for (const stage of stagesPerIteration) {
|
|
837
|
-
await executeStage(stage);
|
|
838
|
-
if (stopMatched || successReached) {
|
|
839
|
-
break;
|
|
840
|
-
}
|
|
841
|
-
}
|
|
842
|
-
|
|
843
|
-
if (stopMatched || successReached) {
|
|
844
|
-
break;
|
|
845
|
-
}
|
|
846
|
-
|
|
847
|
-
if (!promotionApplied && isMeaningful(mode.promotionCheckCommand)) {
|
|
848
|
-
const promotionCheck = await runCheckCommand({
|
|
849
|
-
targetDir,
|
|
850
|
-
label: `promotion check after iteration ${iteration}`,
|
|
851
|
-
command: mode.promotionCheckCommand,
|
|
852
|
-
deadlineMs,
|
|
853
|
-
});
|
|
854
|
-
if (promotionCheck.matched) {
|
|
855
|
-
const promotionSnapshot = snapshotPaths(targetDir, PROMOTION_CANONICAL_FILES);
|
|
856
|
-
await runCommandWithPolling({
|
|
857
|
-
targetDir,
|
|
858
|
-
stage: "promotion",
|
|
859
|
-
command: mode.promotionCommand,
|
|
860
|
-
pollIntervalMs,
|
|
861
|
-
deadlineMs,
|
|
862
|
-
startedAt,
|
|
863
|
-
status: currentStatus,
|
|
864
|
-
lang,
|
|
865
|
-
});
|
|
866
|
-
writeRunningStatus({
|
|
867
|
-
currentStage: stagesPerIteration.at(-1) || currentStatus.currentStage,
|
|
868
|
-
currentCommand: mode.promotionCommand,
|
|
869
|
-
decision: `promotion policy matched after iteration ${iteration}`,
|
|
870
|
-
});
|
|
871
|
-
promotionApplied = true;
|
|
872
|
-
refreshContext({ targetDir });
|
|
873
|
-
verifyPromotionWriteback(targetDir, promotionSnapshot);
|
|
874
|
-
const frozenCoreChangesAfterPromotion = detectFrozenCoreChanges(frozenCoreSnapshot);
|
|
875
|
-
if (frozenCoreChangesAfterPromotion.length > 0) {
|
|
876
|
-
failAutoMode(`frozen core changed: ${frozenCoreChangesAfterPromotion.join(", ")}`);
|
|
877
|
-
}
|
|
878
|
-
}
|
|
879
|
-
}
|
|
880
|
-
|
|
881
|
-
const successCheck = await runCheckCommand({
|
|
882
|
-
targetDir,
|
|
883
|
-
label: `success check after iteration ${iteration}`,
|
|
884
|
-
command: mode.successCheckCommand,
|
|
885
|
-
deadlineMs,
|
|
886
|
-
});
|
|
887
|
-
if (successCheck.matched) {
|
|
888
|
-
successReached = true;
|
|
889
|
-
break;
|
|
890
|
-
}
|
|
891
|
-
|
|
892
|
-
}
|
|
893
|
-
|
|
894
|
-
if (stopMatched) {
|
|
895
|
-
currentStatus = {
|
|
896
|
-
...currentStatus,
|
|
897
|
-
status: "stopped",
|
|
898
|
-
currentStage: executedStages.at(-1) || currentStatus.currentStage,
|
|
899
|
-
currentCommand: mode.stageCommands[executedStages.at(-1)] || currentStatus.currentCommand,
|
|
900
|
-
lastHeartbeat: new Date().toISOString(),
|
|
901
|
-
decision: stopReason || "stopped by stop condition",
|
|
902
|
-
};
|
|
903
|
-
writeAutoStatus(targetDir, currentStatus, { lang });
|
|
904
|
-
return {
|
|
905
|
-
mode,
|
|
906
|
-
status: currentStatus,
|
|
907
|
-
executedStages,
|
|
908
|
-
};
|
|
909
|
-
}
|
|
910
|
-
|
|
911
|
-
if (!successReached && loopStages.length > 0) {
|
|
912
|
-
failAutoMode(`max iterations exhausted without meeting success criteria: ${maxIterations}`);
|
|
913
|
-
}
|
|
914
|
-
|
|
915
|
-
for (const stage of finalStages) {
|
|
916
|
-
await executeStage(stage);
|
|
917
|
-
if (stopMatched) {
|
|
918
|
-
currentStatus = {
|
|
919
|
-
...currentStatus,
|
|
920
|
-
status: "stopped",
|
|
921
|
-
currentStage: executedStages.at(-1) || currentStatus.currentStage,
|
|
922
|
-
currentCommand: mode.stageCommands[executedStages.at(-1)] || currentStatus.currentCommand,
|
|
923
|
-
lastHeartbeat: new Date().toISOString(),
|
|
924
|
-
decision: stopReason || "stopped by stop condition",
|
|
925
|
-
};
|
|
926
|
-
writeAutoStatus(targetDir, currentStatus, { lang });
|
|
927
|
-
return {
|
|
928
|
-
mode,
|
|
929
|
-
status: currentStatus,
|
|
930
|
-
executedStages,
|
|
931
|
-
};
|
|
932
|
-
}
|
|
933
|
-
}
|
|
934
|
-
|
|
935
|
-
currentStatus = {
|
|
936
|
-
...currentStatus,
|
|
937
|
-
status: "completed",
|
|
938
|
-
currentStage: executedStages.at(-1) || currentStatus.currentStage,
|
|
939
|
-
currentCommand: mode.stageCommands[executedStages.at(-1)] || currentStatus.currentCommand,
|
|
940
|
-
lastHeartbeat: new Date().toISOString(),
|
|
941
|
-
decision: successReached ? "completed bounded auto orchestration" : "completed configured stages",
|
|
942
|
-
};
|
|
943
|
-
writeAutoStatus(targetDir, currentStatus, { lang });
|
|
944
|
-
return {
|
|
945
|
-
mode,
|
|
946
|
-
status: currentStatus,
|
|
947
|
-
executedStages,
|
|
948
|
-
};
|
|
949
|
-
}
|
|
950
|
-
|
|
951
|
-
function stopAutoMode({ targetDir, now = new Date() }) {
|
|
952
|
-
const existing = parseAutoStatus(targetDir);
|
|
953
|
-
const lang = readWorkflowLanguage(targetDir);
|
|
954
|
-
const status = {
|
|
955
|
-
...existing,
|
|
956
|
-
status: "stopped",
|
|
957
|
-
lastHeartbeat: now.toISOString(),
|
|
958
|
-
decision: "stopped by operator",
|
|
959
|
-
};
|
|
960
|
-
writeAutoStatus(targetDir, status, { lang });
|
|
961
|
-
return status;
|
|
962
|
-
}
|
|
963
|
-
|
|
964
|
-
function getAutoStatus({ targetDir }) {
|
|
965
|
-
const mode = parseAutoMode(targetDir);
|
|
966
|
-
const status = parseAutoStatus(targetDir);
|
|
967
|
-
return {
|
|
968
|
-
mode,
|
|
969
|
-
status,
|
|
970
|
-
issues: validateAutoMode(mode, status).concat(validateAutoStatus(status, mode)),
|
|
971
|
-
};
|
|
972
|
-
}
|
|
1
|
+
const {
|
|
2
|
+
ALLOWED_AUTO_STAGES,
|
|
3
|
+
validateAutoMode,
|
|
4
|
+
validateAutoStatus,
|
|
5
|
+
} = require("./auto_contracts.cjs");
|
|
6
|
+
const {
|
|
7
|
+
parseAutoMode,
|
|
8
|
+
parseAutoStatus,
|
|
9
|
+
} = require("./auto_state.cjs");
|
|
10
|
+
const {
|
|
11
|
+
getAutoStatus,
|
|
12
|
+
startAutoMode,
|
|
13
|
+
stopAutoMode,
|
|
14
|
+
} = require("./auto_runner.cjs");
|
|
973
15
|
|
|
974
16
|
module.exports = {
|
|
975
17
|
ALLOWED_AUTO_STAGES,
|