superlab 0.1.11 → 0.1.12

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 CHANGED
@@ -151,7 +151,7 @@ superlab doctor
151
151
 
152
152
  ## Auto Mode
153
153
 
154
- First fill `.lab/context/auto-mode.md` with the bounded contract, the per-stage commands, and the policy check commands for the campaign, then arm it for the current project:
154
+ First fill `.lab/context/auto-mode.md` with the bounded contract, the per-stage commands, the stage output contracts, and the policy check commands for the campaign, then arm it for the current project:
155
155
 
156
156
  ```bash
157
157
  superlab auto start
@@ -169,7 +169,15 @@ Stop the current auto-mode run:
169
169
  superlab auto stop
170
170
  ```
171
171
 
172
- `/lab:auto` is an orchestration mode layered on top of approved execution stages. It reuses `run`, `iterate`, `review`, `report`, and optional `write` inside the limits defined by `.lab/context/auto-mode.md` and `.lab/context/auto-status.md`. `superlab auto start` runs the configured stage commands in the foreground, polls for completion, enforces `success/stop/promotion` check commands, and guards the configured frozen core. It does not replace manual `idea`, `data`, `framing`, or `spec` decisions.
172
+ `/lab:auto` is an orchestration mode layered on top of approved execution stages. It reuses `run`, `iterate`, `review`, `report`, and optional `write` inside the limits defined by `.lab/context/auto-mode.md` and `.lab/context/auto-status.md`. `superlab auto start` runs the configured stage commands in the foreground, polls for completion, enforces `success/stop/promotion` check commands, guards the configured frozen core, and validates stage-specific contracts:
173
+
174
+ - `run` and `iterate` must change persistent outputs under `results_root`
175
+ - `review` must update canonical review context
176
+ - `report` must write `<deliverables_root>/report.md`
177
+ - `write` must produce LaTeX output under `<deliverables_root>/paper/`
178
+ - a successful promotion must write back into `.lab/context/data-decisions.md`, `.lab/context/decisions.md`, `.lab/context/state.md`, and `.lab/context/session-brief.md`
179
+
180
+ It does not replace manual `idea`, `data`, `framing`, or `spec` decisions.
173
181
 
174
182
  ## Version
175
183
 
package/README.zh-CN.md CHANGED
@@ -149,7 +149,7 @@ superlab doctor
149
149
 
150
150
  ## 自动模式
151
151
 
152
- 先填写 `.lab/context/auto-mode.md`,明确本次自治执行的边界契约、各阶段命令,以及 success/stop/promotion 的检查命令,再启动当前项目的自动模式:
152
+ 先填写 `.lab/context/auto-mode.md`,明确本次自治执行的边界契约、各阶段命令、阶段产物约束,以及 success/stop/promotion 的检查命令,再启动当前项目的自动模式:
153
153
 
154
154
  ```bash
155
155
  superlab auto start
@@ -167,7 +167,15 @@ superlab auto status
167
167
  superlab auto stop
168
168
  ```
169
169
 
170
- `/lab:auto` 是叠加在现有执行阶段之上的编排模式。它会在 `.lab/context/auto-mode.md` 和 `.lab/context/auto-status.md` 的约束下,复用 `run`、`iterate`、`review`、`report`,以及可选的 `write`。`superlab auto start` 会在前台执行这些已配置阶段命令、轮询完成情况,并真正执行 success/stop/promotion 检查命令,同时保护已声明的 frozen core。它不会替代手动的 `idea`、`data`、`framing`、`spec` 决策。
170
+ `/lab:auto` 是叠加在现有执行阶段之上的编排模式。它会在 `.lab/context/auto-mode.md` 和 `.lab/context/auto-status.md` 的约束下,复用 `run`、`iterate`、`review`、`report`,以及可选的 `write`。`superlab auto start` 会在前台执行这些已配置阶段命令、轮询完成情况,并真正执行 success/stop/promotion 检查命令,同时保护已声明的 frozen core,并校验各阶段的产物约束:
171
+
172
+ - `run` 和 `iterate` 必须更新 `results_root` 下的持久输出
173
+ - `review` 必须更新规范的审查上下文
174
+ - `report` 必须写出 `<deliverables_root>/report.md`
175
+ - `write` 必须写出 `<deliverables_root>/paper/` 下的 LaTeX 论文产物
176
+ - promotion 成功后必须写回 `.lab/context/data-decisions.md`、`.lab/context/decisions.md`、`.lab/context/state.md` 和 `.lab/context/session-brief.md`
177
+
178
+ 它不会替代手动的 `idea`、`data`、`framing`、`spec` 决策。
171
179
 
