@accelerator-mzq/forge 1.4.0 → 2.0.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.
Files changed (59) hide show
  1. package/dist/cli/commands/archive.d.ts +73 -10
  2. package/dist/cli/commands/archive.d.ts.map +1 -1
  3. package/dist/cli/commands/archive.js +333 -85
  4. package/dist/cli/commands/archive.js.map +1 -1
  5. package/dist/cli/commands/legacy-bridge.d.ts +110 -0
  6. package/dist/cli/commands/legacy-bridge.d.ts.map +1 -1
  7. package/dist/cli/commands/legacy-bridge.js +1058 -438
  8. package/dist/cli/commands/legacy-bridge.js.map +1 -1
  9. package/dist/cli/index.js +0 -0
  10. package/dist/core/backlog/assets/backlog-readme.md +4 -2
  11. package/dist/core/backlog/index.d.ts +1 -1
  12. package/dist/core/backlog/index.d.ts.map +1 -1
  13. package/dist/core/backlog/index.js +27 -8
  14. package/dist/core/backlog/index.js.map +1 -1
  15. package/dist/core/backlog/render.d.ts +48 -3
  16. package/dist/core/backlog/render.d.ts.map +1 -1
  17. package/dist/core/backlog/render.js +151 -15
  18. package/dist/core/backlog/render.js.map +1 -1
  19. package/dist/core/legacy-bridge/extractor.d.ts +79 -0
  20. package/dist/core/legacy-bridge/extractor.d.ts.map +1 -0
  21. package/dist/core/legacy-bridge/extractor.js +374 -0
  22. package/dist/core/legacy-bridge/extractor.js.map +1 -0
  23. package/dist/core/legacy-bridge/indexer.d.ts +8 -2
  24. package/dist/core/legacy-bridge/indexer.d.ts.map +1 -1
  25. package/dist/core/legacy-bridge/indexer.js +61 -9
  26. package/dist/core/legacy-bridge/indexer.js.map +1 -1
  27. package/dist/core/legacy-bridge/legacy-requirements.d.ts +53 -0
  28. package/dist/core/legacy-bridge/legacy-requirements.d.ts.map +1 -0
  29. package/dist/core/legacy-bridge/legacy-requirements.js +114 -0
  30. package/dist/core/legacy-bridge/legacy-requirements.js.map +1 -0
  31. package/dist/core/legacy-bridge/llm-task.d.ts +54 -0
  32. package/dist/core/legacy-bridge/llm-task.d.ts.map +1 -0
  33. package/dist/core/legacy-bridge/llm-task.js +87 -0
  34. package/dist/core/legacy-bridge/llm-task.js.map +1 -0
  35. package/dist/core/legacy-bridge/mapper.d.ts +8 -9
  36. package/dist/core/legacy-bridge/mapper.d.ts.map +1 -1
  37. package/dist/core/legacy-bridge/mapper.js +24 -39
  38. package/dist/core/legacy-bridge/mapper.js.map +1 -1
  39. package/dist/core/legacy-bridge/quality-judge.d.ts +5 -0
  40. package/dist/core/legacy-bridge/quality-judge.d.ts.map +1 -1
  41. package/dist/core/legacy-bridge/quality-judge.js +25 -21
  42. package/dist/core/legacy-bridge/quality-judge.js.map +1 -1
  43. package/dist/core/legacy-bridge/regenerator.d.ts +21 -1
  44. package/dist/core/legacy-bridge/regenerator.d.ts.map +1 -1
  45. package/dist/core/legacy-bridge/regenerator.js +132 -2
  46. package/dist/core/legacy-bridge/regenerator.js.map +1 -1
  47. package/dist/core/legacy-bridge/runners.d.ts +33 -0
  48. package/dist/core/legacy-bridge/runners.d.ts.map +1 -0
  49. package/dist/core/legacy-bridge/runners.js +77 -0
  50. package/dist/core/legacy-bridge/runners.js.map +1 -0
  51. package/dist/core/legacy-bridge/sync-check.d.ts +8 -0
  52. package/dist/core/legacy-bridge/sync-check.d.ts.map +1 -1
  53. package/dist/core/legacy-bridge/sync-check.js +83 -0
  54. package/dist/core/legacy-bridge/sync-check.js.map +1 -1
  55. package/dist/core/legacy-bridge/types.d.ts +2 -0
  56. package/dist/core/legacy-bridge/types.d.ts.map +1 -1
  57. package/dist/core/templates/commands/archive.md +15 -0
  58. package/dist/core/templates/skills/legacy-bridge-fulfillment.md +22 -0
  59. package/package.json +21 -20
@@ -1,11 +1,16 @@
1
1
  import { Command } from 'commander';
2
+ import type { SyncCheckInput } from '../../core/legacy-bridge/sync-check.js';
2
3
  export declare function buildArchiveCommand(): Command;
3
4
  /**
4
5
  * preflight 结果 union — caller 在 try 块内根据 kind 手动 release + exit,
5
6
  * 与 archive.ts 现有"business-rule fail 处理"convention 一致(line 154 / 160 等)。
6
7
  *
7
- * kind 'ok':可继续 archive(graceful skip 全过)
8
- * kind 'ack-missing' / 'critical-pending':caller 应 await release + exit 2
8
+ * kind 'ok':可继续 archive(graceful skip / 无 affected anchor / --api 跑完无 critical)
9
+ * kind 'ack-missing':ack 未就绪,caller 应 await release + exit 2
10
+ * kind 'critical-pending':**仅 --api 模式产生** —— 进程内跑 sync-check 后发现
11
+ * critical pending 差异,sync-state 已写,caller 应 await release + exit 2
12
+ * kind 'halted-for-fulfillment':**仅 agent 模式产生** —— emit manifest +
13
+ * 暂停态文件已写,archive 应 halt,caller await release + exit 2
9
14
  */
