superlab 0.1.44 → 0.1.46

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.
Files changed (70) hide show
  1. package/bin/superlab.cjs +35 -4
  2. package/lib/auto_contracts.cjs +145 -0
  3. package/lib/auto_runner.cjs +181 -18
  4. package/lib/auto_state.cjs +146 -1
  5. package/lib/i18n.cjs +2 -2
  6. package/lib/install.cjs +54 -2
  7. package/package-assets/claude/commands/lab/auto.md +15 -0
  8. package/package-assets/claude/commands/lab/data.md +10 -0
  9. package/package-assets/claude/commands/lab/framing.md +10 -0
  10. package/package-assets/claude/commands/lab/idea.md +10 -0
  11. package/package-assets/claude/commands/lab/iterate.md +10 -0
  12. package/package-assets/claude/commands/lab/report.md +10 -0
  13. package/package-assets/claude/commands/lab/review.md +10 -0
  14. package/package-assets/claude/commands/lab/run.md +10 -0
  15. package/package-assets/claude/commands/lab/spec.md +10 -0
  16. package/package-assets/claude/commands/lab/write.md +10 -0
  17. package/package-assets/claude/commands/lab.md +1 -0
  18. package/package-assets/claude/commands/lab:auto.md +15 -0
  19. package/package-assets/claude/commands/lab:data.md +10 -0
  20. package/package-assets/claude/commands/lab:framing.md +10 -0
  21. package/package-assets/claude/commands/lab:idea.md +10 -0
  22. package/package-assets/claude/commands/lab:iterate.md +10 -0
  23. package/package-assets/claude/commands/lab:report.md +10 -0
  24. package/package-assets/claude/commands/lab:review.md +10 -0
  25. package/package-assets/claude/commands/lab:run.md +10 -0
  26. package/package-assets/claude/commands/lab:spec.md +10 -0
  27. package/package-assets/claude/commands/lab:write.md +10 -0
  28. package/package-assets/claude/commands/lab/357/274/232auto.md +15 -0
  29. package/package-assets/claude/commands/lab/357/274/232data.md +10 -0
  30. package/package-assets/claude/commands/lab/357/274/232framing.md +10 -0
  31. package/package-assets/claude/commands/lab/357/274/232idea.md +10 -0
  32. package/package-assets/claude/commands/lab/357/274/232iterate.md +10 -0
  33. package/package-assets/claude/commands/lab/357/274/232report.md +10 -0
  34. package/package-assets/claude/commands/lab/357/274/232review.md +10 -0
  35. package/package-assets/claude/commands/lab/357/274/232run.md +10 -0
  36. package/package-assets/claude/commands/lab/357/274/232spec.md +10 -0
  37. package/package-assets/claude/commands/lab/357/274/232write.md +10 -0
  38. package/package-assets/codex/prompts/lab/auto.md +14 -0
  39. package/package-assets/codex/prompts/lab/data.md +9 -0
  40. package/package-assets/codex/prompts/lab/framing.md +9 -0
  41. package/package-assets/codex/prompts/lab/idea.md +9 -0
  42. package/package-assets/codex/prompts/lab/iterate.md +9 -0
  43. package/package-assets/codex/prompts/lab/report.md +9 -0
  44. package/package-assets/codex/prompts/lab/review.md +9 -0
  45. package/package-assets/codex/prompts/lab/run.md +9 -0
  46. package/package-assets/codex/prompts/lab/spec.md +9 -0
  47. package/package-assets/codex/prompts/lab/write.md +9 -0
  48. package/package-assets/codex/prompts/lab.md +1 -0
  49. package/package-assets/codex/prompts/lab:auto.md +14 -0
  50. package/package-assets/codex/prompts/lab:data.md +9 -0
  51. package/package-assets/codex/prompts/lab:framing.md +9 -0
  52. package/package-assets/codex/prompts/lab:idea.md +9 -0
  53. package/package-assets/codex/prompts/lab:iterate.md +9 -0
  54. package/package-assets/codex/prompts/lab:report.md +9 -0
  55. package/package-assets/codex/prompts/lab:review.md +9 -0
  56. package/package-assets/codex/prompts/lab:run.md +9 -0
  57. package/package-assets/codex/prompts/lab:spec.md +9 -0
  58. package/package-assets/codex/prompts/lab:write.md +9 -0
  59. package/package-assets/codex/prompts/lab/357/274/232auto.md +14 -0
  60. package/package-assets/codex/prompts/lab/357/274/232data.md +9 -0
  61. package/package-assets/codex/prompts/lab/357/274/232framing.md +9 -0
  62. package/package-assets/codex/prompts/lab/357/274/232idea.md +9 -0
  63. package/package-assets/codex/prompts/lab/357/274/232iterate.md +9 -0
  64. package/package-assets/codex/prompts/lab/357/274/232report.md +9 -0
  65. package/package-assets/codex/prompts/lab/357/274/232review.md +9 -0
  66. package/package-assets/codex/prompts/lab/357/274/232run.md +9 -0
  67. package/package-assets/codex/prompts/lab/357/274/232spec.md +9 -0
  68. package/package-assets/codex/prompts/lab/357/274/232write.md +9 -0
  69. package/package-assets/shared/lab/context/auto-mode.md +3 -0
  70. package/package.json +1 -1
