@accelerator-mzq/forge 3.0.0 → 4.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.
- package/README.md +12 -12
- package/dist/cli/commands/ack.d.ts.map +1 -1
- package/dist/cli/commands/ack.js +87 -12
- package/dist/cli/commands/ack.js.map +1 -1
- package/dist/cli/commands/archive.d.ts +32 -33
- package/dist/cli/commands/archive.d.ts.map +1 -1
- package/dist/cli/commands/archive.js +339 -667
- package/dist/cli/commands/archive.js.map +1 -1
- package/dist/cli/commands/legacy-bridge.js +1 -1
- package/dist/cli/commands/legacy-bridge.js.map +1 -1
- package/dist/cli/commands/upgrade.d.ts.map +1 -1
- package/dist/cli/commands/upgrade.js +2 -51
- package/dist/cli/commands/upgrade.js.map +1 -1
- package/dist/cli/commands/validate.d.ts.map +1 -1
- package/dist/cli/commands/validate.js +4 -25
- package/dist/cli/commands/validate.js.map +1 -1
- package/dist/cli/index.js +0 -9
- package/dist/cli/index.js.map +1 -1
- package/dist/core/ack/marker-ack.d.ts +28 -0
- package/dist/core/ack/marker-ack.d.ts.map +1 -0
- package/dist/core/ack/marker-ack.js +155 -0
- package/dist/core/ack/marker-ack.js.map +1 -0
- package/dist/core/ack-log.d.ts.map +1 -1
- package/dist/core/ack-log.js +10 -7
- package/dist/core/ack-log.js.map +1 -1
- package/dist/core/archive/index.d.ts +2 -3
- package/dist/core/archive/index.d.ts.map +1 -1
- package/dist/core/archive/index.js +7 -5
- package/dist/core/archive/index.js.map +1 -1
- package/dist/core/archive/pause-decisions-fence.js +12 -12
- package/dist/core/archive/pause-decisions-fence.js.map +1 -1
- package/dist/core/archive/summary-builder.d.ts +26 -21
- package/dist/core/archive/summary-builder.d.ts.map +1 -1
- package/dist/core/archive/summary-builder.js +115 -223
- package/dist/core/archive/summary-builder.js.map +1 -1
- package/dist/core/archive/summary-render.d.ts +5 -3
- package/dist/core/archive/summary-render.d.ts.map +1 -1
- package/dist/core/archive/summary-render.js +38 -44
- package/dist/core/archive/summary-render.js.map +1 -1
- package/dist/core/lock.d.ts +46 -0
- package/dist/core/lock.d.ts.map +1 -0
- package/dist/core/lock.js +98 -0
- package/dist/core/lock.js.map +1 -0
- package/dist/core/markers/types.d.ts +27 -134
- package/dist/core/markers/types.d.ts.map +1 -1
- package/dist/core/markers/types.js +10 -1
- package/dist/core/markers/types.js.map +1 -1
- package/dist/core/migrate/index.js +1 -1
- package/dist/core/migrate/index.js.map +1 -1
- package/dist/core/monitor/artifact-observer.d.ts.map +1 -1
- package/dist/core/monitor/artifact-observer.js +28 -78
- package/dist/core/monitor/artifact-observer.js.map +1 -1
- package/dist/core/monitor/divergence-map.d.ts.map +1 -1
- package/dist/core/monitor/divergence-map.js +9 -7
- package/dist/core/monitor/divergence-map.js.map +1 -1
- package/dist/core/monitor/health-verdict.d.ts.map +1 -1
- package/dist/core/monitor/health-verdict.js +2 -1
- package/dist/core/monitor/health-verdict.js.map +1 -1
- package/dist/core/monitor/trace-store.d.ts +1 -1
- package/dist/core/monitor/trace-store.js +2 -2
- package/dist/core/monitor/trace-store.js.map +1 -1
- package/dist/core/monitor/types.d.ts +1 -1
- package/dist/core/monitor/types.d.ts.map +1 -1
- package/dist/core/monitor/types.js +0 -1
- package/dist/core/monitor/types.js.map +1 -1
- package/dist/core/schemas/archive-summary.d.ts +39 -109
- package/dist/core/schemas/archive-summary.d.ts.map +1 -1
- package/dist/core/schemas/archive-summary.js +15 -23
- package/dist/core/schemas/archive-summary.js.map +1 -1
- package/dist/core/templates/commands/ack-confirm.md +1 -1
- package/dist/core/templates/commands/apply.md +51 -114
- package/dist/core/templates/commands/archive.md +43 -160
- package/dist/core/templates/commands/review.md +49 -74
- package/dist/core/templates/commands/upgrade.md +21 -40
- package/dist/core/templates/commands/verify.md +43 -146
- package/dist/core/templates/skills/_shared/tier23-command-bridge.md +34 -0
- package/dist/core/templates/skills/exploring.md +3 -3
- package/dist/core/templates/skills/finishing-a-development-branch.md +6 -21
- package/dist/core/templates/skills/receiving-code-review.md +14 -45
- package/dist/core/templates/skills/requesting-code-review.md +11 -0
- package/dist/core/templates/skills/subagent-driven-development.md +33 -29
- package/dist/core/templates/skills/subagent-driven-discipline.md +28 -28
- package/dist/core/templates/skills/using-forge.md +25 -24
- package/dist/core/templates/skills/verification-before-completion.md +7 -18
- package/dist/core/templates/skills/verifying-three-dimensions.md +93 -169
- package/dist/core/templates/skills/writing-plans.md +6 -17
- package/dist/core/validate/archive-summary-schema.d.ts +1 -1
- package/dist/core/validate/archive-summary-schema.d.ts.map +1 -1
- package/dist/core/validate/archive-summary-schema.js +101 -125
- package/dist/core/validate/archive-summary-schema.js.map +1 -1
- package/dist/core/validate/index.d.ts +0 -1
- package/dist/core/validate/index.d.ts.map +1 -1
- package/dist/core/validate/index.js +2 -1
- package/dist/core/validate/index.js.map +1 -1
- package/dist/core/validate/marker-schema.d.ts +12 -4
- package/dist/core/validate/marker-schema.d.ts.map +1 -1
- package/dist/core/validate/marker-schema.js +98 -605
- package/dist/core/validate/marker-schema.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -1
- package/dist/index.js.map +1 -1
- package/package.json +5 -4
- package/scripts/codex-review-helper.mjs +337 -0
- package/src/core/codex-review/prompts/adversarial-default.md +105 -0
|
@@ -1,625 +1,352 @@
|
|
|
1
|
-
// forge archive 子命令 —
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
1
|
+
// forge archive 子命令 — v4 OpenSpec alignment 简版(plan-v4 Phase 1 Task 1.5)
|
|
2
|
+
//
|
|
3
|
+
// v4 BREAKING(沿 plan-v4 §6.1 v9 + §10.1.2):
|
|
4
|
+
// - 删反加固协议:transaction / lock / recover / resume-summary / 13 fence / process_evidence /
|
|
5
|
+
// ack-log / verify-findings / pause-decisions-fence / three-level-fence / legacy-exemption /
|
|
6
|
+
// version-retrograde / hash-mismatch fence / git integrity fence
|
|
7
|
+
// - 删 CLI flag:--recover / --resume-summary / --resume
|
|
8
|
+
// - 新增 CLI flag:--yes(沿 OpenSpec)/ --skip-specs(沿 OpenSpec)
|
|
9
|
+
// - 保留:--force(human-override / 非 git)/ --api(legacy-bridge sync-check 直连)
|
|
10
|
+
// - marker 校验 schema v1 → v2(VerifyMarker / ReviewMarker v2,无 hash/git/process_evidence)
|
|
11
|
+
// - spec deltas:沿用 forge 现有 readDeltas + applyDeltas(Path B — 不移植 OpenSpec specs-apply)
|
|
12
|
+
// - archive_summary v1 → v2(无 acked_warnings / pending_suggestions / process_evidence_summary)
|
|
13
|
+
// - 保留 legacy-bridge preflight/posthook 接口(本文件内 export,sync-check 双路径不变)
|
|
7
14
|
import { Command } from 'commander';
|
|
8
|
-
import { readFile,
|
|
15
|
+
import { readFile, writeFile, readdir, mkdir, rename, cp, rm, stat } from 'node:fs/promises';
|
|
9
16
|
import { existsSync, statSync } from 'node:fs';
|
|
10
|
-
import {
|
|
17
|
+
import { createInterface } from 'node:readline/promises';
|
|
18
|
+
import { stdin, stdout } from 'node:process';
|
|
11
19
|
import { join } from 'node:path';
|
|
12
|
-
import { parse as parseYaml } from 'yaml';
|
|
20
|
+
import { parse as parseYaml, stringify as stringifyYaml } from 'yaml';
|
|
13
21
|
import { parseMarker } from '../../core/markers/index.js';
|
|
14
22
|
import { validateMarkerSchema } from '../../core/validate/index.js';
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
17
|
-
import {
|
|
18
|
-
import {
|
|
19
|
-
import {
|
|
20
|
-
import {
|
|
21
|
-
import {
|
|
23
|
+
import { validateChange } from '../../core/validate/change.js';
|
|
24
|
+
import { readDeltas, applyDeltas } from '../../core/specs-sync/index.js';
|
|
25
|
+
import { buildArchiveSummary } from '../../core/archive/summary-builder.js';
|
|
26
|
+
import { renderArchiveSummaryOutput } from '../../core/archive/summary-render.js';
|
|
27
|
+
import { generateBacklog } from '../../core/backlog/index.js';
|
|
28
|
+
import { appendTraceEvent } from '../../core/monitor/trace-store.js';
|
|
29
|
+
import { FORGE_VERSION } from '../../index.js';
|
|
30
|
+
// —— legacy-bridge sync-check 双路径依赖(仅 runArchivePreflight/PostHook + emit 用)——
|
|
22
31
|
import { loadAnchorsFile } from '../../core/legacy-bridge/anchors.js';
|
|
23
32
|
import { checkAck } from '../../core/legacy-bridge/ack.js';
|
|
24
33
|
import { buildSyncCheckTask, applySyncCheckResult } from '../../core/legacy-bridge/sync-check.js';
|
|
25
34
|
import { AgentHandoffRunner, ApiRunner, makeForgeApiClient, } from '../../core/legacy-bridge/runners.js';
|
|
26
35
|
import { renderDiffMarkdown, renderDiffYaml } from '../../core/legacy-bridge/diff-report.js';
|
|
27
36
|
import { readAnchorFile } from '../../core/legacy-bridge/encoding.js';
|
|
28
|
-
|
|
29
|
-
//
|
|
30
|
-
|
|
31
|
-
// plan-9d Task 6:verify_findings fence + ack-log consistency
|
|
32
|
-
import { validateVerifyFindingsFence } from '../../core/archive/verify-findings-fence.js';
|
|
33
|
-
import { validateAckLogConsistency } from '../../core/archive/ack-log-consistency.js';
|
|
34
|
-
// plan-9c Task 2:pause_decisions fence + Task 12:cross-check
|
|
35
|
-
import { validatePauseDecisionsFence, crossCheckPauseDecisions, } from '../../core/archive/pause-decisions-fence.js';
|
|
36
|
-
// plan-9e1 Task 4:三级 fence + summary builder + render + ScopeEntriesIntegrityError(v2 BLOCKER 4)
|
|
37
|
-
import { validateThreeLevelFence } from '../../core/archive/three-level-fence.js';
|
|
38
|
-
import { buildArchiveSummary, ScopeEntriesIntegrityError, } from '../../core/archive/summary-builder.js';
|
|
39
|
-
import { renderArchiveSummaryOutput } from '../../core/archive/summary-render.js';
|
|
40
|
-
// plan-backlog-registry Task 7:archive 成功后自动重生成 forge/backlog/
|
|
41
|
-
import { generateBacklog } from '../../core/backlog/index.js';
|
|
42
|
-
// plan-9e1 Task 5:resume-summary 子模式
|
|
43
|
-
import { resumeArchiveSummary } from '../../core/archive/resume-summary.js';
|
|
44
|
-
// plan-9j Task 5:legacy-exemption + version-retrograde fence
|
|
45
|
-
import { validateLegacyExemption } from '../../core/archive/legacy-exemption.js';
|
|
46
|
-
import { validateVersionRetrograde } from '../../core/archive/version-retrograde-fence.js';
|
|
37
|
+
// ============================================================================
|
|
38
|
+
// 本地 helper(沿 plan §10.1.2 Task 1.5 v6 修订)
|
|
39
|
+
// ============================================================================
|
|
47
40
|
/**
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
41
|
+
* getArchiveDate:返回 YYYY-MM-DD(沿 plan §6.1 v4 F-R3-2 — archive 主流程算一次)
|
|
42
|
+
*
|
|
43
|
+
* 关键:archive.ts 内只能调一次,后续 archiveName + posthook 共用同一变量,
|
|
44
|
+
* 防跨午夜 archive 时 archiveName 用 day N、posthook 拿 day N+1 不一致。
|
|
45
|
+
*/
|
|
46
|
+
function getArchiveDate() {
|
|
47
|
+
return new Date().toISOString().slice(0, 10);
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* moveDirectory:rename 跨盘失败时降级到 cp + rm(沿 OpenSpec EPERM/EXDEV fallback)
|
|
51
|
+
*
|
|
52
|
+
* Windows 上 rename 同盘正常,跨盘抛 EXDEV;某些权限场景抛 EPERM。
|
|
53
|
+
* 失败时用 fs.cp 递归复制 + rm 删源,语义等同 mv。
|
|
52
54
|
*/
|
|
53
|
-
function
|
|
55
|
+
async function moveDirectory(src, dst) {
|
|
54
56
|
try {
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
57
|
+
await rename(src, dst);
|
|
58
|
+
}
|
|
59
|
+
catch (err) {
|
|
60
|
+
const code = err.code;
|
|
61
|
+
if (code === 'EXDEV' || code === 'EPERM' || code === 'EACCES') {
|
|
62
|
+
// 跨盘 / 权限错 → cp + rm 模拟 mv
|
|
63
|
+
await cp(src, dst, { recursive: true });
|
|
64
|
+
await rm(src, { recursive: true, force: true });
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
throw err;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* renderDeltasSummary:把 SpecDelta[] 渲染成用户可读字符串(供 confirm 前显示)
|
|
72
|
+
*
|
|
73
|
+
* 沿 plan §6.1 v6 修订(Codex v5 F-R5-2):archive.ts 内本地 helper(~10 行),
|
|
74
|
+
* 不在 specs-sync/ 新增 — Path B "无新增代码" 仅指 specs-sync/ 不变。
|
|
75
|
+
*/
|
|
76
|
+
function renderDeltasSummary(deltas) {
|
|
77
|
+
const lines = ['Spec deltas to apply:'];
|
|
78
|
+
for (const d of deltas) {
|
|
79
|
+
lines.push(` - ${d.name}: ${d.operation}`);
|
|
80
|
+
}
|
|
81
|
+
return lines.join('\n');
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* selectChange:在 forge/changes/ 下交互式让 user 选 changeId(若没传 argv)
|
|
85
|
+
*
|
|
86
|
+
* 沿 OpenSpec archive.ts 风格:列出 active change(忽略 archive/ 子目录),按 mtime 倒序,
|
|
87
|
+
* 提示用户输入序号。空目录返 undefined。
|
|
88
|
+
*/
|
|
89
|
+
async function selectChange(changesDir) {
|
|
90
|
+
const entries = await readdir(changesDir).catch(() => []);
|
|
91
|
+
const candidates = [];
|
|
92
|
+
for (const name of entries) {
|
|
93
|
+
if (name === 'archive')
|
|
94
|
+
continue;
|
|
95
|
+
const stats = await stat(join(changesDir, name)).catch(() => null);
|
|
96
|
+
if (!stats || !stats.isDirectory())
|
|
97
|
+
continue;
|
|
98
|
+
candidates.push({ name, mtime: stats.mtimeMs });
|
|
99
|
+
}
|
|
100
|
+
if (candidates.length === 0)
|
|
101
|
+
return undefined;
|
|
102
|
+
// 按 mtime 倒序(最近修改的优先)
|
|
103
|
+
candidates.sort((a, b) => b.mtime - a.mtime);
|
|
104
|
+
console.log('Active changes:');
|
|
105
|
+
for (let i = 0; i < candidates.length; i++) {
|
|
106
|
+
console.log(` ${i + 1}. ${candidates[i].name}`);
|
|
107
|
+
}
|
|
108
|
+
const rl = createInterface({ input: stdin, output: stdout });
|
|
109
|
+
try {
|
|
110
|
+
const ans = (await rl.question(`Select change number (1-${candidates.length}): `)).trim();
|
|
111
|
+
const idx = Number(ans) - 1;
|
|
112
|
+
if (!Number.isInteger(idx) || idx < 0 || idx >= candidates.length) {
|
|
113
|
+
console.error(`✗ Invalid selection: ${ans}`);
|
|
114
|
+
return undefined;
|
|
115
|
+
}
|
|
116
|
+
return candidates[idx].name;
|
|
61
117
|
}
|
|
62
|
-
|
|
63
|
-
|
|
118
|
+
finally {
|
|
119
|
+
rl.close();
|
|
64
120
|
}
|
|
65
121
|
}
|
|
122
|
+
/**
|
|
123
|
+
* confirmPrompt:Y/n 交互(--yes 时跳过 — 由 caller 判断)
|
|
124
|
+
*
|
|
125
|
+
* defaultYes=true:回车 = yes(spec deltas 等 safe-by-default 场景)
|
|
126
|
+
* defaultYes=false:回车 = no(incomplete tasks 等 dangerous-by-default 场景,沿 plan §6.1
|
|
127
|
+
* line 326 `default: false`;Codex Phase 1 review F-2 修订)
|
|
128
|
+
* 沿 forge 现有 readline/promises 惯例(upgrade.ts:144 / migrate/index.ts 同模式)。
|
|
129
|
+
*/
|
|
130
|
+
async function confirmPrompt(message, defaultYes) {
|
|
131
|
+
const rl = createInterface({ input: stdin, output: stdout });
|
|
132
|
+
try {
|
|
133
|
+
const hint = defaultYes ? '[Y/n]' : '[y/N]';
|
|
134
|
+
const ans = (await rl.question(`${message} ${hint} `)).trim().toLowerCase();
|
|
135
|
+
if (ans === '')
|
|
136
|
+
return defaultYes;
|
|
137
|
+
return ans === 'y' || ans === 'yes';
|
|
138
|
+
}
|
|
139
|
+
finally {
|
|
140
|
+
rl.close();
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* countIncompleteTasks:数 tasks.md 内未打勾的 task(- [ ] 行计数)
|
|
145
|
+
*
|
|
146
|
+
* 沿 OpenSpec archive.ts — 未完成 task 时 prompt confirm(--yes 跳过)
|
|
147
|
+
*/
|
|
148
|
+
async function countIncompleteTasks(changeDir) {
|
|
149
|
+
const tasksPath = join(changeDir, 'tasks.md');
|
|
150
|
+
if (!existsSync(tasksPath))
|
|
151
|
+
return 0;
|
|
152
|
+
const text = await readFile(tasksPath, 'utf8');
|
|
153
|
+
// 匹配 "- [ ] " 行(空格代表未打勾)
|
|
154
|
+
const matches = text.match(/^\s*- \[ \]\s+/gm);
|
|
155
|
+
return matches ? matches.length : 0;
|
|
156
|
+
}
|
|
157
|
+
// ============================================================================
|
|
158
|
+
// buildArchiveCommand — commander 工厂(沿 plan §6.1 v7 F-R6-1)
|
|
159
|
+
// ============================================================================
|
|
160
|
+
/**
|
|
161
|
+
* Forge archive 命令工厂 — v4 简版(沿 plan §6.1 v9 伪代码)
|
|
162
|
+
*
|
|
163
|
+
* 9 步主流程:
|
|
164
|
+
* 1. forgeRoot + changeId selection
|
|
165
|
+
* 2. legacy-bridge preflight(可 graceful skip)
|
|
166
|
+
* 3. spec validate(blocking,永远跑)
|
|
167
|
+
* 4. marker check + v2 schema validate
|
|
168
|
+
* 5. task progress confirm(--yes 跳过)
|
|
169
|
+
* 6. spec deltas(--skip-specs 跳;否则 readDeltas + confirm + applyDeltas)
|
|
170
|
+
* 7. mv change → archive/<archiveDate>-<changeId>(算一次 archiveDate)
|
|
171
|
+
* 8. buildArchiveSummary + 写 archive_summary.yaml
|
|
172
|
+
* 9. legacy-bridge posthook + generateBacklog + appendTraceEvent
|
|
173
|
+
*/
|
|
66
174
|
export function buildArchiveCommand() {
|
|
67
|
-
return
|
|
175
|
+
return new Command('archive')
|
|
68
176
|
.argument('[changeId]', 'change directory id (e.g., add-login)')
|
|
69
177
|
.description('Archive a verified+reviewed change')
|
|
70
|
-
.option('--force', 'accept human-override
|
|
71
|
-
.option('--
|
|
72
|
-
.option('--
|
|
73
|
-
// Task 6.3:--api 模式 — preflight/posthook 的 sync-check 进程内直连 API 跑完,
|
|
74
|
-
// 不 emit manifest、不写暂停态、不 halt(高保证 CI 场景绕过 agent-pause)
|
|
178
|
+
.option('--force', 'accept human-override markers')
|
|
179
|
+
.option('--yes', 'auto-accept all interactive confirms (CI mode)')
|
|
180
|
+
.option('--skip-specs', 'skip applying spec deltas to forge/specs/')
|
|
75
181
|
.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 幂等重评)')
|
|
78
182
|
.action(async (changeId, opts) => {
|
|
79
|
-
const
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
if (r.case === 'C' && r.caseCData) {
|
|
89
|
-
const { backupIntegrity, archiveIntegrity } = r.caseCData;
|
|
90
|
-
// 任一不完整 → 退出码 4(spec §3.5 case C 末尾"任一不完整 → 输出诊断 + 退出码 4")
|
|
91
|
-
if (!backupIntegrity.ok || !archiveIntegrity.ok) {
|
|
92
|
-
console.error('✗ case C 完整性 check 失败,需人工介入:');
|
|
93
|
-
if (!backupIntegrity.ok)
|
|
94
|
-
console.error(` backup: ${backupIntegrity.reason}`);
|
|
95
|
-
if (!archiveIntegrity.ok)
|
|
96
|
-
console.error(` archive: ${archiveIntegrity.reason}`);
|
|
97
|
-
const rel = release;
|
|
98
|
-
release = undefined;
|
|
99
|
-
if (rel)
|
|
100
|
-
await rel();
|
|
101
|
-
process.exit(4);
|
|
102
|
-
}
|
|
103
|
-
// 两者都完整 → 交互式二选一
|
|
104
|
-
const choice = await promptRecoverChoice();
|
|
105
|
-
if (choice === 'complete-archive') {
|
|
106
|
-
// 完成归档:重跑 Sync(applyDeltas)+ 删 backup
|
|
107
|
-
await applyDeltas(r.caseCData.currentSpecsDir, r.caseCData.deltas);
|
|
108
|
-
await rm(r.caseCData.backupDir, { recursive: true, force: true });
|
|
109
|
-
console.log('✓ case C 选择 [1] 完成归档:Sync 重跑成功,backup 已清理');
|
|
110
|
-
}
|
|
111
|
-
else {
|
|
112
|
-
// 撤销归档:从 backup 恢复 forge/specs/ + 反向 rename
|
|
113
|
-
await restoreSpecsFromBackup(r.caseCData.backupDir, r.caseCData.currentSpecsDir);
|
|
114
|
-
// rename 失败时 specs 已还原但 archive 仍在原位,整体可重入(再跑 --recover 仍可完成)
|
|
115
|
-
await rename(r.caseCData.archiveChangeDir, join(forgeRoot, 'changes', r.caseCData.changeOrigId));
|
|
116
|
-
await rm(r.caseCData.backupDir, { recursive: true, force: true });
|
|
117
|
-
console.log('✓ case C 选择 [2] 撤销归档:specs 已还原,archive 反向 rename 完成');
|
|
118
|
-
}
|
|
119
|
-
const rel = release;
|
|
120
|
-
release = undefined;
|
|
121
|
-
if (rel)
|
|
122
|
-
await rel();
|
|
123
|
-
process.exit(0);
|
|
124
|
-
}
|
|
125
|
-
if (r.case === 'corrupt') {
|
|
126
|
-
// C2 修复:先 release lock 再 exit,避免 process.exit 跳过 finally
|
|
127
|
-
// 置 undefined 防止 finally 再次调用
|
|
128
|
-
const rel = release;
|
|
129
|
-
release = undefined;
|
|
130
|
-
if (rel)
|
|
131
|
-
await rel();
|
|
132
|
-
process.exit(4);
|
|
133
|
-
}
|
|
134
|
-
// C2 修复:先 release lock 再 exit,置 undefined 防止 finally 重复调用
|
|
135
|
-
const rel = release;
|
|
136
|
-
release = undefined;
|
|
137
|
-
if (rel)
|
|
138
|
-
await rel();
|
|
139
|
-
process.exit(0);
|
|
140
|
-
}
|
|
141
|
-
catch (err) {
|
|
142
|
-
if (err instanceof LockHeldError) {
|
|
143
|
-
console.error(err.message);
|
|
144
|
-
// LockHeldError 时 release 未赋值,无需 release
|
|
145
|
-
process.exit(5);
|
|
146
|
-
}
|
|
147
|
-
throw err;
|
|
148
|
-
}
|
|
149
|
-
finally {
|
|
150
|
-
if (release)
|
|
151
|
-
await release();
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
// —— plan-9e1 Task 5:--resume-summary 独立路径(v2 BLOCKER 1 + 5 修订)——
|
|
155
|
-
if (opts.resumeSummary) {
|
|
156
|
-
let release;
|
|
157
|
-
try {
|
|
158
|
-
release = await acquireLock(forgeRoot, 'resume-summary');
|
|
159
|
-
const r = await resumeArchiveSummary(forgeRoot, opts.resumeSummary);
|
|
160
|
-
if (r.kind === 'ok') {
|
|
161
|
-
console.log(r.message);
|
|
162
|
-
const rel = release;
|
|
163
|
-
release = undefined;
|
|
164
|
-
if (rel)
|
|
165
|
-
await rel();
|
|
166
|
-
process.exit(0);
|
|
167
|
-
}
|
|
168
|
-
// v2 BLOCKER 5:corrupt → exit 3(沿 master §3.12.3 corrupt 档)
|
|
169
|
-
if (r.kind === 'corrupt') {
|
|
170
|
-
console.error(r.message);
|
|
171
|
-
const rel = release;
|
|
172
|
-
release = undefined;
|
|
173
|
-
if (rel)
|
|
174
|
-
await rel();
|
|
175
|
-
process.exit(3);
|
|
176
|
-
}
|
|
177
|
-
// conflict / missing 都是 business-fail → exit 1
|
|
178
|
-
console.error(r.message);
|
|
179
|
-
const rel = release;
|
|
180
|
-
release = undefined;
|
|
181
|
-
if (rel)
|
|
182
|
-
await rel();
|
|
183
|
-
process.exit(1);
|
|
184
|
-
}
|
|
185
|
-
catch (err) {
|
|
186
|
-
if (err instanceof LockHeldError) {
|
|
187
|
-
// v2 BLOCKER 1:lock-held → exit 2(对齐 master §3.12.3 freeze;v0.4 --recover exit 5 是遗留)
|
|
188
|
-
console.error(err.message);
|
|
189
|
-
process.exit(2);
|
|
190
|
-
}
|
|
191
|
-
throw err;
|
|
192
|
-
}
|
|
193
|
-
finally {
|
|
194
|
-
if (release)
|
|
195
|
-
await release();
|
|
196
|
-
}
|
|
183
|
+
const targetPath = process.cwd();
|
|
184
|
+
const forgeRoot = join(targetPath, 'forge');
|
|
185
|
+
const changesDir = join(forgeRoot, 'changes');
|
|
186
|
+
const archiveDir = join(changesDir, 'archive');
|
|
187
|
+
const mainSpecsDir = join(forgeRoot, 'specs');
|
|
188
|
+
// —— 1. forge/changes/ 存在性 + changeId 选择 ——
|
|
189
|
+
if (!existsSync(changesDir)) {
|
|
190
|
+
console.error("✗ No forge/changes/ directory. Run 'forge init' first.");
|
|
191
|
+
process.exit(1);
|
|
197
192
|
}
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
// 若 gate 通过但后续 marker check / fence / archiveTransaction 任一失败,
|
|
201
|
-
// 暂停态保留 → 用户修好后可再 `forge archive --resume` 重入(否则会因
|
|
202
|
-
// 「无暂停态文件」被拒,--resume 失去重入性陷入死角)。
|
|
203
|
-
let resumePausePath;
|
|
204
|
-
if (opts.resume) {
|
|
193
|
+
if (!changeId) {
|
|
194
|
+
changeId = await selectChange(changesDir);
|
|
205
195
|
if (!changeId) {
|
|
206
|
-
console.error('✗
|
|
196
|
+
console.error('✗ No active change selected.');
|
|
207
197
|
process.exit(1);
|
|
208
198
|
}
|
|
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
199
|
}
|
|
220
|
-
|
|
221
|
-
if (!
|
|
222
|
-
console.error('
|
|
200
|
+
const changeDir = join(changesDir, changeId);
|
|
201
|
+
if (!existsSync(changeDir) || !statSync(changeDir).isDirectory()) {
|
|
202
|
+
console.error(`✗ Change '${changeId}' not found in forge/changes/`);
|
|
223
203
|
process.exit(1);
|
|
224
204
|
}
|
|
225
|
-
//
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
});
|
|
241
|
-
if (preflightResult.kind !== 'ok') {
|
|
242
|
-
console.error(preflightResult.message);
|
|
243
|
-
await archiveRelease();
|
|
244
|
-
process.exit(2);
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
// plan-9g 默认开启 14 不变量 fence(brainstorm v6 删两 opt-in flag);所有 archive 调用都跑
|
|
248
|
-
const fenceResult = await crossCuttingFenceCheck(join(forgeRoot, 'changes', changeId));
|
|
249
|
-
if (!fenceResult.ok) {
|
|
250
|
-
console.error('✗ cross-cutting fence rejected archive:');
|
|
251
|
-
for (const r of fenceResult.results) {
|
|
252
|
-
if (!r.ok)
|
|
253
|
-
console.error(` - ${r.invariant}: ${r.reason}`);
|
|
254
|
-
}
|
|
255
|
-
const rel = archiveRelease;
|
|
256
|
-
archiveRelease = undefined;
|
|
257
|
-
if (rel)
|
|
258
|
-
await rel();
|
|
259
|
-
process.exit(1);
|
|
260
|
-
}
|
|
261
|
-
const changeDir = join(forgeRoot, 'changes', changeId);
|
|
262
|
-
const verifyPath = join(changeDir, '.verify-passed');
|
|
263
|
-
const reviewPath = join(changeDir, '.review-passed');
|
|
264
|
-
// 步骤 1:检查 .verify-passed 和 .review-passed 都存在
|
|
265
|
-
if (!existsSync(verifyPath)) {
|
|
266
|
-
console.error(`✗ archive 拒绝:缺 .verify-passed (in ${changeDir})`);
|
|
267
|
-
// C2 修复:先 release lock 再 exit
|
|
268
|
-
await archiveRelease();
|
|
269
|
-
process.exit(2);
|
|
270
|
-
}
|
|
271
|
-
if (!existsSync(reviewPath)) {
|
|
272
|
-
console.error(`✗ archive 拒绝:缺 .review-passed (in ${changeDir})`);
|
|
273
|
-
// C2 修复:先 release lock 再 exit
|
|
274
|
-
await archiveRelease();
|
|
275
|
-
process.exit(2);
|
|
276
|
-
}
|
|
277
|
-
// 步骤 2:解析 + schema 校验两个 marker
|
|
278
|
-
const verifyText = await readFile(verifyPath, 'utf8');
|
|
279
|
-
const reviewText = await readFile(reviewPath, 'utf8');
|
|
280
|
-
const verifyMarker = parseMarker(verifyText);
|
|
281
|
-
const reviewMarker = parseMarker(reviewText);
|
|
282
|
-
// 步骤 2a:schema 结构校验
|
|
283
|
-
const verifyResult = validateMarkerSchema(verifyMarker, verifyPath);
|
|
284
|
-
if (!verifyResult.valid) {
|
|
285
|
-
console.error(`✗ .verify-passed marker schema 校验失败:${verifyResult.errors[0]?.message}`);
|
|
286
|
-
// C2 修复:先 release lock 再 exit
|
|
287
|
-
await archiveRelease();
|
|
288
|
-
process.exit(2);
|
|
289
|
-
}
|
|
290
|
-
const reviewResult = validateMarkerSchema(reviewMarker, reviewPath);
|
|
291
|
-
if (!reviewResult.valid) {
|
|
292
|
-
console.error(`✗ .review-passed marker schema 校验失败:${reviewResult.errors[0]?.message}`);
|
|
293
|
-
// C2 修复:先 release lock 再 exit
|
|
294
|
-
await archiveRelease();
|
|
295
|
-
process.exit(2);
|
|
296
|
-
}
|
|
297
|
-
// 步骤 2b:P1.1 — schema 类型限定(.verify-passed 必须是 forge-verify/v1)
|
|
298
|
-
// 通过 unknown 中转以绕过 TS 的交叉类型检查
|
|
299
|
-
const verifyRec = verifyMarker;
|
|
300
|
-
const reviewRec = reviewMarker;
|
|
301
|
-
if (verifyRec['schema'] !== 'forge-verify/v1') {
|
|
302
|
-
console.error(`✗ .verify-passed 必须是 forge-verify/v1,实际:${String(verifyRec['schema'])}`);
|
|
303
|
-
await archiveRelease();
|
|
304
|
-
process.exit(2);
|
|
305
|
-
}
|
|
306
|
-
if (reviewRec['schema'] !== 'forge-review/v1') {
|
|
307
|
-
console.error(`✗ .review-passed 必须是 forge-review/v1,实际:${String(reviewRec['schema'])}`);
|
|
308
|
-
await archiveRelease();
|
|
309
|
-
process.exit(2);
|
|
310
|
-
}
|
|
311
|
-
// 步骤 3:重算 tasks_hash 和 content_hash + 比对
|
|
312
|
-
const tasksContent = await readFile(join(changeDir, 'tasks.md'), 'utf8');
|
|
313
|
-
const tasksHashNow = computeTasksHash(tasksContent);
|
|
314
|
-
const contentHashNow = await computeContentHash(changeDir);
|
|
315
|
-
// 将 marker 转为 Record 以便访问字段
|
|
316
|
-
const vRec = verifyRec;
|
|
317
|
-
const rRec = reviewRec;
|
|
318
|
-
// 比对 verify marker 里的 hash
|
|
319
|
-
if (tasksHashNow !== vRec['tasks_hash'] || contentHashNow !== vRec['content_hash']) {
|
|
320
|
-
console.error('✗ .verify-passed marker 已过期(tasks/content hash 不匹配),请重跑 verify');
|
|
321
|
-
// C2 修复:先 release lock 再 exit
|
|
322
|
-
// plan-9d Task 6 v2 M-1 修订:hash mismatch = fence business-fail,exit 1(沿 master §3.12.3)
|
|
323
|
-
await archiveRelease();
|
|
324
|
-
process.exit(1);
|
|
325
|
-
}
|
|
326
|
-
// 比对 review marker 里的 hash
|
|
327
|
-
if (tasksHashNow !== rRec['tasks_hash'] || contentHashNow !== rRec['content_hash']) {
|
|
328
|
-
console.error('✗ .review-passed marker 已过期(tasks/content hash 不匹配),请重跑 review');
|
|
329
|
-
// C2 修复:先 release lock 再 exit
|
|
330
|
-
// plan-9d Task 6 v2 M-1 修订:hash mismatch = fence business-fail,exit 1
|
|
331
|
-
await archiveRelease();
|
|
332
|
-
process.exit(1);
|
|
333
|
-
}
|
|
334
|
-
// 步骤 3.4(plan-9j Task 5):legacy-exemption + version-retrograde fence
|
|
335
|
-
// 沿 design §3.4.4.1 + §3.4.4.3 — 在 evidence 校验之前拦截 legacy marker 异常
|
|
336
|
-
const legacyVerifyResult = validateLegacyExemption(verifyRec, verifyPath);
|
|
337
|
-
if (!legacyVerifyResult.valid) {
|
|
338
|
-
console.error('✗ legacy-exemption fence 拒签(verify-passed):');
|
|
339
|
-
for (const e of legacyVerifyResult.errors)
|
|
340
|
-
console.error(` - ${e.field}: ${e.message}`);
|
|
341
|
-
await archiveRelease();
|
|
342
|
-
process.exit(1);
|
|
343
|
-
}
|
|
344
|
-
const legacyReviewResult = validateLegacyExemption(reviewRec, reviewPath);
|
|
345
|
-
if (!legacyReviewResult.valid) {
|
|
346
|
-
console.error('✗ legacy-exemption fence 拒签(review-passed):');
|
|
347
|
-
for (const e of legacyReviewResult.errors)
|
|
348
|
-
console.error(` - ${e.field}: ${e.message}`);
|
|
349
|
-
await archiveRelease();
|
|
350
|
-
process.exit(1);
|
|
351
|
-
}
|
|
352
|
-
const retrogradeVerifyResult = await validateVersionRetrograde(verifyPath, verifyRec, process.cwd());
|
|
353
|
-
if (!retrogradeVerifyResult.valid) {
|
|
354
|
-
console.error('✗ version-retrograde fence 拒签(verify-passed):');
|
|
355
|
-
for (const e of retrogradeVerifyResult.errors)
|
|
356
|
-
console.error(` - ${e.field}: ${e.message}`);
|
|
357
|
-
await archiveRelease();
|
|
358
|
-
process.exit(1);
|
|
359
|
-
}
|
|
360
|
-
const retrogradeReviewResult = await validateVersionRetrograde(reviewPath, reviewRec, process.cwd());
|
|
361
|
-
if (!retrogradeReviewResult.valid) {
|
|
362
|
-
console.error('✗ version-retrograde fence 拒签(review-passed):');
|
|
363
|
-
for (const e of retrogradeReviewResult.errors)
|
|
364
|
-
console.error(` - ${e.field}: ${e.message}`);
|
|
365
|
-
await archiveRelease();
|
|
366
|
-
process.exit(1);
|
|
367
|
-
}
|
|
368
|
-
// 步骤 3.5:P1.2 — 验证 evidence 完整性
|
|
369
|
-
const evResult = await validateEvidence(verifyRec, verifyPath);
|
|
370
|
-
if (!evResult.valid) {
|
|
371
|
-
console.error('✗ verify evidence 校验失败:');
|
|
372
|
-
for (const e of evResult.errors)
|
|
373
|
-
console.error(` - ${e.field}: ${e.message}`);
|
|
374
|
-
// plan-9d Task 6 v2 M-1 修订:evidence 校验失败 = fence business-fail,exit 1
|
|
375
|
-
await archiveRelease();
|
|
376
|
-
process.exit(1);
|
|
377
|
-
}
|
|
378
|
-
// 步骤 3.6:plan-9d Task 6 — verify_findings fence 三级 × resolved × ack 矩阵 + finding_hash 篡改拒签
|
|
379
|
-
const vfResult = validateVerifyFindingsFence(verifyRec, verifyPath);
|
|
380
|
-
if (!vfResult.valid) {
|
|
381
|
-
console.error('✗ verify_findings fence 拒签:');
|
|
382
|
-
for (const e of vfResult.errors)
|
|
383
|
-
console.error(` - ${e.field}: ${e.message}`);
|
|
384
|
-
await archiveRelease();
|
|
385
|
-
process.exit(1);
|
|
386
|
-
}
|
|
387
|
-
// 步骤 3.7:plan-9c Task 2 — pause_decisions fence(option 1-4 五类业务校验 + CRITICAL 重定向)
|
|
388
|
-
// v2 codex MAJOR 4 修订:对 verifyRec + reviewRec 都跑 fence
|
|
389
|
-
// v4 codex NEW-MAJOR A6 + B4 联动:fence 不再需要 ctx 参数(B4 改用 parseMarkdown 局部段校验,
|
|
390
|
-
// 不再调 validateScopeEntries → ctx unused → 沿 YAGNI 移除)
|
|
391
|
-
// Task 12:从 verify marker 取链固化值组成 pauseFenceOpts(design §6.2 verify 是最后 freeze)
|
|
392
|
-
// marker 由 AI 产出,字段运行时可能是 string / null / undefined — 断言须含 null,
|
|
393
|
-
// 否则 null 会被错误窄化为 string,绕过 checkOption2TaskChecked 的 == null fail-closed 检查
|
|
394
|
-
const pauseFenceOpts = {
|
|
395
|
-
verifyAckLogTailHash: verifyRec['ack_log_tail_hash'],
|
|
396
|
-
verifyAckLogEntryCount: verifyRec['ack_log_entry_count'],
|
|
397
|
-
changeId,
|
|
398
|
-
};
|
|
399
|
-
const pdVerifyResult = await validatePauseDecisionsFence(verifyRec, changeDir, process.cwd(), pauseFenceOpts, verifyPath);
|
|
400
|
-
if (!pdVerifyResult.valid) {
|
|
401
|
-
console.error('✗ pause_decisions fence 拒签(verify-passed):');
|
|
402
|
-
for (const e of pdVerifyResult.errors)
|
|
403
|
-
console.error(` - ${e.field}: ${e.message}`);
|
|
404
|
-
await archiveRelease();
|
|
405
|
-
process.exit(1);
|
|
406
|
-
}
|
|
407
|
-
// 两处调用都传同一 pauseFenceOpts(都用 verify marker 的 tail/count — design §6.2)
|
|
408
|
-
const pdReviewResult = await validatePauseDecisionsFence(reviewRec, changeDir, process.cwd(), pauseFenceOpts, reviewPath);
|
|
409
|
-
if (!pdReviewResult.valid) {
|
|
410
|
-
console.error('✗ pause_decisions fence 拒签(review-passed):');
|
|
411
|
-
for (const e of pdReviewResult.errors)
|
|
412
|
-
console.error(` - ${e.field}: ${e.message}`);
|
|
413
|
-
await archiveRelease();
|
|
414
|
-
process.exit(1);
|
|
415
|
-
}
|
|
416
|
-
// 步骤 3.7.1:Task 12 — verify/review pause_decisions cross-check(design §6.5)
|
|
417
|
-
// 对两侧共有 id 的 pause_decision 逐字段比对;单侧独有 id 不拒签
|
|
418
|
-
const vDecisions = Array.isArray(verifyRec['pause_decisions'])
|
|
419
|
-
? verifyRec['pause_decisions']
|
|
420
|
-
: [];
|
|
421
|
-
const rDecisions = Array.isArray(reviewRec['pause_decisions'])
|
|
422
|
-
? reviewRec['pause_decisions']
|
|
423
|
-
: [];
|
|
424
|
-
const ccResult = crossCheckPauseDecisions(vDecisions, rDecisions, verifyPath);
|
|
425
|
-
if (!ccResult.valid) {
|
|
426
|
-
console.error('✗ pause_decisions verify/review cross-check 拒签:');
|
|
427
|
-
for (const e of ccResult.errors)
|
|
428
|
-
console.error(` - ${e.field}: ${e.message}`);
|
|
429
|
-
await archiveRelease();
|
|
430
|
-
process.exit(1);
|
|
431
|
-
}
|
|
432
|
-
// 步骤 3.8:plan-9d Task 6 v2 B-4 — ack-log 一致性 cross-check
|
|
433
|
-
// v3 codex BLOCKER 2 修订:对 verifyRec + reviewRec 都跑 cross-check
|
|
434
|
-
// (review marker 同样可承载 pause_decisions superset additive,沿 9c Task 1 schema)
|
|
435
|
-
const ackVerifyResult = await validateAckLogConsistency(changeDir, verifyRec, changeId);
|
|
436
|
-
if (!ackVerifyResult.valid) {
|
|
437
|
-
console.error('✗ ack-log 一致性校验失败(verify-passed):');
|
|
438
|
-
for (const e of ackVerifyResult.errors)
|
|
439
|
-
console.error(` - ${e.field}: ${e.message}`);
|
|
440
|
-
await archiveRelease();
|
|
441
|
-
process.exit(1);
|
|
442
|
-
}
|
|
443
|
-
const ackReviewResult = await validateAckLogConsistency(changeDir, reviewRec, changeId);
|
|
444
|
-
if (!ackReviewResult.valid) {
|
|
445
|
-
console.error('✗ ack-log 一致性校验失败(review-passed):');
|
|
446
|
-
for (const e of ackReviewResult.errors)
|
|
447
|
-
console.error(` - ${e.field}: ${e.message}`);
|
|
448
|
-
await archiveRelease();
|
|
449
|
-
process.exit(1);
|
|
450
|
-
}
|
|
451
|
-
// 步骤 3.9:plan-9e1 Task 4 — 三级业务行为 fence(沿 design §2.4.2)
|
|
452
|
-
// CRITICAL+resolved=false 拒签(sanity 双重保险)/ WARNING+resolved=false+无 ack 拒签 /
|
|
453
|
-
// WARNING+resolved=false+acked 通过 / SUGGESTION+resolved=false 通过(handoff to backlog)
|
|
454
|
-
const tlfResult = validateThreeLevelFence(verifyRec, reviewRec, verifyPath, reviewPath);
|
|
455
|
-
if (!tlfResult.valid) {
|
|
456
|
-
console.error('✗ 三级业务行为 fence 拒签:');
|
|
457
|
-
for (const e of tlfResult.errors)
|
|
458
|
-
console.error(` - ${e.field}: ${e.message}`);
|
|
459
|
-
await archiveRelease();
|
|
460
|
-
process.exit(1);
|
|
461
|
-
}
|
|
462
|
-
// 步骤 4:human-override + 真实 git 状态校验 + outcomes 校验
|
|
463
|
-
const verifyBy = vRec['verified_by'];
|
|
464
|
-
const reviewBy = rRec['reviewed_by'];
|
|
465
|
-
const reviewGit = reviewRec['git'];
|
|
466
|
-
const markerSaysGit = reviewGit?.['is_git_repo'] === true;
|
|
467
|
-
// P1 修复:用真实 git 状态决定,不信 marker 字段
|
|
468
|
-
const projectIsGit = isProjectActuallyGit(process.cwd());
|
|
469
|
-
// 4a. human-override 必须 --force
|
|
470
|
-
if (verifyBy === 'human-override' || reviewBy === 'human-override') {
|
|
471
|
-
if (!opts.force) {
|
|
472
|
-
console.error('✗ human-override 标记需要 --force 接受\n' +
|
|
473
|
-
' verified_by=' +
|
|
474
|
-
verifyBy +
|
|
475
|
-
' reviewed_by=' +
|
|
476
|
-
reviewBy);
|
|
477
|
-
// C2 修复:先 release lock 再 exit
|
|
478
|
-
await archiveRelease();
|
|
479
|
-
process.exit(2);
|
|
480
|
-
}
|
|
481
|
-
}
|
|
482
|
-
// 4b. marker 与真实 git 状态不一致 → 可疑伪造,拒绝(--force 也不能覆盖)
|
|
483
|
-
if (projectIsGit && !markerSaysGit) {
|
|
484
|
-
console.error('✗ review marker 标记 is_git_repo=false,但项目实际是 git。可能为伪造,拒绝(即使 --force)');
|
|
485
|
-
await archiveRelease();
|
|
486
|
-
process.exit(2);
|
|
487
|
-
}
|
|
488
|
-
if (!projectIsGit && markerSaysGit) {
|
|
489
|
-
console.error('✗ review marker 标记 is_git_repo=true,但项目实际不是 git。可能为伪造,拒绝');
|
|
490
|
-
await archiveRelease();
|
|
491
|
-
process.exit(2);
|
|
492
|
-
}
|
|
493
|
-
// 4c. 真非 git → 必须 --force(spec §3.4 要求)
|
|
494
|
-
if (!projectIsGit && !opts.force) {
|
|
495
|
-
console.error(`✗ 非 git 项目下 review 标记不绑定代码 diff,archive 必须 --force 才接受`);
|
|
496
|
-
await archiveRelease();
|
|
497
|
-
process.exit(2);
|
|
498
|
-
}
|
|
499
|
-
// 4d. 真 git → 跑 git integrity(重算 head + diff_hash)
|
|
500
|
-
if (projectIsGit) {
|
|
501
|
-
const gitResult = await validateReviewGitIntegrity(reviewRec, process.cwd(), reviewPath);
|
|
502
|
-
if (!gitResult.valid) {
|
|
503
|
-
console.error('✗ review git 完整性校验失败:');
|
|
504
|
-
for (const e of gitResult.errors)
|
|
505
|
-
console.error(` - ${e.field}: ${e.message}`);
|
|
506
|
-
await archiveRelease();
|
|
507
|
-
process.exit(2);
|
|
508
|
-
}
|
|
205
|
+
// —— 2. legacy-bridge preflight(brownfield 同步检查;graceful skip 路径多)——
|
|
206
|
+
const preflightResult = await runArchivePreflight(forgeRoot, changeId, {
|
|
207
|
+
api: opts.api ?? false,
|
|
208
|
+
});
|
|
209
|
+
if (preflightResult.kind !== 'ok') {
|
|
210
|
+
console.error(preflightResult.message);
|
|
211
|
+
process.exit(2);
|
|
212
|
+
}
|
|
213
|
+
// —— 3. Spec validate(永远跑,blocking — sanity check 非 fence)——
|
|
214
|
+
// v9 修订(Codex v8 F-R8-1):删 --no-validate flag,archive 永远 validate
|
|
215
|
+
const validateResult = await validateChange(changeDir);
|
|
216
|
+
if (!validateResult.valid) {
|
|
217
|
+
console.error('✗ Spec validate failed:');
|
|
218
|
+
for (const e of validateResult.errors) {
|
|
219
|
+
console.error(` - ${e.artifact}${e.field ? `.${e.field}` : ''}: ${e.message}`);
|
|
509
220
|
}
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
221
|
+
process.exit(1);
|
|
222
|
+
}
|
|
223
|
+
// —— 4. marker 存在性 + v2 schema validate ——
|
|
224
|
+
const verifyPath = join(changeDir, '.verify-passed');
|
|
225
|
+
const reviewPath = join(changeDir, '.review-passed');
|
|
226
|
+
if (!existsSync(verifyPath)) {
|
|
227
|
+
console.error(`✗ Missing .verify-passed in ${changeDir}. Run /forge:verify first.`);
|
|
228
|
+
process.exit(1);
|
|
229
|
+
}
|
|
230
|
+
if (!existsSync(reviewPath)) {
|
|
231
|
+
console.error(`✗ Missing .review-passed in ${changeDir}. Run /forge:review first.`);
|
|
232
|
+
process.exit(1);
|
|
233
|
+
}
|
|
234
|
+
const verifyText = await readFile(verifyPath, 'utf8');
|
|
235
|
+
const reviewText = await readFile(reviewPath, 'utf8');
|
|
236
|
+
const verifyMarker = parseMarker(verifyText);
|
|
237
|
+
const reviewMarker = parseMarker(reviewText);
|
|
238
|
+
const verifyValid = validateMarkerSchema(verifyMarker, verifyPath);
|
|
239
|
+
if (!verifyValid.valid) {
|
|
240
|
+
console.error('✗ .verify-passed schema invalid:');
|
|
241
|
+
for (const e of verifyValid.errors) {
|
|
242
|
+
console.error(` - ${e.field ?? 'marker'}: ${e.message}`);
|
|
518
243
|
}
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
changeId, fenceResult);
|
|
244
|
+
process.exit(1);
|
|
245
|
+
}
|
|
246
|
+
const reviewValid = validateMarkerSchema(reviewMarker, reviewPath);
|
|
247
|
+
if (!reviewValid.valid) {
|
|
248
|
+
console.error('✗ .review-passed schema invalid:');
|
|
249
|
+
for (const e of reviewValid.errors) {
|
|
250
|
+
console.error(` - ${e.field ?? 'marker'}: ${e.message}`);
|
|
527
251
|
}
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
252
|
+
process.exit(1);
|
|
253
|
+
}
|
|
254
|
+
// schema 字段 v2 类型限定(双保险)
|
|
255
|
+
if (verifyMarker.schema !== 'forge-verify/v2') {
|
|
256
|
+
console.error(`✗ .verify-passed must be forge-verify/v2, got: ${String(verifyMarker.schema)}`);
|
|
257
|
+
process.exit(1);
|
|
258
|
+
}
|
|
259
|
+
if (reviewMarker.schema !== 'forge-review/v2') {
|
|
260
|
+
console.error(`✗ .review-passed must be forge-review/v2, got: ${String(reviewMarker.schema)}`);
|
|
261
|
+
process.exit(1);
|
|
262
|
+
}
|
|
263
|
+
// —— 4b. human-override 处理 ——
|
|
264
|
+
if ((verifyMarker.verified_by === 'human-override' ||
|
|
265
|
+
reviewMarker.reviewed_by === 'human-override') &&
|
|
266
|
+
!opts.force) {
|
|
267
|
+
console.error('✗ human-override marker requires --force\n' +
|
|
268
|
+
` verified_by=${verifyMarker.verified_by} reviewed_by=${reviewMarker.reviewed_by}`);
|
|
269
|
+
process.exit(1);
|
|
270
|
+
}
|
|
271
|
+
// —— 5. Task progress confirm(未完成 + 非 --yes 才弹)——
|
|
272
|
+
const incomplete = await countIncompleteTasks(changeDir);
|
|
273
|
+
if (incomplete > 0 && !opts.yes) {
|
|
274
|
+
// dangerous-by-default:回车 = NO(沿 plan §6.1 line 326 + Codex Phase 1 F-2)
|
|
275
|
+
const proceed = await confirmPrompt(`${incomplete} incomplete task(s) in tasks.md. Continue archive anyway?`, false);
|
|
276
|
+
if (!proceed) {
|
|
277
|
+
console.log('Archive cancelled.');
|
|
278
|
+
return;
|
|
535
279
|
}
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
280
|
+
}
|
|
281
|
+
// —— 6. Spec deltas 应用 — Path B 用 forge 现有 readDeltas + applyDeltas ——
|
|
282
|
+
let deltas = [];
|
|
283
|
+
if (!opts.skipSpecs) {
|
|
284
|
+
const changeSpecsDir = join(changeDir, 'specs');
|
|
285
|
+
deltas = await readDeltas(changeSpecsDir, mainSpecsDir);
|
|
286
|
+
if (deltas.length > 0) {
|
|
287
|
+
console.log(renderDeltasSummary(deltas));
|
|
288
|
+
if (!opts.yes) {
|
|
289
|
+
// safe-by-default:回车 = YES(spec deltas 是 archive 正常路径,plan §6.1 line 345 `default: true`)
|
|
290
|
+
const proceed = await confirmPrompt('Apply spec deltas?', true);
|
|
291
|
+
if (!proceed) {
|
|
292
|
+
console.log('Skipping spec deltas. Continuing with archive.');
|
|
293
|
+
deltas = [];
|
|
294
|
+
}
|
|
545
295
|
}
|
|
546
|
-
|
|
547
|
-
|
|
296
|
+
if (deltas.length > 0) {
|
|
297
|
+
await applyDeltas(mainSpecsDir, deltas);
|
|
548
298
|
}
|
|
549
299
|
}
|
|
550
|
-
// 步骤 5.5:Plan 7 post-archive hook(enforce_sync=false 时不阻塞,只产报告)
|
|
551
|
-
// Task 6.3 I-2:透传 archiveDate(避免 posthook 自己算 new Date() 跨午夜偏一天)
|
|
552
|
-
// Task 6.3 C-1:透传 --api
|
|
553
|
-
await runArchivePostHook(forgeRoot, changeId, {
|
|
554
|
-
archiveDate,
|
|
555
|
-
api: opts.api ?? false,
|
|
556
|
-
});
|
|
557
|
-
// 步骤 6:plan-9e1 Task 4 — 渲染 archive_summary 输出(沿 design §2.4.4)
|
|
558
|
-
const archiveDirName = `${archiveDate}-${changeId}`;
|
|
559
|
-
console.log(renderArchiveSummaryOutput(archiveSummary, archiveDirName));
|
|
560
|
-
// 步骤 6.5:plan-backlog-registry — 重生成 forge/backlog/ 注册表
|
|
561
|
-
// 失败不回滚 archive(archive 主流程已成功,backlog 是衍生产物)
|
|
562
|
-
try {
|
|
563
|
-
const bl = await generateBacklog(forgeRoot);
|
|
564
|
-
console.log(`Backlog: forge/backlog/active.md (${bl.openCount} open, ${bl.warningCount} warnings)`);
|
|
565
|
-
}
|
|
566
|
-
catch (blErr) {
|
|
567
|
-
const m = blErr instanceof Error ? blErr.message : String(blErr);
|
|
568
|
-
console.error(`⚠ backlog 重生成失败(不影响 archive):${m} —— 可手动跑 \`forge backlog\``);
|
|
569
|
-
}
|
|
570
300
|
}
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
await archiveRelease();
|
|
578
|
-
process.exit(5);
|
|
579
|
-
}
|
|
580
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
581
|
-
if (msg.includes('AND rollback failed')) {
|
|
582
|
-
// 同步失败且回滚也失败 → 需人工介入 → exit 3
|
|
583
|
-
console.error(`✗ ${msg}`);
|
|
584
|
-
if (archiveRelease)
|
|
585
|
-
await archiveRelease();
|
|
586
|
-
process.exit(3);
|
|
587
|
-
}
|
|
588
|
-
if (msg.includes('rolled back')) {
|
|
589
|
-
// 同步失败但回滚成功 → 可重试 → exit 2
|
|
590
|
-
console.error(`✗ ${msg}`);
|
|
591
|
-
if (archiveRelease)
|
|
592
|
-
await archiveRelease();
|
|
593
|
-
process.exit(2);
|
|
594
|
-
}
|
|
595
|
-
// 兜底:未知错误 → exit 1
|
|
596
|
-
console.error(`✗ ${msg}`);
|
|
597
|
-
if (archiveRelease)
|
|
598
|
-
await archiveRelease();
|
|
301
|
+
// —— 7. archiveDate(算一次 — F-R3-2)+ mv changeDir → archive/<archiveName> ——
|
|
302
|
+
const archiveDate = getArchiveDate();
|
|
303
|
+
const archiveName = `${archiveDate}-${changeId}`;
|
|
304
|
+
const archivePath = join(archiveDir, archiveName);
|
|
305
|
+
if (existsSync(archivePath)) {
|
|
306
|
+
console.error(`✗ Archive '${archiveName}' already exists.`);
|
|
599
307
|
process.exit(1);
|
|
600
308
|
}
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
309
|
+
await mkdir(archiveDir, { recursive: true });
|
|
310
|
+
await moveDirectory(changeDir, archivePath);
|
|
311
|
+
// —— 8. buildArchiveSummary(v2 新 signature)+ 写 archive_summary.yaml ——
|
|
312
|
+
const summary = await buildArchiveSummary({
|
|
313
|
+
archivePath,
|
|
314
|
+
changeId,
|
|
315
|
+
verifyMarker,
|
|
316
|
+
reviewMarker,
|
|
317
|
+
deltas,
|
|
318
|
+
});
|
|
319
|
+
await writeFile(join(archivePath, 'archive_summary.yaml'), stringifyYaml(summary), 'utf8');
|
|
320
|
+
// —— 9a. legacy-bridge posthook(透传 archiveDate 防跨午夜偏)——
|
|
321
|
+
await runArchivePostHook(forgeRoot, changeId, {
|
|
322
|
+
archiveDate,
|
|
323
|
+
api: opts.api ?? false,
|
|
324
|
+
});
|
|
325
|
+
// —— 9b. 打印用户输出(渲染 summary)——
|
|
326
|
+
console.log(renderArchiveSummaryOutput(summary, archiveName));
|
|
327
|
+
// —— 9c. 自动重生成 backlog(失败仅 warn,不回滚 archive)——
|
|
328
|
+
try {
|
|
329
|
+
const bl = await generateBacklog(forgeRoot);
|
|
330
|
+
console.log(`Backlog: forge/backlog/active.md (${bl.openCount} open, ${bl.warningCount} warnings)`);
|
|
605
331
|
}
|
|
606
|
-
|
|
607
|
-
}
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
332
|
+
catch (e) {
|
|
333
|
+
console.warn(`⚠ backlog 重生成失败(不影响 archive):${e.message} —— 可手动跑 \`forge backlog\``);
|
|
334
|
+
}
|
|
335
|
+
// —— 9d. 顺手 emit monitor trace event(沿 plan §10.3 Task 3.15)——
|
|
336
|
+
appendTraceEvent(targetPath, {
|
|
337
|
+
ts: new Date().toISOString(),
|
|
338
|
+
schema: 'forge-monitor-trace/v1',
|
|
339
|
+
change_id: changeId,
|
|
340
|
+
stage: 'archive',
|
|
341
|
+
layer: 'cli',
|
|
342
|
+
event: 'change_archived',
|
|
343
|
+
data: {
|
|
344
|
+
archive_name: archiveName,
|
|
345
|
+
spec_updates: deltas.length,
|
|
346
|
+
forge_version: FORGE_VERSION,
|
|
347
|
+
},
|
|
348
|
+
});
|
|
349
|
+
});
|
|
623
350
|
}
|
|
624
351
|
/**
|
|
625
352
|
* Plan 7 §2.5 + Task 6.3:archive preflight — enforce_sync=true 时拦截 sync 差异。
|
|
@@ -631,24 +358,18 @@ async function restoreSpecsFromBackup(backupDir, currentSpecsDir) {
|
|
|
631
358
|
* 4. legacy_bridge.enforce_sync != true
|
|
632
359
|
* 5. 无受影响 anchor / 全部读取失败(buildSyncCheckTask 返 null)
|
|
633
360
|
*
|
|
634
|
-
*
|
|
635
|
-
* - **agent 模式(
|
|
636
|
-
*
|
|
637
|
-
* - **--api 模式**:进程内直连 Anthropic API 跑 sync-check + 写 sync-state →
|
|
638
|
-
* 含 critical pending 返 {kind:'critical-pending'};否则返 {kind:'ok'} 续跑 archive。
|
|
361
|
+
* 双模式:
|
|
362
|
+
* - **agent 模式(默认)**:emit sync-check manifest + 暂停态 → halted-for-fulfillment
|
|
363
|
+
* - **--api 模式**:进程内直连 API 跑 sync-check + 写 sync-state → critical-pending 或 ok
|
|
639
364
|
*
|
|
640
|
-
* ack 不就绪
|
|
641
|
-
*
|
|
642
|
-
* **不再调 process.exit / console.error**:caller(archive 命令)负责打印消息 +
|
|
643
|
-
* release archive.lock + exit,避免本函数内 process.exit 跳过 caller finally
|
|
644
|
-
* 导致 archive.lock 文件残留。
|
|
365
|
+
* ack 不就绪 → ack-missing。
|
|
645
366
|
*/
|
|
646
367
|
export async function runArchivePreflight(forgeRoot, changeId, opts = { api: false }) {
|
|
647
368
|
const configPath = join(forgeRoot, 'config.yaml');
|
|
648
369
|
if (!existsSync(configPath))
|
|
649
370
|
return { kind: 'ok' };
|
|
650
371
|
const config = parseYaml(await readFile(configPath, 'utf8'));
|
|
651
|
-
// §2.5:仅当 legacy-anchors.yaml 存在 AND allow_llm_calls
|
|
372
|
+
// §2.5:仅当 legacy-anchors.yaml 存在 AND allow_llm_calls AND enforce_sync 时进入
|
|
652
373
|
const anchors = await loadAnchorsFile(forgeRoot).catch(() => null);
|
|
653
374
|
if (!anchors)
|
|
654
375
|
return { kind: 'ok' };
|
|
@@ -663,25 +384,19 @@ export async function runArchivePreflight(forgeRoot, changeId, opts = { api: fal
|
|
|
663
384
|
message: `legacy_bridge.enforce_sync=true 但 ack 未就绪:${ack.reason};请先跑 forge legacy-bridge --acknowledge-data-transfer`,
|
|
664
385
|
};
|
|
665
386
|
}
|
|
666
|
-
// 拼 change context(I-3:gate 已加载的 config + anchors
|
|
667
|
-
// preflight 时 change 还在 forge/changes/<id>/(archived=false,无需 archiveDate)
|
|
387
|
+
// 拼 change context(I-3:gate 已加载的 config + anchors 传入)
|
|
668
388
|
const ctx = await buildSyncCheckChangeContext(forgeRoot, changeId, { archived: false }, config, anchors);
|
|
669
389
|
if (opts.api) {
|
|
670
|
-
// —— --api 模式:进程内直连 API 跑 sync-check
|
|
390
|
+
// —— --api 模式:进程内直连 API 跑 sync-check ——
|
|
671
391
|
const task = await buildSyncCheckTask(ctx, async (p) => (await readAnchorFile(p)).text);
|
|
672
392
|
if (!task)
|
|
673
|
-
return { kind: 'ok' };
|
|
674
|
-
// 进程内调 Anthropic SDK(client 构造方式与旧 archive 代码一致:forge-eval/load-env)
|
|
393
|
+
return { kind: 'ok' };
|
|
675
394
|
const client = (await makeForgeApiClient());
|
|
676
395
|
const results = await new ApiRunner(client).run([task]);
|
|
677
|
-
// --api 无 manifest,produced_from 标 'api-inline' sentinel(区别于 agent 路径的 manifest_hash;
|
|
678
|
-
// 该 sync-state 不经 resume gate,Task 6.4 据此 sentinel 可识别 --api 来源)
|
|
679
396
|
const syncState = applySyncCheckResult(results[0]?.text ?? '', changeId, 'api-inline');
|
|
680
|
-
// 写 sync-state 到旧路径(沿旧 preflight 写盘方式)
|
|
681
397
|
await mkdir(join(forgeRoot, 'legacy-sync-state'), { recursive: true });
|
|
682
398
|
await writeFile(join(forgeRoot, 'legacy-sync-state', `${changeId}.md`), renderDiffMarkdown(syncState), 'utf8');
|
|
683
399
|
await writeFile(join(forgeRoot, 'legacy-sync-state', `${changeId}.yaml`), renderDiffYaml(syncState), 'utf8');
|
|
684
|
-
// 含 critical pending → critical-pending(死变体的归宿:archive 主流程 kind!=='ok' 会 halt)
|
|
685
400
|
const criticalDiffs = syncState.diffs.filter((d) => d.severity === 'critical' && d.status === 'pending');
|
|
686
401
|
if (criticalDiffs.length > 0) {
|
|
687
402
|
return {
|
|
@@ -693,13 +408,10 @@ export async function runArchivePreflight(forgeRoot, changeId, opts = { api: fal
|
|
|
693
408
|
}
|
|
694
409
|
return { kind: 'ok' };
|
|
695
410
|
}
|
|
696
|
-
// —— agent 模式(默认):emit sync-check manifest +
|
|
411
|
+
// —— agent 模式(默认):emit sync-check manifest + 暂停态 ——
|
|
697
412
|
const emitResult = await emitPreflightSyncCheck(forgeRoot, changeId, ctx);
|
|
698
|
-
if (emitResult.kind === 'skip')
|
|
699
|
-
// 无受影响 anchor / 全部读取失败 → graceful skip,继续 archive
|
|
413
|
+
if (emitResult.kind === 'skip')
|
|
700
414
|
return { kind: 'ok' };
|
|
701
|
-
}
|
|
702
|
-
// halted-for-fulfillment:manifest 已写,暂停态已写,告知 caller halt
|
|
703
415
|
return {
|
|
704
416
|
kind: 'halted-for-fulfillment',
|
|
705
417
|
message: `sync-check manifest 已 emit(preflight);\n` +
|
|
@@ -708,11 +420,10 @@ export async function runArchivePreflight(forgeRoot, changeId, opts = { api: fal
|
|
|
708
420
|
};
|
|
709
421
|
}
|
|
710
422
|
/**
|
|
711
|
-
* Plan 7 §2.5 + Task 6.3:post-archive hook —
|
|
712
|
-
* enforce_sync=true 已由 preflight 跑过;graceful skip 路径同 preflight。
|
|
423
|
+
* Plan 7 §2.5 + Task 6.3:post-archive hook — 不阻塞,enforce_sync=false 时跑(产报告)。
|
|
713
424
|
*
|
|
714
|
-
* agent 模式:emit sync-check manifest(非阻塞,archive
|
|
715
|
-
* --api 模式:进程内直连 API 跑 sync-check + 写 sync-state(
|
|
425
|
+
* agent 模式:emit sync-check manifest(非阻塞,archive 已完成)
|
|
426
|
+
* --api 模式:进程内直连 API 跑 sync-check + 写 sync-state(非阻塞)
|
|
716
427
|
*
|
|
717
428
|
* I-2:archiveDate 由 archive 主流程透传(不自己算 new Date(),避免跨午夜偏一天)。
|
|
718
429
|
*/
|
|
@@ -730,18 +441,14 @@ export async function runArchivePostHook(forgeRoot, changeId, opts) {
|
|
|
730
441
|
return;
|
|
731
442
|
const ack = await checkAck(forgeRoot, config, anchors);
|
|
732
443
|
if (!ack.ok)
|
|
733
|
-
return;
|
|
734
|
-
// 拼 change context(I-3:gate 已加载的 config + anchors 传入;I-2:archiveDate 透传)
|
|
735
|
-
// posthook 时 change 已归档到 archive/<archiveDate>-<id>/
|
|
444
|
+
return;
|
|
736
445
|
const ctx = await buildSyncCheckChangeContext(forgeRoot, changeId, { archived: true, archiveDate: opts.archiveDate }, config, anchors);
|
|
737
446
|
if (opts.api) {
|
|
738
|
-
// —— --api 模式:进程内直连 API 跑 sync-check + 写 sync-state(
|
|
739
|
-
// archive transaction 已完成;posthook 的 API 调用失败不应让 archive 报 exit 1 ——
|
|
740
|
-
// I-A:整段包 try/catch,失败降级为 warn(agent 模式 posthook 只写本地文件无此风险)
|
|
447
|
+
// —— --api 模式:进程内直连 API 跑 sync-check + 写 sync-state(非阻塞,失败仅 warn)——
|
|
741
448
|
try {
|
|
742
449
|
const task = await buildSyncCheckTask(ctx, async (p) => (await readAnchorFile(p)).text);
|
|
743
450
|
if (!task)
|
|
744
|
-
return;
|
|
451
|
+
return;
|
|
745
452
|
const client = (await makeForgeApiClient());
|
|
746
453
|
const results = await new ApiRunner(client).run([task]);
|
|
747
454
|
const syncState = applySyncCheckResult(results[0]?.text ?? '', changeId, 'api-inline');
|
|
@@ -755,12 +462,11 @@ export async function runArchivePostHook(forgeRoot, changeId, opts) {
|
|
|
755
462
|
}
|
|
756
463
|
return;
|
|
757
464
|
}
|
|
758
|
-
// —— agent 模式(默认):emit sync-check manifest(
|
|
465
|
+
// —— agent 模式(默认):emit sync-check manifest(非阻塞)——
|
|
759
466
|
const emitResult = await emitPostHookSyncCheck(forgeRoot, changeId, ctx);
|
|
760
467
|
if (emitResult.kind === 'emitted') {
|
|
761
468
|
console.log(`⚠ sync-check manifest 已 emit(posthook);forge agent 履行后跑 forge legacy-bridge sync-check --apply --change-id ${changeId} 可得报告`);
|
|
762
469
|
}
|
|
763
|
-
// kind='skip':无受影响 anchor / 全部读取失败 → graceful skip,无输出
|
|
764
470
|
}
|
|
765
471
|
/**
|
|
766
472
|
* 抽出:合并 runArchivePreflight/runArchivePostHook 两处重复的 change context 拼装。
|
|
@@ -768,25 +474,18 @@ export async function runArchivePostHook(forgeRoot, changeId, opts) {
|
|
|
768
474
|
* archived=false → forge/changes/<id>/;
|
|
769
475
|
* archived=true → forge/changes/archive/<archiveDate>-<id>/。
|
|
770
476
|
*
|
|
771
|
-
* I-3:config 与 anchors
|
|
772
|
-
*
|
|
773
|
-
* loadAnchorsFile 损坏时裸抛 LegacyAnchorsError 穿透成 unhandled rejection。
|
|
774
|
-
* I-2:archived=true 时用调用方透传的 archiveDate(archive 主流程算出的那个),
|
|
775
|
-
* 不再 new Date() —— 避免 archive 跨午夜完成时目录名偏一天致静默 skip。
|
|
477
|
+
* I-3:config 与 anchors 由调用方已加载并传入,避免 double-IO。
|
|
478
|
+
* I-2:archived=true 时用调用方透传的 archiveDate(防跨午夜偏)。
|
|
776
479
|
*/
|
|
777
480
|
async function buildSyncCheckChangeContext(forgeRoot, changeId, opts, config, anchors) {
|
|
778
|
-
// 决定 change 目录:归档后在 archive/<archiveDate>-<id>/,归档前在 changes/<id>/
|
|
779
|
-
// archived=true 时 archiveDate 必传(由 posthook 透传 archive 主流程算出的值)
|
|
780
481
|
const changesDir = opts.archived && opts.archiveDate
|
|
781
482
|
? join(forgeRoot, 'changes', 'archive', `${opts.archiveDate}-${changeId}`)
|
|
782
483
|
: join(forgeRoot, 'changes', changeId);
|
|
783
484
|
let changeContext = '';
|
|
784
485
|
const affectedModules = [];
|
|
785
|
-
// 读 proposal.md
|
|
786
486
|
if (existsSync(join(changesDir, 'proposal.md'))) {
|
|
787
487
|
changeContext += await readFile(join(changesDir, 'proposal.md'), 'utf8');
|
|
788
488
|
}
|
|
789
|
-
// 读 specs/ 下各 .md,拼 changeContext + 收集 affectedModules
|
|
790
489
|
const specsDir = join(changesDir, 'specs');
|
|
791
490
|
if (existsSync(specsDir)) {
|
|
792
491
|
for (const f of await readdir(specsDir)) {
|
|
@@ -800,7 +499,6 @@ async function buildSyncCheckChangeContext(forgeRoot, changeId, opts, config, an
|
|
|
800
499
|
affectedModules,
|
|
801
500
|
anchors,
|
|
802
501
|
autoResolveCrossAnchor: config.legacy_bridge?.auto_resolve_cross_anchor ?? false,
|
|
803
|
-
// mtime 提供者:用于 cross-anchor 决策
|
|
804
502
|
mtimeOf: (p) => {
|
|
805
503
|
try {
|
|
806
504
|
return Math.floor(statSync(p).mtimeMs / 1000);
|
|
@@ -820,19 +518,13 @@ async function buildSyncCheckChangeContext(forgeRoot, changeId, opts, config, an
|
|
|
820
518
|
* @param ctx 已拼好的 SyncCheckInput(可选;不传则内部自拼,供测试直接调用)
|
|
821
519
|
*/
|
|
822
520
|
export async function emitPreflightSyncCheck(forgeRoot, changeId, ctx) {
|
|
823
|
-
// ctx 未传(测试直接调用)→ 自行加载 config + anchors 拼 context
|
|
824
521
|
const syncCtx = ctx ?? (await buildSyncCheckContextStandalone(forgeRoot, changeId, false));
|
|
825
522
|
if (!syncCtx)
|
|
826
|
-
return { kind: 'skip' };
|
|
827
|
-
// 构建 LlmTask(确定性 prep:findAffectedAnchors + redact)
|
|
523
|
+
return { kind: 'skip' };
|
|
828
524
|
const task = await buildSyncCheckTask(syncCtx, async (p) => (await readAnchorFile(p)).text);
|
|
829
|
-
// 无受影响 anchor 或全部读取失败 → graceful skip
|
|
830
525
|
if (!task)
|
|
831
526
|
return { kind: 'skip' };
|
|
832
|
-
// emit sync-check manifest 到 forge/.cache/legacy-bridge-task-sync-check.json
|
|
833
|
-
// meta 键名与 Task 6.2 runSyncCheckCommand emit 保持一致:changeId(驼峰)
|
|
834
527
|
const manifest = await new AgentHandoffRunner(forgeRoot, FORGE_VERSION).emit('sync-check', 1, [task], { gate_context: 'archive-preflight', changeId });
|
|
835
|
-
// 写暂停态文件:archive-pause-<changeId>.json
|
|
836
528
|
await mkdir(join(forgeRoot, '.cache'), { recursive: true });
|
|
837
529
|
await writeFile(join(forgeRoot, '.cache', `archive-pause-${changeId}.json`), JSON.stringify({
|
|
838
530
|
changeId,
|
|
@@ -842,29 +534,21 @@ export async function emitPreflightSyncCheck(forgeRoot, changeId, ctx) {
|
|
|
842
534
|
return { kind: 'halted-for-fulfillment' };
|
|
843
535
|
}
|
|
844
536
|
/**
|
|
845
|
-
* agent 模式:enforce_sync=false 时 post-archive emit sync-check manifest(
|
|
537
|
+
* agent 模式:enforce_sync=false 时 post-archive emit sync-check manifest(非阻塞)。
|
|
846
538
|
*
|
|
847
539
|
* archive 已完成;fulfill 后跑 forge legacy-bridge sync-check --apply 出报告。
|
|
848
|
-
* 调用前 gate 检查由 runArchivePostHook 负责。
|
|
849
|
-
*
|
|
850
|
-
* @param ctx 已拼好的 SyncCheckInput(可选;不传则内部自拼,供测试直接调用)
|
|
851
540
|
*/
|
|
852
541
|
export async function emitPostHookSyncCheck(forgeRoot, changeId, ctx) {
|
|
853
|
-
// ctx 未传(测试直接调用)→ 自行加载 config + anchors 拼 context(archived=true)
|
|
854
542
|
const syncCtx = ctx ?? (await buildSyncCheckContextStandalone(forgeRoot, changeId, true));
|
|
855
543
|
if (!syncCtx)
|
|
856
|
-
return { kind: 'skip' };
|
|
857
|
-
// 构建 LlmTask
|
|
544
|
+
return { kind: 'skip' };
|
|
858
545
|
const task = await buildSyncCheckTask(syncCtx, async (p) => (await readAnchorFile(p)).text);
|
|
859
|
-
// 无受影响 anchor → graceful skip
|
|
860
546
|
if (!task)
|
|
861
547
|
return { kind: 'skip' };
|
|
862
|
-
// emit sync-check manifest(meta 键名与 Task 6.2 一致:changeId 驼峰)
|
|
863
548
|
await new AgentHandoffRunner(forgeRoot, FORGE_VERSION).emit('sync-check', 1, [task], {
|
|
864
549
|
gate_context: 'archive-posthook',
|
|
865
550
|
changeId,
|
|
866
551
|
});
|
|
867
|
-
// 非阻塞:不写暂停态文件(archive 已完成)
|
|
868
552
|
return { kind: 'emitted' };
|
|
869
553
|
}
|
|
870
554
|
/**
|
|
@@ -872,7 +556,7 @@ export async function emitPostHookSyncCheck(forgeRoot, changeId, ctx) {
|
|
|
872
556
|
* (不经 runArchivePreflight/runArchivePostHook gate)时,自行加载 config + anchors。
|
|
873
557
|
*
|
|
874
558
|
* archived=true 时用「今日」作 archiveDate —— 仅 standalone 测试路径,生产路径
|
|
875
|
-
* 由 runArchivePostHook 透传准确的 archiveDate
|
|
559
|
+
* 由 runArchivePostHook 透传准确的 archiveDate。
|
|
876
560
|
*
|
|
877
561
|
* 返回 null:config 缺失 / anchors 缺失 → 调用方按 skip 处理。
|
|
878
562
|
*/
|
|
@@ -887,31 +571,24 @@ async function buildSyncCheckContextStandalone(forgeRoot, changeId, archived) {
|
|
|
887
571
|
return buildSyncCheckChangeContext(forgeRoot, changeId, { archived, archiveDate: archived ? new Date().toISOString().slice(0, 10) : undefined }, config, anchors);
|
|
888
572
|
}
|
|
889
573
|
/**
|
|
890
|
-
* Task 6.4:forge archive --resume 的 gate
|
|
574
|
+
* Task 6.4:forge archive --resume 的 gate 复核(legacy 留存 — v4 archive 主流程已无 --resume,
|
|
575
|
+
* 但 tests/cli/legacy-bridge/archive-dualpath.test.ts 直接 import 测试此 gate 逻辑,需保留)。
|
|
891
576
|
*
|
|
892
577
|
* 复核两项:
|
|
893
|
-
* ① 产物绑定 — sync-state
|
|
894
|
-
*
|
|
895
|
-
* ② critical 幂等重评 — sync-state 中不得有 severity=critical + status=pending 的差异;
|
|
896
|
-
* 若有,说明 agent 未完全 resolve,不可续跑 archive。
|
|
578
|
+
* ① 产物绑定 — sync-state.produced_from 必须等于暂停态文件的 manifest_hash
|
|
579
|
+
* ② critical 幂等重评 — sync-state 中不得有 severity=critical + status=pending 的差异
|
|
897
580
|
*
|
|
898
|
-
*
|
|
899
|
-
*
|
|
900
|
-
* @returns { ok: true } 表示 gate 通过;{ ok: false, reason } 表示拒绝并附原因
|
|
581
|
+
* v4 备注:archive 命令本身不再调用此函数;legacy-bridge sync-check 命令(若有 resume 路径)
|
|
582
|
+
* 可调用此函数验证 sync-state 绑定。本函数留存为 sync-check 子系统的 building block。
|
|
901
583
|
*/
|
|
902
584
|
export async function resumeArchiveGateCheck(forgeRoot, changeId) {
|
|
903
|
-
// 读暂停态文件:forge/.cache/archive-pause-<changeId>.json
|
|
904
585
|
const pausePath = join(forgeRoot, '.cache', `archive-pause-${changeId}.json`);
|
|
905
586
|
if (!existsSync(pausePath)) {
|
|
906
587
|
return {
|
|
907
588
|
ok: false,
|
|
908
|
-
reason: `无暂停态文件 archive-pause-${changeId}.json;archive 未在 preflight 暂停,无需
|
|
589
|
+
reason: `无暂停态文件 archive-pause-${changeId}.json;archive 未在 preflight 暂停,无需 resume`,
|
|
909
590
|
};
|
|
910
591
|
}
|
|
911
|
-
// 暂停态文件格式(Task 6.3 emitPreflightSyncCheck 写入):{ changeId, paused_step, manifest_hash }
|
|
912
|
-
// I-1:parse 包 try/catch —— 文件截断/手动误编辑致 JSON 损坏时转 business-fail,
|
|
913
|
-
// 用户看得出是哪个文件坏(否则裸 SyntaxError 被 action 外层兜底当未知错误)。
|
|
914
|
-
// readFile 的 ENOENT 竞态(existsSync 后文件被删)也顺带被这层 catch 兜住,可接受。
|
|
915
592
|
let pause;
|
|
916
593
|
try {
|
|
917
594
|
pause = JSON.parse(await readFile(pausePath, 'utf8'));
|
|
@@ -922,7 +599,6 @@ export async function resumeArchiveGateCheck(forgeRoot, changeId) {
|
|
|
922
599
|
reason: `暂停态文件 ${pausePath} 解析失败(可能损坏):${err.message}`,
|
|
923
600
|
};
|
|
924
601
|
}
|
|
925
|
-
// 读 sync-state YAML:forge/legacy-sync-state/<changeId>.yaml
|
|
926
602
|
const statePath = join(forgeRoot, 'legacy-sync-state', `${changeId}.yaml`);
|
|
927
603
|
if (!existsSync(statePath)) {
|
|
928
604
|
return {
|
|
@@ -930,7 +606,6 @@ export async function resumeArchiveGateCheck(forgeRoot, changeId) {
|
|
|
930
606
|
reason: `sync-state 缺失(forge/legacy-sync-state/${changeId}.yaml);请先让 agent fulfill manifest 后跑 forge legacy-bridge sync-check --apply --change-id ${changeId}`,
|
|
931
607
|
};
|
|
932
608
|
}
|
|
933
|
-
// I-1:同样包 try/catch —— sync-state YAML 损坏时转 business-fail
|
|
934
609
|
let state;
|
|
935
610
|
try {
|
|
936
611
|
state = parseYaml(await readFile(statePath, 'utf8'));
|
|
@@ -941,15 +616,12 @@ export async function resumeArchiveGateCheck(forgeRoot, changeId) {
|
|
|
941
616
|
reason: `sync-state 文件 ${statePath} 解析失败(可能损坏):${err.message}`,
|
|
942
617
|
};
|
|
943
618
|
}
|
|
944
|
-
// ① 产物绑定:sync-state.produced_from 必须等于暂停态的 manifest_hash
|
|
945
619
|
if (state.produced_from !== pause.manifest_hash) {
|
|
946
620
|
return {
|
|
947
621
|
ok: false,
|
|
948
622
|
reason: `produced_from(${state.produced_from ?? 'undefined'}) ≠ 暂停态 manifest_hash(${pause.manifest_hash});sync-state 非本次 --apply 产物,请重新履行 manifest 并跑 sync-check --apply`,
|
|
949
623
|
};
|
|
950
624
|
}
|
|
951
|
-
// ② critical 幂等重评:sync-state 中不得有 critical+pending 差异
|
|
952
|
-
// 注:archive.ts Task 6.3 已用内联 filter 而非 hasCriticalPending import,此处保持一致
|
|
953
625
|
const criticalPending = (state.diffs ?? []).filter((d) => d.severity === 'critical' && d.status === 'pending');
|
|
954
626
|
if (criticalPending.length > 0) {
|
|
955
627
|
return {
|