10
15
  export type PreflightResult = {
11
16
  kind: 'ok';
@@ -16,29 +21,87 @@ export type PreflightResult = {
16
21
  kind: 'critical-pending';
17
22
  criticalCount: number;
18
23
  message: string;
24
+ } | {
25
+ kind: 'halted-for-fulfillment';
26
+ message: string;
19
27
  };
20
28
  /**
21
- * Plan 7 §2.5:archive preflight — enforce_sync=true 时阻塞 critical 差异。
29
+ * Plan 7 §2.5 + Task 6.3:archive preflight — enforce_sync=true 时拦截 sync 差异。
22
30
  *
23
31
  * Graceful skip 路径(全部返 {kind:'ok'}):
24
32
  * 1. forge/config.yaml 不存在
25
33
  * 2. legacy-anchors.yaml 不存在
26
34
  * 3. legacy_bridge.allow_llm_calls != true
27
35
  * 4. legacy_bridge.enforce_sync != true
36
+ * 5. 无受影响 anchor / 全部读取失败(buildSyncCheckTask 返 null)
37
+ *
38
+ * 双模式(ack 就绪后):
39
+ * - **agent 模式(默认,不带 --api)**:emit sync-check manifest 到 .cache +
40
+ * 写暂停态文件 → 返 {kind:'halted-for-fulfillment'},archive halt 等 agent fulfill。
41
+ * - **--api 模式**:进程内直连 Anthropic API 跑 sync-check + 写 sync-state →
42
+ * 含 critical pending 返 {kind:'critical-pending'};否则返 {kind:'ok'} 续跑 archive。
28
43
  *
29
- * LLM 路径时:
30
- * - ack 不就绪 → 返 {kind:'ack-missing'}
31
- * - 含 critical pending → sync-state 已写后返 {kind:'critical-pending'}
32
- * - 全过 → 返 {kind:'ok'}
44
+ * ack 不就绪(两模式共有)→ 返 {kind:'ack-missing'}。
33
45
  *
34
46
  * **不再调 process.exit / console.error**:caller(archive 命令)负责打印消息 +
35
47
  * release archive.lock + exit,避免本函数内 process.exit 跳过 caller finally
36
48
  * 导致 archive.lock 文件残留。
37
49
  */
38
- export declare function runArchivePreflight(forgeRoot: string, changeId: string): Promise<PreflightResult>;
50
+ export declare function runArchivePreflight(forgeRoot: string, changeId: string, opts?: {
51
+ api: boolean;
52
+ }): Promise<PreflightResult>;
39
53
  /**
40
- * Plan 7 §2.5:post-archive hook — 不阻塞,仅在 enforce_sync=false 时跑(产报告)。
54
+ * Plan 7 §2.5 + Task 6.3:post-archive hook — 不阻塞,仅在 enforce_sync=false 时跑(产报告)。
41
55
  * enforce_sync=true 已由 preflight 跑过;graceful skip 路径同 preflight。
56
+ *
57
+ * agent 模式:emit sync-check manifest(非阻塞,archive 已完成,fulfill 后跑 --apply 出报告)。
58
+ * --api 模式:进程内直连 API 跑 sync-check + 写 sync-state(非阻塞,直接出报告)。
59
+ *
60
+ * I-2:archiveDate 由 archive 主流程透传(不自己算 new Date(),避免跨午夜偏一天)。
61
+ */
62
+ export declare function runArchivePostHook(forgeRoot: string, changeId: string, opts: {
63
+ archiveDate: string;
64
+ api: boolean;
65
+ }): Promise<void>;
66
+ /**
67
+ * agent 模式:emit preflight sync-check manifest + 暂停态文件,halt archive(Task 6.3)。
68
+ *
69
+ * 调用前 gate 检查(config 存在 + allow_llm_calls + enforce_sync + anchors + ack)由
70
+ * runArchivePreflight 负责;本函数专注 emit 逻辑。
71
+ *
72
+ * @param ctx 已拼好的 SyncCheckInput(可选;不传则内部自拼,供测试直接调用)
42
73
  */
43
- export declare function runArchivePostHook(forgeRoot: string, changeId: string): Promise<void>;
74
+ export declare function emitPreflightSyncCheck(forgeRoot: string, changeId: string, ctx?: SyncCheckInput): Promise<{
75
+ kind: 'halted-for-fulfillment' | 'skip';
76
+ }>;
77
+ /**
78
+ * agent 模式:enforce_sync=false 时 post-archive emit sync-check manifest(非阻塞,无暂停态)(Task 6.3)。
79
+ *
80
+ * archive 已完成;fulfill 后跑 forge legacy-bridge sync-check --apply 出报告。
81
+ * 调用前 gate 检查由 runArchivePostHook 负责。
82
+ *
83
+ * @param ctx 已拼好的 SyncCheckInput(可选;不传则内部自拼,供测试直接调用)
84
+ */
85
+ export declare function emitPostHookSyncCheck(forgeRoot: string, changeId: string, ctx?: SyncCheckInput): Promise<{
86
+ kind: 'emitted' | 'skip';
87
+ }>;
88
+ /**
89
+ * Task 6.4:forge archive --resume 的 gate 复核。
90
+ *
91
+ * 复核两项:
92
+ * ① 产物绑定 — sync-state 的 produced_from 必须等于暂停态文件的 manifest_hash,
93
+ * 确保 sync-check --apply 产出的 sync-state 确实来自本次 preflight emit 的 manifest。
94
+ * ② critical 幂等重评 — sync-state 中不得有 severity=critical + status=pending 的差异;
95
+ * 若有,说明 agent 未完全 resolve,不可续跑 archive。
96
+ *
97
+ * @param forgeRoot forge/ 根目录路径
98
+ * @param changeId change id(e.g. 'add-pay')
99
+ * @returns { ok: true } 表示 gate 通过;{ ok: false, reason } 表示拒绝并附原因
100
+ */
101
+ export declare function resumeArchiveGateCheck(forgeRoot: string, changeId: string): Promise<{
102
+ ok: true;
103
+ } | {
104
+ ok: false;
105
+ reason: string;
106
+ }>;
44
107
  //# sourceMappingURL=archive.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"archive.d.ts","sourceRoot":"","sources":["../../../src/cli/commands/archive.ts"],"names":[],"mappings":"AAOA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAuEpC,wBAAgB,mBAAmB,IAAI,OAAO,CAihB7C;AAkBD;;;;;;GAMG;AACH,MAAM,MAAM,eAAe,GACvB;IAAE,IAAI,EAAE,IAAI,CAAA;CAAE,GACd;IAAE,IAAI,EAAE,aAAa,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,GACxC;IAAE,IAAI,EAAE,kBAAkB,CAAC;IAAC,aAAa,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC;AAEzE;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAsB,mBAAmB,CACvC,SAAS,EAAE,MAAM,EACjB,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,eAAe,CAAC,CAwF1B;AAED;;;GAGG;AACH,wBAAsB,kBAAkB,CAAC,SAAS,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAkE3F"}
1
+ {"version":3,"file":"archive.d.ts","sourceRoot":"","sources":["../../../src/cli/commands/archive.ts"],"names":[],"mappings":"AAOA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAsBpC,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,wCAAwC,CAAC;AAqD7E,wBAAgB,mBAAmB,IAAI,OAAO,CAglB7C;AAkBD;;;;;;;;;;GAUG;AACH,MAAM,MAAM,eAAe,GACvB;IAAE,IAAI,EAAE,IAAI,CAAA;CAAE,GACd;IAAE,IAAI,EAAE,aAAa,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,GACxC;IAAE,IAAI,EAAE,kBAAkB,CAAC;IAAC,aAAa,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,GACpE;IAAE,IAAI,EAAE,wBAAwB,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC;AAExD;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAsB,mBAAmB,CACvC,SAAS,EAAE,MAAM,EACjB,QAAQ,EAAE,MAAM,EAChB,IAAI,GAAE;IAAE,GAAG,EAAE,OAAO,CAAA;CAAmB,GACtC,OAAO,CAAC,eAAe,CAAC,CAiF1B;AAED;;;;;;;;GAQG;AACH,wBAAsB,kBAAkB,CACtC,SAAS,EAAE,MAAM,EACjB,QAAQ,EAAE,MAAM,EAChB,IAAI,EAAE;IAAE,WAAW,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,OAAO,CAAA;CAAE,GAC1C,OAAO,CAAC,IAAI,CAAC,CA6Df;AA0DD;;;;;;;GAOG;AACH,wBAAsB,sBAAsB,CAC1C,SAAS,EAAE,MAAM,EACjB,QAAQ,EAAE,MAAM,EAChB,GAAG,CAAC,EAAE,cAAc,GACnB,OAAO,CAAC;IAAE,IAAI,EAAE,wBAAwB,GAAG,MAAM,CAAA;CAAE,CAAC,CAgCtD;AAED;;;;;;;GAOG;AACH,wBAAsB,qBAAqB,CACzC,SAAS,EAAE,MAAM,EACjB,QAAQ,EAAE,MAAM,EAChB,GAAG,CAAC,EAAE,cAAc,GACnB,OAAO,CAAC;IAAE,IAAI,EAAE,SAAS,GAAG,MAAM,CAAA;CAAE,CAAC,CAevC;AA8BD;;;;;;;;;;;;GAYG;AACH,wBAAsB,sBAAsB,CAC1C,SAAS,EAAE,MAAM,EACjB,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC;IAAE,EAAE,EAAE,IAAI,CAAA;CAAE,GAAG;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC,CA+DvD"}
@@ -10,7 +10,6 @@ import { existsSync, statSync } from 'node:fs';
10
10
  import { execFileSync } from 'node:child_process';
11
11
  import { join } from 'node:path';
12
12
  import { parse as parseYaml } from 'yaml';
13
- import Anthropic from '@anthropic-ai/sdk';
14
13
  import { parseMarker } from '../../core/markers/index.js';
15
14
  import { validateMarkerSchema } from '../../core/validate/index.js';
16
15
  import { validateEvidence, validateReviewGitIntegrity, validateReviewOutcomes, } from '../../core/validate/marker-integrity.js';
@@ -22,9 +21,11 @@ import { promptRecoverChoice } from '../../core/archive/recover-prompt.js';
22
21
  import { applyDeltas } from '../../core/specs-sync/index.js';
23
22
  import { loadAnchorsFile } from '../../core/legacy-bridge/anchors.js';
24
23
  import { checkAck } from '../../core/legacy-bridge/ack.js';
25
- import { runSyncCheck } from '../../core/legacy-bridge/sync-check.js';
26
- import { renderDiffMarkdown, renderDiffYaml, hasCriticalPending, } from '../../core/legacy-bridge/diff-report.js';
24
+ import { buildSyncCheckTask, applySyncCheckResult } from '../../core/legacy-bridge/sync-check.js';
25
+ import { AgentHandoffRunner, ApiRunner, makeForgeApiClient, } from '../../core/legacy-bridge/runners.js';
26
+ import { renderDiffMarkdown, renderDiffYaml } from '../../core/legacy-bridge/diff-report.js';
27
27
  import { readAnchorFile } from '../../core/legacy-bridge/encoding.js';
28
+ import { FORGE_VERSION } from '../../index.js';
28
29
  // Task 8 (plan-9a §9): cross-cutting fence framework — 9g 实施完整 13 不变量逻辑
29
30
  import { crossCuttingFenceCheck } from '../../core/archive/fence.js';
30
31
  // plan-9d Task 6:verify_findings fence + ack-log consistency
@@ -63,12 +64,17 @@ function isProjectActuallyGit(cwd) {
63
64
  }
64
65
  }