172
180
  ## 版本查询
173
181
 
package/lib/auto.cjs CHANGED
@@ -17,6 +17,18 @@ const FROZEN_CORE_ALIASES = {
17
17
  claims: [path.join(".lab", "context", "terminology-lock.md")],
18
18
  "terminology-lock": [path.join(".lab", "context", "terminology-lock.md")],
19
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
+ ];
20
32
 
21
33
  function contextFile(targetDir, name) {
22
34
  return path.join(targetDir, ".lab", "context", name);
@@ -160,7 +172,15 @@ function hashPathState(filePath) {
160
172
 
161
173
  const stat = fs.statSync(filePath);
162
174
  if (stat.isDirectory()) {
163
- return "__dir__";
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");
164
184
  }
165
185
 
166
186
  return crypto.createHash("sha256").update(fs.readFileSync(filePath)).digest("hex");
@@ -367,6 +387,117 @@ function readWorkflowLanguage(targetDir) {
367
387
  }
368
388
  }
369
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
+
370
501
  async function runCommandWithPolling({ targetDir, stage, command, pollIntervalMs, deadlineMs, startedAt, status, lang }) {
371
502
  const child = spawn(command, {
372
503
  cwd: targetDir,
@@ -564,6 +695,79 @@ async function startAutoMode({ targetDir, now = new Date() }) {
564
695
  throw new Error(message);
565
696
  };
566
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
+
567
771
  const executeStage = async (stage) => {
568
772
  const command = mode.stageCommands[stage];
569
773
  if (!isMeaningful(command)) {
@@ -573,16 +777,11 @@ async function startAutoMode({ targetDir, now = new Date() }) {
573
777
  let stageCompleted = false;
574
778
  while (!stageCompleted) {
575
779
  try {
576
- await runCommandWithPolling({
577
- targetDir,
578
- stage,
579
- command,
580
- pollIntervalMs,
581
- deadlineMs,
582
- startedAt,
583
- status: currentStatus,
584
- lang,
585
- });
780
+ const executor = stageExecutors[stage];
781
+ if (!executor) {
782
+ throw new Error(`unsupported auto stage executor: ${stage}`);
783
+ }
784
+ await executor();
586
785
  executedStages.push(stage);
587
786
  writeRunningStatus({
588
787
  currentStage: stage,
@@ -653,6 +852,7 @@ async function startAutoMode({ targetDir, now = new Date() }) {
653
852
  deadlineMs,
654
853
  });
655
854
  if (promotionCheck.matched) {
855
+ const promotionSnapshot = snapshotPaths(targetDir, PROMOTION_CANONICAL_FILES);
656
856
  await runCommandWithPolling({
657
857
  targetDir,
658
858
  stage: "promotion",
@@ -663,17 +863,18 @@ async function startAutoMode({ targetDir, now = new Date() }) {
663
863
  status: currentStatus,
664
864
  lang,
665
865
  });
866
+ writeRunningStatus({
867
+ currentStage: stagesPerIteration.at(-1) || currentStatus.currentStage,
868
+ currentCommand: mode.promotionCommand,
869
+ decision: `promotion policy matched after iteration ${iteration}`,
870
+ });
666
871
  promotionApplied = true;
667
872
  refreshContext({ targetDir });
873
+ verifyPromotionWriteback(targetDir, promotionSnapshot);
668
874
  const frozenCoreChangesAfterPromotion = detectFrozenCoreChanges(frozenCoreSnapshot);
669
875
  if (frozenCoreChangesAfterPromotion.length > 0) {
670
876
  failAutoMode(`frozen core changed: ${frozenCoreChangesAfterPromotion.join(", ")}`);
671
877
  }
672
- writeRunningStatus({
673
- currentStage: stagesPerIteration.at(-1) || currentStatus.currentStage,
674
- currentCommand: mode.promotionCommand,
675
- decision: `promotion policy matched after iteration ${iteration}`,
676
- });
677
878
  }
678
879
  }
679
880
 
package/lib/i18n.cjs CHANGED
@@ -941,6 +941,14 @@ const ZH_SKILL_FILES = {
941
941
  - Promotion check command:
942
942
  - Promotion command:
943
943
 
944
+ ## 阶段产物约束
945
+
946
+ - Run stage contract: write persistent outputs under \`results_root\`.
947
+ - Iterate stage contract: update persistent outputs under \`results_root\`.
948
+ - Review stage contract: update canonical review context such as \`.lab/context/decisions.md\`、\`state.md\`、\`open-questions.md\` or \`evidence-index.md\`.
949
+ - Report stage contract: write the final report to \`<deliverables_root>/report.md\`.
950
+ - Write stage contract: write LaTeX output under \`<deliverables_root>/paper/\`.
951
+
944
952
  ## 升格策略
945
953
 
946
954
  - Promotion policy:
@@ -954,6 +962,7 @@ const ZH_SKILL_FILES = {
954
962
 
955
963
  - Stop conditions:
956
964
  - Escalation conditions:
965
+ - Canonical promotion writeback: update \`.lab/context/data-decisions.md\`、\`.lab/context/decisions.md\`、\`.lab/context/state.md\` and \`.lab/context/session-brief.md\`.
957
966
  `,
958
967
  [path.join(".lab", "context", "auto-status.md")]:
959
968
  `# 自动模式状态
@@ -1794,6 +1803,12 @@ ZH_CONTENT[path.join(".codex", "skills", "lab", "stages", "auto.md")] = `# \`/la
1794
1803
  - 可以在 exploration envelope 内增加数据集、benchmark 和 comparison methods。
1795
1804
  - 只有在 auto-mode 契约中的升格策略满足时,才允许把 exploratory addition 自动升格为 primary package。
1796
1805
  - 长任务必须通过轮询推进,直到完成、超时或命中停止条件。
1806
+ - 不要只看命令退出码;必须检查阶段产物约束:
1807
+ - \`run\` 和 \`iterate\` 更新 \`results_root\`
1808
+ - \`review\` 更新规范审查上下文
1809
+ - \`report\` 写出 \`<deliverables_root>/report.md\`
1810
+ - \`write\` 写出 \`<deliverables_root>/paper/\` 下的 LaTeX 产物
1811
+ - promotion 成功后,必须写回 \`data-decisions.md\`、\`decisions.md\`、\`state.md\` 和 \`session-brief.md\`。
1797
1812
 
1798
1813
  ## 最小流程
1799
1814
 
@@ -27,6 +27,14 @@ Use this file to define the bounded autonomous execution envelope for `/lab:auto
27
27
  - Promotion check command:
28
28
  - Promotion command:
29
29
 
30
+ ## Stage Output Contracts
31
+
32
+ - Run stage contract: write persistent outputs under `results_root`.
33
+ - Iterate stage contract: update persistent outputs under `results_root`.
34
+ - Review stage contract: update canonical review context such as `.lab/context/decisions.md`, `state.md`, `open-questions.md`, or `evidence-index.md`.
35
+ - Report stage contract: write the final report to `<deliverables_root>/report.md`.
36
+ - Write stage contract: write LaTeX output under `<deliverables_root>/paper/`.
37
+
30
38
  ## Promotion Policy
31
39
 
32
40
  - Promotion policy:
@@ -40,3 +48,4 @@ Use this file to define the bounded autonomous execution envelope for `/lab:auto
40
48
 
41
49
  - Stop conditions:
42
50
  - Escalation conditions:
51
+ - Canonical promotion writeback: update `.lab/context/data-decisions.md`, `.lab/context/decisions.md`, `.lab/context/state.md`, and `.lab/context/session-brief.md`.
@@ -40,6 +40,12 @@
40
40
  - Poll long-running commands until they finish, hit a timeout, or hit a stop condition.
41
41
  - Keep a poll-based waiting loop instead of sleeping blindly.
42
42
  - Reuse the existing `/lab:run`, `/lab:iterate`, `/lab:review`, `/lab:report`, and optional `/lab:write` contracts instead of inventing a parallel workflow.
43
+ - Enforce stage contracts, not just exit codes:
44
+ - `run` and `iterate` must change persistent outputs under `results_root`
45
+ - `review` must update canonical review context
46
+ - `report` must produce `<deliverables_root>/report.md`
47
+ - `write` must produce LaTeX output under `<deliverables_root>/paper/`
48
+ - Treat promotion as incomplete unless it writes back to `data-decisions.md`, `decisions.md`, `state.md`, and `session-brief.md`.
43
49
 
44
50
  ## Minimum Procedure
45
51
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "superlab",
3
- "version": "0.1.11",
3
+ "version": "0.1.12",
4
4
  "description": "Strict /lab research workflow installer for Codex and Claude",
5
5
  "keywords": [
6
6
  "codex",