package/bin/superlab.cjs CHANGED
@@ -39,7 +39,7 @@ Usage:
39
39
  superlab init [--target <dir>] [--platform codex|claude|both|all] [--lang en|zh] [--force]
40
40
  superlab install [--target <dir>] [--platform codex|claude|both|all] [--lang en|zh] [--force]
41
41
  superlab paper attach-template --path <dir> [--target <dir>]
42
- superlab auto start [--target <dir>]
42
+ superlab auto start [--target <dir>] [--objective <text>] [--campaign-kind <kind>] [--allowed-stages <csv>]
43
43
  superlab auto status [--target <dir>]
44
44
  superlab auto stop [--target <dir>]
45
45
  superlab update [--target <dir>]
@@ -222,10 +222,34 @@ function parseAutoArgs(argv) {
222
222
  if (!["start", "status", "stop"].includes(action || "")) {
223
223
  throw new Error(`Unknown auto action: ${action || "(missing)"}`);
224
224
  }
225
- return {
225
+ const options = {
226
226
  action,
227
- ...parseTargetOnlyArgs(rest),
227
+ targetDir: process.cwd(),
228
+ requestedObjective: "",
229
+ requestedCampaignKind: "",
230
+ requestedAllowedStages: "",
228
231
  };
232
+
233
+ for (let index = 0; index < rest.length; index += 1) {
234
+ const value = rest[index];
235
+ if (value === "--target") {
236
+ options.targetDir = path.resolve(rest[index + 1]);
237
+ index += 1;
238
+ } else if (action === "start" && value === "--objective") {
239
+ options.requestedObjective = rest[index + 1] || "";
240
+ index += 1;
241
+ } else if (action === "start" && value === "--campaign-kind") {
242
+ options.requestedCampaignKind = rest[index + 1] || "";
243
+ index += 1;
244
+ } else if (action === "start" && value === "--allowed-stages") {
245
+ options.requestedAllowedStages = rest[index + 1] || "";
246
+ index += 1;
247
+ } else {
248
+ throw new Error(`Unknown option: ${value}`);
249
+ }
250
+ }
251
+
252
+ return options;
229
253
  }
230
254
 
231
255
  function printVersion(options) {
@@ -963,7 +987,14 @@ async function main() {
963
987
  return;
964
988
  }
965
989
  if (options.action === "start") {
966
- const result = await startAutoMode({ targetDir: options.targetDir });
990
+ const result = await startAutoMode({
991
+ targetDir: options.targetDir,
992
+ requestedContract: {
993
+ objective: options.requestedObjective,
994
+ campaignKind: options.requestedCampaignKind,
995
+ allowedStages: options.requestedAllowedStages,
996
+ },
997
+ });
967
998
  const verb = result.status.status === "stopped" ? "stopped" : "completed";
968
999
  console.log(`auto mode ${verb} in ${options.targetDir}`);
969
1000
  console.log(`objective: ${result.mode.objective}`);
@@ -43,6 +43,146 @@ const PROMOTION_CANONICAL_FILES = [
43
43
  path.join(".lab", "context", "workflow-state.md"),
44
44
  ];
45
45
 
46
+ function isLocalProcessAlive(ownerId) {
47
+ const pid = parseInteger(ownerId, null);
48
+ if (!Number.isInteger(pid) || pid <= 0) {
49
+ return false;
50
+ }
51
+ try {
52
+ process.kill(pid, 0);
53
+ return true;
54
+ } catch (error) {
55
+ if (error && error.code === "EPERM") {
56
+ return true;
57
+ }
58
+ return false;
59
+ }
60
+ }
61
+
62
+ function inferCampaignKind({ campaignKind = "", allowedStages = [] }) {
63
+ if (isMeaningful(campaignKind)) {
64
+ return campaignKind.trim().toLowerCase();
65
+ }
66
+ const stageSet = new Set((allowedStages || []).map((stage) => stage.trim().toLowerCase()).filter(Boolean));
67
+ const hasPlanning = ["idea", "data", "framing", "spec"].some((stage) => stageSet.has(stage));
68
+ const hasExecution = ["run", "iterate"].some((stage) => stageSet.has(stage));
69
+ const hasWriteOnly = stageSet.has("write") && !hasExecution;
70
+
71
+ if (hasPlanning && !hasExecution) {
72
+ return "spec";
73
+ }
74
+ if (hasExecution) {
75
+ return "experiment-loop";
76
+ }
77
+ if (hasWriteOnly || stageSet.has("report")) {
78
+ return "report-polish";
79
+ }
80
+ return "generic";
81
+ }
82
+
83
+ function normalizeRequestedAutoContract(requested = {}) {
84
+ const allowedStages = Array.isArray(requested.allowedStages)
85
+ ? requested.allowedStages.map((stage) => String(stage).trim().toLowerCase()).filter(Boolean)
86
+ : normalizeList(requested.allowedStages || "").map((stage) => stage.toLowerCase());
87
+ return {
88
+ objective: (requested.objective || "").trim(),
89
+ campaignKind:
90
+ isMeaningful(requested.campaignKind || "") || allowedStages.length > 0
91
+ ? inferCampaignKind({
92
+ campaignKind: requested.campaignKind || "",
93
+ allowedStages,
94
+ })
95
+ : "",
96
+ allowedStages,
97
+ };
98
+ }
99
+
100
+ function sameStageSet(left, right) {
101
+ if (left.length !== right.length) {
102
+ return false;
103
+ }
104
+ const leftSet = new Set(left);
105
+ return right.every((value) => leftSet.has(value));
106
+ }
107
+
108
+ function hasLiveOwner(status, ledger) {
109
+ const normalizedStatus = (status.status || "").trim().toLowerCase();
110
+ const observedState = (ledger.observedState || "").trim().toLowerCase();
111
+ if ((ledger.ownerType || "").trim().toLowerCase() === "local-process" && isLocalProcessAlive(ledger.ownerId)) {
112
+ return true;
113
+ }
114
+ if (["local-runner", "remote-runner"].includes((ledger.ownerType || "").trim().toLowerCase())) {
115
+ return ["running", "retrying", "resuming", "checkpointed"].includes(observedState);
116
+ }
117
+ return normalizedStatus === "running" || ["running", "retrying", "resuming"].includes(observedState);
118
+ }
119
+
120
+ function classifyAutoContractFit({ mode, status, ledger, requested }) {
121
+ const normalizedRequest = normalizeRequestedAutoContract(requested);
122
+ const requestIsEmpty =
123
+ !isMeaningful(normalizedRequest.objective) &&
124
+ !isMeaningful(normalizedRequest.campaignKind) &&
125
+ normalizedRequest.allowedStages.length === 0;
126
+ const currentCampaignKind = inferCampaignKind(mode);
127
+ if (requestIsEmpty) {
128
+ return {
129
+ classification: "fit",
130
+ reason: "no requested contract override",
131
+ currentCampaignKind,
132
+ requestedCampaignKind: "",
133
+ };
134
+ }
135
+
136
+ const requestedCampaignKind = normalizedRequest.campaignKind || currentCampaignKind;
137
+ const requestedStages = normalizedRequest.allowedStages.length > 0
138
+ ? normalizedRequest.allowedStages
139
+ : mode.allowedStages;
140
+ const stageOutsideCurrentEnvelope = requestedStages.some((stage) => !mode.allowedStages.includes(stage));
141
+ const objectiveDiffers =
142
+ isMeaningful(normalizedRequest.objective) &&
143
+ isMeaningful(mode.objective) &&
144
+ normalizedRequest.objective.trim() !== mode.objective.trim();
145
+ const kindDiffers = requestedCampaignKind !== currentCampaignKind;
146
+
147
+ if (hasLiveOwner(status, ledger) && (kindDiffers || stageOutsideCurrentEnvelope || objectiveDiffers)) {
148
+ return {
149
+ classification: "live-conflict",
150
+ reason: "requested campaign conflicts with a live auto campaign",
151
+ currentCampaignKind,
152
+ requestedCampaignKind,
153
+ };
154
+ }
155
+
156
+ if (kindDiffers || stageOutsideCurrentEnvelope) {
157
+ return {
158
+ classification: "hard-mismatch",
159
+ reason: stageOutsideCurrentEnvelope
160
+ ? "requested stages fall outside the current auto-stage envelope"
161
+ : "requested campaign kind differs from the current auto campaign",
162
+ currentCampaignKind,
163
+ requestedCampaignKind,
164
+ };
165
+ }
166
+
167
+ if (!sameStageSet(requestedStages, mode.allowedStages) || objectiveDiffers) {
168
+ return {
169
+ classification: "soft-mismatch",
170
+ reason: !sameStageSet(requestedStages, mode.allowedStages)
171
+ ? "requested stages differ but stay inside the current envelope"
172
+ : "requested objective differs inside the same campaign family",
173
+ currentCampaignKind,
174
+ requestedCampaignKind,
175
+ };
176
+ }
177
+
178
+ return {
179
+ classification: "fit",
180
+ reason: "requested campaign matches current auto contract",
181
+ currentCampaignKind,
182
+ requestedCampaignKind,
183
+ };
184
+ }
185
+
46
186
  function resolveFrozenCoreEntries(rawValue) {
47
187
  const normalized = normalizeList(rawValue);
48
188
  const paths = new Set();
@@ -380,9 +520,14 @@ module.exports = {
380
520
  REVIEW_CONTEXT_FILES,
381
521
  VALID_APPROVAL_STATUSES,
382
522
  VALID_TERMINAL_GOAL_TYPES,
523
+ classifyAutoContractFit,
383
524
  changedSnapshotPaths,
384
525
  detectFrozenCoreChanges,
526
+ hasLiveOwner,
385
527
  hashPathState,
528
+ inferCampaignKind,
529
+ isLocalProcessAlive,
530
+ normalizeRequestedAutoContract,
386
531
  resolveFrozenCoreEntries,
387
532
  resolveStageCommand,
388
533
  snapshotFrozenCore,
@@ -1,4 +1,5 @@
1
1
  const fs = require("node:fs");
2
+ const path = require("node:path");
2
3
  const { spawn } = require("node:child_process");
3
4
  const { refreshContext } = require("./context.cjs");
4
5
  const { parseEvalProtocol, validateEvalProtocol } = require("./eval_protocol.cjs");
@@ -9,7 +10,11 @@ const {
9
10
  sleep,
10
11
  } = require("./auto_common.cjs");
11
12
  const {
13
+ classifyAutoContractFit,
12
14
  detectFrozenCoreChanges,
15
+ inferCampaignKind,
16
+ isLocalProcessAlive,
17
+ normalizeRequestedAutoContract,
13
18
  resolveStageCommand,
14
19
  snapshotFrozenCore,
15
20
  snapshotPaths,
@@ -28,6 +33,7 @@ const {
28
33
  readWorkflowLanguage,
29
34
  resolveRequiredArtifact,
30
35
  writeAutoLedger,
36
+ writeAutoMode,
31
37
  writeAutoOutcome,
32
38
  writeAutoStatus,
33
39
  } = require("./auto_state.cjs");
@@ -48,22 +54,6 @@ function isStopTransition(value) {
48
54
  return ["stop", "campaign-stop", "terminal-stop"].includes((value || "").trim().toLowerCase());
49
55
  }
50
56
 
51
- function isLocalProcessAlive(ownerId) {
52
- const pid = parseInteger(ownerId, null);
53
- if (!Number.isInteger(pid) || pid <= 0) {
54
- return false;
55
- }
56
- try {
57
- process.kill(pid, 0);
58
- return true;
59
- } catch (error) {
60
- if (error && error.code === "EPERM") {
61
- return true;
62
- }
63
- return false;
64
- }
65
- }
66
-
67
57
  function resolveResumePlan({ mode, evalProtocol, status, ledger, now }) {
68
58
  const hasLedgerState = [
69
59
  ledger.campaignId,
@@ -143,6 +133,146 @@ function resolveResumePlan({ mode, evalProtocol, status, ledger, now }) {
143
133
  return { blockingIssue: "", resumePlan: null };
144
134
  }
145
135
 
136
+ function makeCampaignId({ requested, now }) {
137
+ const raw = isMeaningful(requested.campaignKind)
138
+ ? `${requested.campaignKind}-${now.toISOString()}`
139
+ : `auto-${now.toISOString()}`;
140
+ return raw.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
141
+ }
142
+
143
+ function archiveAutoArtifact(targetDir, relativePath, campaignId, now) {
144
+ const absolutePath = path.join(targetDir, relativePath);
145
+ if (!fs.existsSync(absolutePath)) {
146
+ return "";
147
+ }
148
+ const archiveDir = path.join(targetDir, ".lab", "context", "archive");
149
+ fs.mkdirSync(archiveDir, { recursive: true });
150
+ const baseName = path.basename(relativePath);
151
+ const archivePath = path.join(
152
+ archiveDir,
153
+ `${now.toISOString().slice(0, 10)}-${campaignId}-${baseName}`
154
+ );
155
+ fs.copyFileSync(absolutePath, archivePath);
156
+ return archivePath;
157
+ }
158
+
159
+ function buildRolledOverAutoMode(mode, requested, now) {
160
+ const allowedStages = requested.allowedStages.length > 0 ? requested.allowedStages : mode.allowedStages;
161
+ const campaignKind = requested.campaignKind || inferCampaignKind({ allowedStages });
162
+ return {
163
+ campaignId: makeCampaignId({ requested: { ...requested, campaignKind }, now }),
164
+ campaignKind,
165
+ campaignStartedAt: now.toISOString(),
166
+ objective: requested.objective || mode.objective,
167
+ autonomyLevel: mode.autonomyLevel || "l2",
168
+ approvalStatus: "draft",
169
+ allowedStages,
170
+ successCriteria: "",
171
+ terminalGoalType: "",
172
+ terminalGoalTarget: "",
173
+ requiredTerminalArtifact: "",
174
+ primaryGate: "",
175
+ secondaryGuard: "",
176
+ promotionCondition: "",
177
+ stopReason: "",
178
+ escalationReason: "",
179
+ maxIterations: mode.maxIterations,
180
+ maxWallClockTime: mode.maxWallClockTime,
181
+ maxFailures: mode.maxFailures,
182
+ pollInterval: mode.pollInterval,
183
+ stageCommands: {
184
+ run: "",
185
+ iterate: "",
186
+ review: "",
187
+ report: "",
188
+ write: "",
189
+ },
190
+ successCheckCommand: "",
191
+ stopCheckCommand: mode.stopCheckCommand,
192
+ promotionCheckCommand: "",
193
+ promotionCommand: "",
194
+ promotionPolicy: "",
195
+ frozenCore: mode.frozenCore,
196
+ explorationEnvelope: "",
197
+ stopConditions: "",
198
+ escalationConditions: "",
199
+ };
200
+ }
201
+
202
+ function rolloverAutoCampaign({ targetDir, mode, status, ledger, requested, lang, now }) {
203
+ const newMode = buildRolledOverAutoMode(mode, requested, now);
204
+ const archivedPaths = [
205
+ archiveAutoArtifact(targetDir, path.join(".lab", "context", "auto-mode.md"), mode.campaignId || "auto-campaign", now),
206
+ archiveAutoArtifact(targetDir, path.join(".lab", "context", "auto-status.md"), mode.campaignId || "auto-campaign", now),
207
+ ].filter(Boolean);
208
+
209
+ const hasMeaningfulLedger = [
210
+ ledger.campaignId,
211
+ ledger.ownerType,
212
+ ledger.ownerId,
213
+ ledger.observedState,
214
+ ledger.activeRung,
215
+ ].some((value) => isMeaningful(value));
216
+ if (hasMeaningfulLedger) {
217
+ const archivedLedger = archiveAutoArtifact(
218
+ targetDir,
219
+ path.join(".lab", "context", "auto-ledger.md"),
220
+ mode.campaignId || "auto-campaign",
221
+ now
222
+ );
223
+ if (archivedLedger) {
224
+ archivedPaths.push(archivedLedger);
225
+ }
226
+ }
227
+
228
+ writeAutoMode(targetDir, newMode, { lang });
229
+ writeAutoStatus(
230
+ targetDir,
231
+ {
232
+ status: "idle",
233
+ currentStage: "",
234
+ currentCommand: "",
235
+ activeRunId: "",
236
+ iterationCount: "0",
237
+ currentRung: "",
238
+ watchTarget: "",
239
+ nextRung: "",
240
+ startedAt: "",
241
+ lastHeartbeat: now.toISOString(),
242
+ lastCheckpoint: "",
243
+ lastSummary: `rolled over from previous auto campaign${archivedPaths.length > 0 ? `; archived: ${archivedPaths.join(", ")}` : ""}`,
244
+ decision: "fill the new auto-mode contract and approve it before starting the next campaign",
245
+ },
246
+ { lang }
247
+ );
248
+ writeAutoLedger(
249
+ targetDir,
250
+ {
251
+ campaignId: newMode.campaignId,
252
+ objective: newMode.objective,
253
+ activeStage: "",
254
+ activeRung: "",
255
+ ownerType: "",
256
+ ownerId: "",
257
+ command: "",
258
+ watchTarget: "",
259
+ startedAt: "",
260
+ lastObservedAt: now.toISOString(),
261
+ observedState: "draft",
262
+ lastCheckpoint: "",
263
+ checkpointSummary: "new campaign draft created by controlled rollover",
264
+ nextTransition: "",
265
+ continueBoundary: "Fill and approve the new contract before starting the campaign.",
266
+ stopBoundary: "",
267
+ escalationBoundary: "",
268
+ requiredReadSet: ".lab/context/eval-protocol.md, .lab/context/auto-mode.md, .lab/context/auto-status.md, .lab/context/auto-ledger.md, .lab/context/auto-outcome.md",
269
+ resumeCommand: "",
270
+ },
271
+ { lang }
272
+ );
273
+ return { newMode, archivedPaths };
274
+ }
275
+
146
276
  async function runCommandWithPolling({
147
277
  targetDir,
148
278
  stage,
@@ -333,17 +463,51 @@ async function evaluateTerminalGoal({ mode, iteration, targetDir, deadlineMs })
333
463
  };
334
464
  }
335
465
 
336
- async function startAutoMode({ targetDir, now = new Date() }) {
466
+ async function startAutoMode({ targetDir, now = new Date(), requestedContract = null }) {
337
467
  const mode = parseAutoMode(targetDir);
338
468
  const existingStatus = parseAutoStatus(targetDir);
339
469
  const existingLedger = parseAutoLedger(targetDir);
340
470
  const evalProtocol = parseEvalProtocol(targetDir);
471
+ const lang = readWorkflowLanguage(targetDir);
341
472
  const missingSchemaFields = listMissingCurrentAutoModeFields(mode);
342
473
  if (missingSchemaFields.length > 0) {
343
474
  throw new Error(
344
475
  `auto-mode.md is missing current contract fields: ${missingSchemaFields.join(", ")}; run \`superlab update --target ${targetDir}\` to apply the managed schema migration, then fill the new fields before starting auto mode`
345
476
  );
346
477
  }
478
+ if (requestedContract) {
479
+ const fit = classifyAutoContractFit({
480
+ mode,
481
+ status: existingStatus,
482
+ ledger: existingLedger,
483
+ requested: requestedContract,
484
+ });
485
+ if (fit.classification === "live-conflict") {
486
+ throw new Error(
487
+ `requested auto campaign conflicts with the current live campaign; stop the live campaign before rolling over (current: ${fit.currentCampaignKind}, requested: ${fit.requestedCampaignKind})`
488
+ );
489
+ }
490
+ if (fit.classification === "hard-mismatch") {
491
+ const normalizedRequest = normalizeRequestedAutoContract(requestedContract);
492
+ const { newMode, archivedPaths } = rolloverAutoCampaign({
493
+ targetDir,
494
+ mode,
495
+ status: existingStatus,
496
+ ledger: existingLedger,
497
+ requested: normalizedRequest,
498
+ lang,
499
+ now,
500
+ });
501
+ throw new Error(
502
+ `current auto contract does not fit the requested campaign and was rolled over to a new draft (${newMode.campaignId}); archived old campaign files: ${archivedPaths.join(", ")}; fill the new contract fields and approve it before starting auto mode`
503
+ );
504
+ }
505
+ if (fit.classification === "soft-mismatch") {
506
+ throw new Error(
507
+ `current auto contract only partially fits the requested campaign: ${fit.reason}; update auto-mode.md or narrow the request before starting auto mode`
508
+ );
509
+ }
510
+ }
347
511
  const issues = validateAutoMode(mode, null, evalProtocol);
348
512
  if (issues.length > 0) {
349
513
  throw new Error(issues.join(" | "));
@@ -366,7 +530,6 @@ async function startAutoMode({ targetDir, now = new Date() }) {
366
530
  throw new Error(blockingIssue);
367
531
  }
368
532
 
369
- const lang = readWorkflowLanguage(targetDir);
370
533
  const timestamp = now.toISOString();
371
534
  const status = {
372
535
  status: "running",
@@ -16,6 +16,9 @@ function parseAutoMode(targetDir) {
16
16
  return {
17
17
  path: contextFile(targetDir, "auto-mode.md"),
18
18
  text,
19
+ campaignId: extractValue(text, ["Campaign id", "Campaign ID", "活动 id"]),
20
+ campaignKind: normalizeScalar(extractValue(text, ["Campaign kind", "活动类型"])),
21
+ campaignStartedAt: extractValue(text, ["Campaign started at", "活动开始时间"]),
19
22
  objective: extractValue(text, ["Objective", "目标"]),
20
23
  autonomyLevel: normalizeScalar(extractValue(text, ["Autonomy level", "自治级别"])),
21
24
  approvalStatus: normalizeScalar(extractValue(text, ["Approval status", "批准状态"])),
@@ -52,7 +55,10 @@ function parseAutoMode(targetDir) {
52
55
  };
53
56
  }
54
57
 
55
- const CURRENT_AUTO_MODE_SCHEMA_FIELDS = [
58
+ const CURRENT_AUTO_MODE_MIGRATION_FIELDS = [
59
+ ["Campaign id", "campaignId"],
60
+ ["Campaign kind", "campaignKind"],
61
+ ["Campaign started at", "campaignStartedAt"],
56
62
  ["Autonomy level", "autonomyLevel"],
57
63
  ["Approval status", "approvalStatus"],
58
64
  ["Terminal goal type", "terminalGoalType"],
@@ -65,12 +71,22 @@ const CURRENT_AUTO_MODE_SCHEMA_FIELDS = [
65
71
  ["Escalation reason", "escalationReason"],
66
72
  ];
67
73
 
74
+ const CURRENT_AUTO_MODE_SCHEMA_FIELDS = CURRENT_AUTO_MODE_MIGRATION_FIELDS.filter(
75
+ ([label]) => !["Campaign id", "Campaign kind", "Campaign started at"].includes(label)
76
+ );
77
+
68
78
  function listMissingCurrentAutoModeFields(mode) {
69
79
  return CURRENT_AUTO_MODE_SCHEMA_FIELDS
70
80
  .filter(([, key]) => !isMeaningful(mode[key]))
71
81
  .map(([label]) => label);
72
82
  }
73
83
 
84
+ function listMissingMigratedAutoModeFields(mode) {
85
+ return CURRENT_AUTO_MODE_MIGRATION_FIELDS
86
+ .filter(([, key]) => !isMeaningful(mode[key]))
87
+ .map(([label]) => label);
88
+ }
89
+
74
90
  function parseAutoStatus(targetDir) {
75
91
  const text = readFileIfExists(contextFile(targetDir, "auto-status.md"));
76
92
  return {
@@ -392,18 +408,147 @@ function resolveRequiredArtifact(targetDir, configuredPath) {
392
408
  };
393
409
  }
394
410
 
411
+ function renderAutoModeContract(mode, { lang = "en" } = {}) {
412
+ const allowedStages = Array.isArray(mode.allowedStages) ? mode.allowedStages.join(", ") : (mode.allowedStages || "");
413
+ if (lang === "zh") {
414
+ return `# 自动模式契约
415
+
416
+ 用这个文件定义 \`/lab:auto\` 的有边界自治执行范围。
417
+
418
+ ## 目标
419
+
420
+ - Campaign id: ${mode.campaignId || ""}
421
+ - 活动类型: ${mode.campaignKind || ""}
422
+ - 活动开始时间: ${mode.campaignStartedAt || ""}
423
+ - Objective: ${mode.objective || ""}
424
+ - 自治级别: ${mode.autonomyLevel || "L2"}
425
+ - 批准状态: ${mode.approvalStatus || "draft"}
426
+ - 允许阶段: ${allowedStages}
427
+ - 成功标准: ${mode.successCriteria || ""}
428
+ - 终止目标类型: ${mode.terminalGoalType || ""}
429
+ - 终止目标目标值: ${mode.terminalGoalTarget || ""}
430
+ - 终止目标工件: ${mode.requiredTerminalArtifact || ""}
431
+ - 主 gate: ${mode.primaryGate || ""}
432
+ - 次级 guard: ${mode.secondaryGuard || ""}
433
+ - 升格条件: ${mode.promotionCondition || ""}
434
+ - 停止原因: ${mode.stopReason || ""}
435
+ - 升级原因: ${mode.escalationReason || ""}
436
+
437
+ ## 循环预算
438
+
439
+ - Max iterations: ${mode.maxIterations || ""}
440
+ - Max wall-clock time: ${mode.maxWallClockTime || ""}
441
+ - Max failures: ${mode.maxFailures || ""}
442
+ - Poll interval: ${mode.pollInterval || ""}
443
+
444
+ ## 阶段命令
445
+
446
+ - Run command: ${mode.stageCommands?.run || ""}
447
+ - Iterate command: ${mode.stageCommands?.iterate || ""}
448
+ - Review command: ${mode.stageCommands?.review || ""}
449
+ - Report command: ${mode.stageCommands?.report || ""}
450
+ - Write command: ${mode.stageCommands?.write || ""}
451
+ - Success check command: ${mode.successCheckCommand || ""}
452
+ - Stop check command: ${mode.stopCheckCommand || ""}
453
+ - Promotion check command: ${mode.promotionCheckCommand || ""}
454
+ - Promotion command: ${mode.promotionCommand || ""}
455
+
456
+ ## 升格策略
457
+
458
+ - Promotion policy: ${mode.promotionPolicy || ""}
459
+
460
+ ## 边界
461
+
462
+ - Frozen core: ${mode.frozenCore || ""}
463
+ - Exploration envelope: ${mode.explorationEnvelope || ""}
464
+
465
+ ## 停止条件
466
+
467
+ - Stop conditions: ${mode.stopConditions || ""}
468
+ - Escalation conditions: ${mode.escalationConditions || ""}
469
+ `;
470
+ }
471
+
472
+ return `# Auto Mode Contract
473
+
474
+ Use this file to define the bounded autonomous execution envelope for \`/lab:auto\`.
475
+
476
+ ## Objective
477
+
478
+ - Campaign id: ${mode.campaignId || ""}
479
+ - Campaign kind: ${mode.campaignKind || ""}
480
+ - Campaign started at: ${mode.campaignStartedAt || ""}
481
+ - Objective: ${mode.objective || ""}
482
+ - Autonomy level: ${mode.autonomyLevel || "L2"}
483
+ - Approval status: ${mode.approvalStatus || "draft"}
484
+ - Allowed stages: ${allowedStages}
485
+ - Success criteria: ${mode.successCriteria || ""}
486
+ - Terminal goal type: ${mode.terminalGoalType || ""}
487
+ - Terminal goal target: ${mode.terminalGoalTarget || ""}
488
+ - Required terminal artifact: ${mode.requiredTerminalArtifact || ""}
489
+ - Primary gate: ${mode.primaryGate || ""}
490
+ - Secondary guard: ${mode.secondaryGuard || ""}
491
+ - Promotion condition: ${mode.promotionCondition || ""}
492
+ - Stop reason: ${mode.stopReason || ""}
493
+ - Escalation reason: ${mode.escalationReason || ""}
494
+
495
+ ## Loop Budget
496
+
497
+ - Max iterations: ${mode.maxIterations || ""}
498
+ - Max wall-clock time: ${mode.maxWallClockTime || ""}
499
+ - Max failures: ${mode.maxFailures || ""}
500
+ - Poll interval: ${mode.pollInterval || ""}
501
+
502
+ ## Stage Commands
503
+
504
+ - Run command: ${mode.stageCommands?.run || ""}
505
+ - Iterate command: ${mode.stageCommands?.iterate || ""}
506
+ - Review command: ${mode.stageCommands?.review || ""}
507
+ - Report command: ${mode.stageCommands?.report || ""}
508
+ - Write command: ${mode.stageCommands?.write || ""}
509
+ - Success check command: ${mode.successCheckCommand || ""}
510
+ - Stop check command: ${mode.stopCheckCommand || ""}
511
+ - Promotion check command: ${mode.promotionCheckCommand || ""}
512
+ - Promotion command: ${mode.promotionCommand || ""}
513
+
514
+ ## Promotion Policy
515
+
516
+ - Promotion policy: ${mode.promotionPolicy || ""}
517
+
518
+ ## Boundaries
519
+
520
+ - Frozen core: ${mode.frozenCore || ""}
521
+ - Exploration envelope: ${mode.explorationEnvelope || ""}
522
+
523
+ ## Stop Conditions
524
+
525
+ - Stop conditions: ${mode.stopConditions || ""}
526
+ - Escalation conditions: ${mode.escalationConditions || ""}
527
+ `;
528
+ }
529
+
530
+ function writeAutoMode(targetDir, mode, { lang = "en" } = {}) {
531
+ const filePath = contextFile(targetDir, "auto-mode.md");
532
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
533
+ fs.writeFileSync(filePath, renderAutoModeContract(mode, { lang }).trimEnd() + "\n");
534
+ }
535
+
395
536
  module.exports = {
537
+ CURRENT_AUTO_MODE_MIGRATION_FIELDS,
396
538
  CURRENT_AUTO_MODE_SCHEMA_FIELDS,
397
539
  listMissingCurrentAutoModeFields,
540
+ listMissingMigratedAutoModeFields,
398
541
  parseAutoLedger,
399
542
  parseAutoMode,
400
543
  parseAutoStatus,
401
544
  readWorkflowLanguage,
402
545
  renderAutoLedger,
546
+ renderAutoModeContract,
403
547
  renderAutoOutcome,
404
548
  renderAutoStatus,
405
549
  resolveRequiredArtifact,
406
550
  writeAutoLedger,
551
+ writeAutoMode,
407
552
  writeAutoOutcome,
408
553
  writeAutoStatus,
409
554
  };
package/lib/i18n.cjs CHANGED
@@ -2217,7 +2217,7 @@ ZH_CONTENT[path.join(".codex", "prompts", "lab.md")] = ZH_CONTENT[
2217
2217
  `- 始终使用 \`skills/lab/SKILL.md\` 作为工作流合同。\n${zhRecipeQuickPathLine}`
2218
2218
  ).replace(
2219
2219
  "- 用户显式调用 `/lab:<stage>` 时,要立刻执行该 stage,而不是只推荐别的 `/lab` stage。\n",
2220
- "- 用户只要显式调用某个 stage,无论写成 `/lab:<stage>`、`/lab: <stage>`、`/lab <stage>`、`/lab-<stage>` 还是 `/lab:<stage>`,都要立刻执行该 stage,而不是只推荐别的 `/lab` stage。\n"
2220
+ "- 用户只要显式调用某个 stage,无论写成 `/lab:<stage>`、`/lab: <stage>`、`/lab <stage>`、`/lab-<stage>` 还是 `/lab:<stage>`,都要立刻执行该 stage,而不是只推荐别的 `/lab` stage。\n- 如果输入看起来像 stage 请求,但又不属于上述受支持写法,就必须停下并要求用户用精确的 stage 名重述,而不是自己猜。\n"
2221
2221
  );
2222
2222
 
2223
2223
  ZH_CONTENT[path.join(".claude", "commands", "lab.md")] = ZH_CONTENT[
@@ -2230,7 +2230,7 @@ ZH_CONTENT[path.join(".claude", "commands", "lab.md")] = ZH_CONTENT[
2230
2230
  `- 始终使用 \`skills/lab/SKILL.md\` 作为工作流合同。\n${zhRecipeQuickPathLine}`
2231
2231
  ).replace(
2232
2232
  "- 用户显式调用 `/lab <stage> ...` 或 `/lab-<stage>` 时,要立刻执行该 stage,而不是只推荐别的阶段。\n",
2233
- "- 用户只要显式调用某个 stage,无论写成 `/lab:<stage>`、`/lab: <stage>`、`/lab <stage>`、`/lab-<stage>` 还是 `/lab:<stage>`,都要立刻执行该 stage,而不是只推荐别的阶段。\n"
2233
+ "- 用户只要显式调用某个 stage,无论写成 `/lab:<stage>`、`/lab: <stage>`、`/lab <stage>`、`/lab-<stage>` 还是 `/lab:<stage>`,都要立刻执行该 stage,而不是只推荐别的阶段。\n- 如果输入看起来像 stage 请求,但又不属于上述受支持写法,就必须停下并要求用户用精确的 stage 名重述,而不是自己猜。\n"
2234
2234
  );
2235
2235
 
2236
2236
  ZH_CONTENT[path.join(".codex", "skills", "lab", "SKILL.md")] = `---