65
66
  export function buildArchiveCommand() {
66
- return new Command('archive')
67
+ return (new Command('archive')
67
68
  .argument('[changeId]', 'change directory id (e.g., add-login)')
68
69
  .description('Archive a verified+reviewed change')
69
70
  .option('--force', 'accept human-override or non-git review markers')
70
71
  .option('--recover', 'recover from a half-completed archive transaction')
71
72
  .option('--resume-summary <archiveId>', 'resume rename archive_summary.tmp.yaml → archive_summary.yaml (rare half-completed state, plan-9e1)')
73
+ // Task 6.3:--api 模式 — preflight/posthook 的 sync-check 进程内直连 API 跑完,
74
+ // 不 emit manifest、不写暂停态、不 halt(高保证 CI 场景绕过 agent-pause)
75
+ .option('--api', '直连 Anthropic API 跑 sync-check(不 emit manifest、不暂停)')
76
+ // Task 6.4:--resume — agent 履行 manifest 并跑 sync-check --apply 后,续跑 archive 主流程
77
+ .option('--resume', 'agent 履行后续跑 archive(gate 复核:produced_from 绑定 + critical 幂等重评)')
72
78
  .action(async (changeId, opts) => {
73
79
  const forgeRoot = join(process.cwd(), 'forge');
74
80
  // —— --recover 独立路径 ——
@@ -189,6 +195,28 @@ export function buildArchiveCommand() {
189
195
  await release();
190
196
  }
191
197
  }
198
+ // —— Task 6.4:--resume 独立路径(agent 履行后 gate 复核 + 续跑 archive)——
199
+ // I-2:暂停态文件此处不删 —— 仅记下路径,延后到 archiveTransaction 成功后才删。
200
+ // 若 gate 通过但后续 marker check / fence / archiveTransaction 任一失败,
201
+ // 暂停态保留 → 用户修好后可再 `forge archive --resume` 重入(否则会因
202
+ // 「无暂停态文件」被拒,--resume 失去重入性陷入死角)。
203
+ let resumePausePath;
204
+ if (opts.resume) {
205
+ if (!changeId) {
206
+ console.error('✗ --resume 需要 changeId');
207
+ process.exit(1);
208
+ }
209
+ // gate 复核:produced_from 绑定 + critical 幂等重评
210
+ const gateResult = await resumeArchiveGateCheck(forgeRoot, changeId);
211
+ if (!gateResult.ok) {
212
+ console.error(`✗ --resume gate 复核失败:${gateResult.reason}`);
213
+ process.exit(2); // business-rule-fail
214
+ }
215
+ // gate 通过 → 记下暂停态路径(延后删),fall-through 续跑正常 archive 主流程
216
+ resumePausePath = join(forgeRoot, '.cache', `archive-pause-${changeId}.json`);
217
+ console.log(`✓ --resume gate 通过;续跑 archive ${changeId}…`);
218
+ // --resume 续跑后,opts.resume=true 不影响后续 opts 读取(changeId 已确认存在)
219
+ }
192
220
  // —— 正常 archive 路径 ——
193
221
  if (!changeId) {
194
222
  console.error('✗ changeId required (unless using --recover)');
@@ -203,11 +231,18 @@ export function buildArchiveCommand() {
203
231
  // 决策 #23:复用 archive.lock,不再 acquire legacy-bridge.lock。
204
232
  // preflight 不再 process.exit,改返 PreflightResult,caller 在 try 块内手动 release+exit
205
233
  // 与 marker check 现有 inline-release convention 一致(避免 process.exit 跳过 finally 致锁残留)
206
- const preflightResult = await runArchivePreflight(forgeRoot, changeId);
207
- if (preflightResult.kind !== 'ok') {
208
- console.error(preflightResult.message);
209
- await archiveRelease();
210
- process.exit(2);
234
+ // Task 6.3:--api 透传 —— agent 模式 emit manifest+halt;--api 模式进程内跑 sync-check
235
+ // Task 6.4:--resume 时跳过 preflight — gate 复核已在 --resume 分支完成(暂停态也已清除),
236
+ // 再跑 preflight 会因 enforce_sync=true 再次 emit manifest + halt,造成死循环。
237
+ if (!opts.resume) {
238
+ const preflightResult = await runArchivePreflight(forgeRoot, changeId, {
239
+ api: opts.api ?? false,
240
+ });
241
+ if (preflightResult.kind !== 'ok') {
242
+ console.error(preflightResult.message);
243
+ await archiveRelease();
244
+ process.exit(2);
245
+ }
211
246
  }
212
247
  // plan-9g 默认开启 14 不变量 fence(brainstorm v6 删两 opt-in flag);所有 archive 调用都跑
213
248
  const fenceResult = await crossCuttingFenceCheck(join(forgeRoot, 'changes', changeId));
@@ -475,8 +510,25 @@ export function buildArchiveCommand() {
475
510
  }
476
511
  // 步骤 5:调 archiveTransaction(Move→Sync,含 .tmp 写 / rename / 回滚)
477
512
  await archiveTransaction({ forgeRoot, changeId, archiveDate, archiveSummary });
513
+ // I-2:archiveTransaction await 成功返回(失败会 throw 走外层 catch)即 transaction 成功点 ——
514
+ // 此处删 --resume 暂停态文件(正常收尾)。transaction 之前任一步失败时暂停态保留,
515
+ // 用户修好后可再 --resume 重入。仅 opts.resume 路径有此文件,非 resume 路径 undefined 跳过。
516
+ if (resumePausePath) {
517
+ // transaction 已成功;删暂停态失败(罕见权限错)不应让 archive 报错 —— 降级为 warn
518
+ try {
519
+ await rm(resumePausePath, { force: true });
520
+ }
521
+ catch (e) {
522
+ console.warn(`⚠ archive 已完成,但暂停态文件 ${resumePausePath} 清理失败(可手动删除):${e.message}`);
523
+ }
524
+ }
478
525
  // 步骤 5.5:Plan 7 post-archive hook(enforce_sync=false 时不阻塞,只产报告)
479
- await runArchivePostHook(forgeRoot, changeId);
526
+ // Task 6.3 I-2:透传 archiveDate(避免 posthook 自己算 new Date() 跨午夜偏一天)
527
+ // Task 6.3 C-1:透传 --api
528
+ await runArchivePostHook(forgeRoot, changeId, {
529
+ archiveDate,
530
+ api: opts.api ?? false,
531
+ });
480
532
  // 步骤 6:plan-9e1 Task 4 — 渲染 archive_summary 输出(沿 design §2.4.4)
481
533
  const archiveDirName = `${archiveDate}-${changeId}`;
482
534
  console.log(renderArchiveSummaryOutput(archiveSummary, archiveDirName));
@@ -526,7 +578,7 @@ export function buildArchiveCommand() {
526
578
  if (archiveRelease)
527
579
  await archiveRelease();
528
580
  }
529
- });
581
+ }));
530
582
  }
531
583
  /**
532
584
  * 从 backup 目录把所有备份的 specs 文件还原到 currentSpecsDir(case C [2] 撤销归档用)。
@@ -545,24 +597,28 @@ async function restoreSpecsFromBackup(backupDir, currentSpecsDir) {
545
597
  }
546
598
  }
547
599
  /**
548
- * Plan 7 §2.5:archive preflight — enforce_sync=true 时阻塞 critical 差异。
600
+ * Plan 7 §2.5 + Task 6.3:archive preflight — enforce_sync=true 时拦截 sync 差异。
549
601
  *
550
602
  * Graceful skip 路径(全部返 {kind:'ok'}):
551
603
  * 1. forge/config.yaml 不存在
552
604
  * 2. legacy-anchors.yaml 不存在
553
605
  * 3. legacy_bridge.allow_llm_calls != true
554
606
  * 4. legacy_bridge.enforce_sync != true
607
+ * 5. 无受影响 anchor / 全部读取失败(buildSyncCheckTask 返 null)
608
+ *
609
+ * 双模式(ack 就绪后):
610
+ * - **agent 模式(默认,不带 --api)**:emit sync-check manifest 到 .cache +
611
+ * 写暂停态文件 → 返 {kind:'halted-for-fulfillment'},archive halt 等 agent fulfill。
612
+ * - **--api 模式**:进程内直连 Anthropic API 跑 sync-check + 写 sync-state →
613
+ * 含 critical pending 返 {kind:'critical-pending'};否则返 {kind:'ok'} 续跑 archive。
555
614
  *
556
- * LLM 路径时:
557
- * - ack 不就绪 → 返 {kind:'ack-missing'}
558
- * - 含 critical pending → sync-state 已写后返 {kind:'critical-pending'}
559
- * - 全过 → 返 {kind:'ok'}
615
+ * ack 不就绪(两模式共有)→ 返 {kind:'ack-missing'}。
560
616
  *
561
617
  * **不再调 process.exit / console.error**:caller(archive 命令)负责打印消息 +
562
618
  * release archive.lock + exit,避免本函数内 process.exit 跳过 caller finally
563
619
  * 导致 archive.lock 文件残留。
564
620
  */
565
- export async function runArchivePreflight(forgeRoot, changeId) {
621
+ export async function runArchivePreflight(forgeRoot, changeId, opts = { api: false }) {
566
622
  const configPath = join(forgeRoot, 'config.yaml');
567
623
  if (!existsSync(configPath))
568
624
  return { kind: 'ok' };
@@ -582,61 +638,60 @@ export async function runArchivePreflight(forgeRoot, changeId) {
582
638
  message: `legacy_bridge.enforce_sync=true 但 ack 未就绪:${ack.reason};请先跑 forge legacy-bridge --acknowledge-data-transfer`,
583
639
  };
584
640
  }
585
- // 拼 change context(同 sync-check 命令)
586
- const changesDir = join(forgeRoot, 'changes', changeId);
587
- let changeContext = '';
588
- const affectedModules = [];
589
- if (existsSync(join(changesDir, 'proposal.md'))) {
590
- changeContext += await readFile(join(changesDir, 'proposal.md'), 'utf8');
591
- }
592
- const specsDir = join(changesDir, 'specs');
593
- if (existsSync(specsDir)) {
594
- const files = await readdir(specsDir);
595
- for (const f of files) {
596
- changeContext += `\n## specs/${f}\n${await readFile(join(specsDir, f), 'utf8')}`;
597
- affectedModules.push(f.replace(/\.md$/, ''));
641
+ // 拼 change context(I-3:gate 已加载的 config + anchors 传入,不再二次 IO)
642
+ // preflight change 还在 forge/changes/<id>/(archived=false,无需 archiveDate)
643
+ const ctx = await buildSyncCheckChangeContext(forgeRoot, changeId, { archived: false }, config, anchors);
644
+ if (opts.api) {
645
+ // —— --api 模式:进程内直连 API 跑 sync-check,不 emit manifest、不写暂停态 ——
646
+ const task = await buildSyncCheckTask(ctx, async (p) => (await readAnchorFile(p)).text);
647
+ if (!task)
648
+ return { kind: 'ok' }; // 无受影响 anchor → 无需 sync-check,续跑 archive
649
+ // 进程内调 Anthropic SDK(client 构造方式与旧 archive 代码一致:forge-eval/load-env)
650
+ const client = (await makeForgeApiClient());
651
+ const results = await new ApiRunner(client).run([task]);
652
+ // --api manifest,produced_from 'api-inline' sentinel(区别于 agent 路径的 manifest_hash;
653
+ // 该 sync-state 不经 resume gate,Task 6.4 据此 sentinel 可识别 --api 来源)
654
+ const syncState = applySyncCheckResult(results[0]?.text ?? '', changeId, 'api-inline');
655
+ // 写 sync-state 到旧路径(沿旧 preflight 写盘方式)
656
+ await mkdir(join(forgeRoot, 'legacy-sync-state'), { recursive: true });
657
+ await writeFile(join(forgeRoot, 'legacy-sync-state', `${changeId}.md`), renderDiffMarkdown(syncState), 'utf8');
658
+ await writeFile(join(forgeRoot, 'legacy-sync-state', `${changeId}.yaml`), renderDiffYaml(syncState), 'utf8');
659
+ // 含 critical pending → critical-pending(死变体的归宿:archive 主流程 kind!=='ok' 会 halt)
660
+ const criticalDiffs = syncState.diffs.filter((d) => d.severity === 'critical' && d.status === 'pending');
661
+ if (criticalDiffs.length > 0) {
662
+ return {
663
+ kind: 'critical-pending',
664
+ criticalCount: criticalDiffs.length,
665
+ message: `✗ ${criticalDiffs.length} 项 critical 差异未 resolve;\n` +
666
+ `跑 forge legacy-bridge resolve ${changeId} 后重试,或在 forge/legacy-sync-state/${changeId}.yaml 标 ack`,
667
+ };
598
668
  }
669
+ return { kind: 'ok' };
599
670
  }
600
- // sync-check(决策 #23:复用 archive.lock,不再 acquire legacy-bridge.lock)
601
- // 运行时动态加载 forge-eval/load-env(避免 src/ rootDir 静态分析边界限制)
602
- const evalLoadEnvPath = new URL('../../../forge-eval/load-env.js', import.meta.url).href;
603
- const { loadEnv } = (await import(/* @vite-ignore */ evalLoadEnvPath));
604
- const { anthropicApiKey } = loadEnv();
605
- const client = new Anthropic({ apiKey: anthropicApiKey });
606
- const out = await runSyncCheck(client, {
607
- changeId,
608
- changeContext,
609
- affectedModules,
610
- anchors,
611
- autoResolveCrossAnchor: config.legacy_bridge.auto_resolve_cross_anchor ?? false,
612
- mtimeOf: (p) => {
613
- try {
614
- return Math.floor(statSync(p).mtimeMs / 1000);
615
- }
616
- catch {
617
- return 0;
618
- }
619
- },
620
- }, async (path) => (await readAnchorFile(path)).text);
621
- await mkdir(join(forgeRoot, 'legacy-sync-state'), { recursive: true });
622
- await writeFile(join(forgeRoot, 'legacy-sync-state', `${changeId}.md`), renderDiffMarkdown(out.syncState), 'utf8');
623
- await writeFile(join(forgeRoot, 'legacy-sync-state', `${changeId}.yaml`), renderDiffYaml(out.syncState), 'utf8');
624
- if (hasCriticalPending(out.syncState)) {
625
- const criticalCount = out.syncState.diffs.filter((d) => d.severity === 'critical' && d.status === 'pending').length;
626
- return {
627
- kind: 'critical-pending',
628
- criticalCount,
629
- message: `✗ ${criticalCount} 项 critical 差异未 resolve;\n` +
630
- `跑 forge legacy-bridge resolve ${changeId} 后重试,或在 forge/legacy-sync-state/${changeId}.yaml 标 ack`,
631
- };
671
+ // —— agent 模式(默认):emit sync-check manifest + 写暂停态文件,halt archive ——
672
+ const emitResult = await emitPreflightSyncCheck(forgeRoot, changeId, ctx);
673
+ if (emitResult.kind === 'skip') {
674
+ // 无受影响 anchor / 全部读取失败 graceful skip,继续 archive
675
+ return { kind: 'ok' };
632
676
  }
633
- return { kind: 'ok' };
677
+ // halted-for-fulfillment:manifest 已写,暂停态已写,告知 caller halt
678
+ return {
679
+ kind: 'halted-for-fulfillment',
680
+ message: `sync-check manifest 已 emit(preflight);\n` +
681
+ `forge agent 履行后跑 forge legacy-bridge sync-check --apply --change-id ${changeId};\n` +
682
+ `暂停态文件:forge/.cache/archive-pause-${changeId}.json`,
683
+ };
634
684
  }
635
685
  /**
636
- * Plan 7 §2.5:post-archive hook — 不阻塞,仅在 enforce_sync=false 时跑(产报告)。
686
+ * Plan 7 §2.5 + Task 6.3:post-archive hook — 不阻塞,仅在 enforce_sync=false 时跑(产报告)。
637
687
  * enforce_sync=true 已由 preflight 跑过;graceful skip 路径同 preflight。
688
+ *
689
+ * agent 模式:emit sync-check manifest(非阻塞,archive 已完成,fulfill 后跑 --apply 出报告)。
690
+ * --api 模式:进程内直连 API 跑 sync-check + 写 sync-state(非阻塞,直接出报告)。
691
+ *
692
+ * I-2:archiveDate 由 archive 主流程透传(不自己算 new Date(),避免跨午夜偏一天)。
638
693
  */
639
- export async function runArchivePostHook(forgeRoot, changeId) {
694
+ export async function runArchivePostHook(forgeRoot, changeId, opts) {
640
695
  const configPath = join(forgeRoot, 'config.yaml');
641
696
  if (!existsSync(configPath))
642
697
  return;
@@ -651,27 +706,76 @@ export async function runArchivePostHook(forgeRoot, changeId) {
651
706
  const ack = await checkAck(forgeRoot, config, anchors);
652
707
  if (!ack.ok)
653
708
  return; // ack 不就绪 → graceful skip
654
- // sync-check 但不阻塞
655
- const changesDir = join(forgeRoot, 'changes', 'archive', `${new Date().toISOString().slice(0, 10)}-${changeId}`);
656
- const proposalPath = existsSync(join(changesDir, 'proposal.md'))
657
- ? join(changesDir, 'proposal.md')
658
- : join(forgeRoot, 'changes', changeId, 'proposal.md');
659
- let changeContext = '';
660
- if (existsSync(proposalPath)) {
661
- changeContext = await readFile(proposalPath, 'utf8');
709
+ // change context(I-3:gate 已加载的 config + anchors 传入;I-2:archiveDate 透传)
710
+ // posthook change 已归档到 archive/<archiveDate>-<id>/
711
+ const ctx = await buildSyncCheckChangeContext(forgeRoot, changeId, { archived: true, archiveDate: opts.archiveDate }, config, anchors);
712
+ if (opts.api) {
713
+ // —— --api 模式:进程内直连 API 跑 sync-check + 写 sync-state(非阻塞)——
714
+ // archive transaction 已完成;posthook 的 API 调用失败不应让 archive 报 exit 1 ——
715
+ // I-A:整段包 try/catch,失败降级为 warn(agent 模式 posthook 只写本地文件无此风险)
716
+ try {
717
+ const task = await buildSyncCheckTask(ctx, async (p) => (await readAnchorFile(p)).text);
718
+ if (!task)
719
+ return; // 无受影响 anchor → graceful skip
720
+ const client = (await makeForgeApiClient());
721
+ const results = await new ApiRunner(client).run([task]);
722
+ const syncState = applySyncCheckResult(results[0]?.text ?? '', changeId, 'api-inline');
723
+ await mkdir(join(forgeRoot, 'legacy-sync-state'), { recursive: true });
724
+ await writeFile(join(forgeRoot, 'legacy-sync-state', `${changeId}.md`), renderDiffMarkdown(syncState), 'utf8');
725
+ await writeFile(join(forgeRoot, 'legacy-sync-state', `${changeId}.yaml`), renderDiffYaml(syncState), 'utf8');
726
+ console.log(`⚠ ${syncState.diffs.length} 项老文档可能需更新,详见 forge/legacy-sync-state/${changeId}.md`);
727
+ }
728
+ catch (e) {
729
+ console.warn(`⚠ --api sync-check(posthook)失败,已跳过(archive 已完成):${e.message}`);
730
+ }
731
+ return;
662
732
  }
733
+ // —— agent 模式(默认):emit sync-check manifest(非阻塞,archive 已完成)——
734
+ const emitResult = await emitPostHookSyncCheck(forgeRoot, changeId, ctx);
735
+ if (emitResult.kind === 'emitted') {
736
+ console.log(`⚠ sync-check manifest 已 emit(posthook);forge agent 履行后跑 forge legacy-bridge sync-check --apply --change-id ${changeId} 可得报告`);
737
+ }
738
+ // kind='skip':无受影响 anchor / 全部读取失败 → graceful skip,无输出
739
+ }
740
+ /**
741
+ * 抽出:合并 runArchivePreflight/runArchivePostHook 两处重复的 change context 拼装。
742
+ *
743
+ * archived=false → forge/changes/<id>/;
744
+ * archived=true → forge/changes/archive/<archiveDate>-<id>/。
745
+ *
746
+ * I-3:config 与 anchors 由调用方(preflight/posthook 的 gate 处)已加载并传入,
747
+ * 本函数不再二次 readFile/loadAnchorsFile —— 消除 double-IO + 避免
748
+ * loadAnchorsFile 损坏时裸抛 LegacyAnchorsError 穿透成 unhandled rejection。
749
+ * I-2:archived=true 时用调用方透传的 archiveDate(archive 主流程算出的那个),
750
+ * 不再 new Date() —— 避免 archive 跨午夜完成时目录名偏一天致静默 skip。
751
+ */
752
+ async function buildSyncCheckChangeContext(forgeRoot, changeId, opts, config, anchors) {
753
+ // 决定 change 目录:归档后在 archive/<archiveDate>-<id>/,归档前在 changes/<id>/
754
+ // archived=true 时 archiveDate 必传(由 posthook 透传 archive 主流程算出的值)
755
+ const changesDir = opts.archived && opts.archiveDate
756
+ ? join(forgeRoot, 'changes', 'archive', `${opts.archiveDate}-${changeId}`)
757
+ : join(forgeRoot, 'changes', changeId);
758
+ let changeContext = '';
663
759
  const affectedModules = [];
664
- // 简化:从 changeId 推测 module(占位,真实环境靠 specs/<area>.md)
665
- const evalLoadEnvPath = new URL('../../../forge-eval/load-env.js', import.meta.url).href;
666
- const { loadEnv } = (await import(/* @vite-ignore */ evalLoadEnvPath));
667
- const { anthropicApiKey } = loadEnv();
668
- const client = new Anthropic({ apiKey: anthropicApiKey });
669
- const out = await runSyncCheck(client, {
760
+ // proposal.md
761
+ if (existsSync(join(changesDir, 'proposal.md'))) {
762
+ changeContext += await readFile(join(changesDir, 'proposal.md'), 'utf8');
763
+ }
764
+ // specs/ 下各 .md,拼 changeContext + 收集 affectedModules
765
+ const specsDir = join(changesDir, 'specs');
766
+ if (existsSync(specsDir)) {
767
+ for (const f of await readdir(specsDir)) {
768
+ changeContext += `\n## specs/${f}\n${await readFile(join(specsDir, f), 'utf8')}`;
769
+ affectedModules.push(f.replace(/\.md$/, ''));
770
+ }
771
+ }
772
+ return {
670
773
  changeId,
671
774
  changeContext,
672
775
  affectedModules,
673
776
  anchors,
674
- autoResolveCrossAnchor: config.legacy_bridge.auto_resolve_cross_anchor ?? false,
777
+ autoResolveCrossAnchor: config.legacy_bridge?.auto_resolve_cross_anchor ?? false,
778
+ // mtime 提供者:用于 cross-anchor 决策
675
779
  mtimeOf: (p) => {
676
780
  try {
677
781
  return Math.floor(statSync(p).mtimeMs / 1000);
@@ -680,10 +784,154 @@ export async function runArchivePostHook(forgeRoot, changeId) {
680
784
  return 0;
681
785
  }
682
786
  },
683
- }, async (path) => (await readAnchorFile(path)).text);
684
- await mkdir(join(forgeRoot, 'legacy-sync-state'), { recursive: true });
685
- await writeFile(join(forgeRoot, 'legacy-sync-state', `${changeId}.md`), renderDiffMarkdown(out.syncState), 'utf8');
686
- await writeFile(join(forgeRoot, 'legacy-sync-state', `${changeId}.yaml`), renderDiffYaml(out.syncState), 'utf8');
687
- console.log(`⚠ ${out.syncState.diffs.length} 项老文档可能需更新,详见 forge/legacy-sync-state/${changeId}.md`);
787
+ };
788
+ }
789
+ /**
790
+ * agent 模式:emit preflight sync-check manifest + 暂停态文件,halt archive(Task 6.3)
791
+ *
792
+ * 调用前 gate 检查(config 存在 + allow_llm_calls + enforce_sync + anchors + ack)由
793
+ * runArchivePreflight 负责;本函数专注 emit 逻辑。
794
+ *
795
+ * @param ctx 已拼好的 SyncCheckInput(可选;不传则内部自拼,供测试直接调用)
796
+ */
797
+ export async function emitPreflightSyncCheck(forgeRoot, changeId, ctx) {
798
+ // ctx 未传(测试直接调用)→ 自行加载 config + anchors 拼 context
799
+ const syncCtx = ctx ?? (await buildSyncCheckContextStandalone(forgeRoot, changeId, false));
800
+ if (!syncCtx)
801
+ return { kind: 'skip' }; // standalone 加载失败(无 config/anchors)
802
+ // 构建 LlmTask(确定性 prep:findAffectedAnchors + redact)
803
+ const task = await buildSyncCheckTask(syncCtx, async (p) => (await readAnchorFile(p)).text);
804
+ // 无受影响 anchor 或全部读取失败 → graceful skip
805
+ if (!task)
806
+ return { kind: 'skip' };
807
+ // emit sync-check manifest 到 forge/.cache/legacy-bridge-task-sync-check.json
808
+ // meta 键名与 Task 6.2 runSyncCheckCommand emit 保持一致:changeId(驼峰)
809
+ const manifest = await new AgentHandoffRunner(forgeRoot, FORGE_VERSION).emit('sync-check', 1, [task], { gate_context: 'archive-preflight', changeId });
810
+ // 写暂停态文件:archive-pause-<changeId>.json
811
+ await mkdir(join(forgeRoot, '.cache'), { recursive: true });
812
+ await writeFile(join(forgeRoot, '.cache', `archive-pause-${changeId}.json`), JSON.stringify({
813
+ changeId,
814
+ paused_step: 'preflight-sync-check',
815
+ manifest_hash: manifest.manifest_hash,
816
+ }, null, 2), 'utf8');
817
+ return { kind: 'halted-for-fulfillment' };
818
+ }
819
+ /**
820
+ * agent 模式:enforce_sync=false 时 post-archive emit sync-check manifest(非阻塞,无暂停态)(Task 6.3)。
821
+ *
822
+ * archive 已完成;fulfill 后跑 forge legacy-bridge sync-check --apply 出报告。
823
+ * 调用前 gate 检查由 runArchivePostHook 负责。
824
+ *
825
+ * @param ctx 已拼好的 SyncCheckInput(可选;不传则内部自拼,供测试直接调用)
826
+ */
827
+ export async function emitPostHookSyncCheck(forgeRoot, changeId, ctx) {
828
+ // ctx 未传(测试直接调用)→ 自行加载 config + anchors 拼 context(archived=true)
829
+ const syncCtx = ctx ?? (await buildSyncCheckContextStandalone(forgeRoot, changeId, true));
830
+ if (!syncCtx)
831
+ return { kind: 'skip' }; // standalone 加载失败
832
+ // 构建 LlmTask
833
+ const task = await buildSyncCheckTask(syncCtx, async (p) => (await readAnchorFile(p)).text);
834
+ // 无受影响 anchor → graceful skip
835
+ if (!task)
836
+ return { kind: 'skip' };
837
+ // emit sync-check manifest(meta 键名与 Task 6.2 一致:changeId 驼峰)
838
+ await new AgentHandoffRunner(forgeRoot, FORGE_VERSION).emit('sync-check', 1, [task], {
839
+ gate_context: 'archive-posthook',
840
+ changeId,
841
+ });
842
+ // 非阻塞:不写暂停态文件(archive 已完成)
843
+ return { kind: 'emitted' };
844
+ }
845
+ /**
846
+ * Task 6.3 I-3 配套:emitPreflightSyncCheck/emitPostHookSyncCheck 被测试直接调用
847
+ * (不经 runArchivePreflight/runArchivePostHook gate)时,自行加载 config + anchors。
848
+ *
849
+ * archived=true 时用「今日」作 archiveDate —— 仅 standalone 测试路径,生产路径
850
+ * 由 runArchivePostHook 透传准确的 archiveDate(I-2 已修主路径跨午夜 bug)。
851
+ *
852
+ * 返回 null:config 缺失 / anchors 缺失 → 调用方按 skip 处理。
853
+ */
854
+ async function buildSyncCheckContextStandalone(forgeRoot, changeId, archived) {
855
+ const configPath = join(forgeRoot, 'config.yaml');
856
+ if (!existsSync(configPath))
857
+ return null;
858
+ const config = parseYaml(await readFile(configPath, 'utf8'));
859
+ const anchors = await loadAnchorsFile(forgeRoot).catch(() => null);
860
+ if (!anchors)
861
+ return null;
862
+ return buildSyncCheckChangeContext(forgeRoot, changeId, { archived, archiveDate: archived ? new Date().toISOString().slice(0, 10) : undefined }, config, anchors);
863
+ }
864
+ /**
865
+ * Task 6.4:forge archive --resume 的 gate 复核。
866
+ *
867
+ * 复核两项:
868
+ * ① 产物绑定 — sync-state 的 produced_from 必须等于暂停态文件的 manifest_hash,
869
+ * 确保 sync-check --apply 产出的 sync-state 确实来自本次 preflight emit 的 manifest。
870
+ * ② critical 幂等重评 — sync-state 中不得有 severity=critical + status=pending 的差异;
871
+ * 若有,说明 agent 未完全 resolve,不可续跑 archive。
872
+ *
873
+ * @param forgeRoot forge/ 根目录路径
874
+ * @param changeId change id(e.g. 'add-pay')
875
+ * @returns { ok: true } 表示 gate 通过;{ ok: false, reason } 表示拒绝并附原因
876
+ */
877
+ export async function resumeArchiveGateCheck(forgeRoot, changeId) {
878
+ // 读暂停态文件:forge/.cache/archive-pause-<changeId>.json
879
+ const pausePath = join(forgeRoot, '.cache', `archive-pause-${changeId}.json`);
880
+ if (!existsSync(pausePath)) {
881
+ return {
882
+ ok: false,
883
+ reason: `无暂停态文件 archive-pause-${changeId}.json;archive 未在 preflight 暂停,无需 --resume`,
884
+ };
885
+ }
886
+ // 暂停态文件格式(Task 6.3 emitPreflightSyncCheck 写入):{ changeId, paused_step, manifest_hash }
887
+ // I-1:parse 包 try/catch —— 文件截断/手动误编辑致 JSON 损坏时转 business-fail,
888
+ // 用户看得出是哪个文件坏(否则裸 SyntaxError 被 action 外层兜底当未知错误)。
889
+ // readFile 的 ENOENT 竞态(existsSync 后文件被删)也顺带被这层 catch 兜住,可接受。
890
+ let pause;
891
+ try {
892
+ pause = JSON.parse(await readFile(pausePath, 'utf8'));
893
+ }
894
+ catch (err) {
895
+ return {
896
+ ok: false,
897
+ reason: `暂停态文件 ${pausePath} 解析失败(可能损坏):${err.message}`,
898
+ };
899
+ }
900
+ // 读 sync-state YAML:forge/legacy-sync-state/<changeId>.yaml
901
+ const statePath = join(forgeRoot, 'legacy-sync-state', `${changeId}.yaml`);
902
+ if (!existsSync(statePath)) {
903
+ return {
904
+ ok: false,
905
+ reason: `sync-state 缺失(forge/legacy-sync-state/${changeId}.yaml);请先让 agent fulfill manifest 后跑 forge legacy-bridge sync-check --apply --change-id ${changeId}`,
906
+ };
907
+ }
908
+ // I-1:同样包 try/catch —— sync-state YAML 损坏时转 business-fail
909
+ let state;
910
+ try {
911
+ state = parseYaml(await readFile(statePath, 'utf8'));
912
+ }
913
+ catch (err) {
914
+ return {
915
+ ok: false,
916
+ reason: `sync-state 文件 ${statePath} 解析失败(可能损坏):${err.message}`,
917
+ };
918
+ }
919
+ // ① 产物绑定:sync-state.produced_from 必须等于暂停态的 manifest_hash
920
+ if (state.produced_from !== pause.manifest_hash) {
921
+ return {
922
+ ok: false,
923
+ reason: `produced_from(${state.produced_from ?? 'undefined'}) ≠ 暂停态 manifest_hash(${pause.manifest_hash});sync-state 非本次 --apply 产物,请重新履行 manifest 并跑 sync-check --apply`,
924
+ };
925
+ }
926
+ // ② critical 幂等重评:sync-state 中不得有 critical+pending 差异
927
+ // 注:archive.ts Task 6.3 已用内联 filter 而非 hasCriticalPending import,此处保持一致
928
+ const criticalPending = (state.diffs ?? []).filter((d) => d.severity === 'critical' && d.status === 'pending');
929
+ if (criticalPending.length > 0) {
930
+ return {
931
+ ok: false,
932
+ reason: `仍有 ${criticalPending.length} 项 critical 差异未 resolve;跑 forge legacy-bridge resolve ${changeId} 后重试`,
933
+ };
934
+ }
935
+ return { ok: true };
688
936
  }
689
937
  //# sourceMappingURL=archive.js.map