agentic-flow-kit 0.8.0 → 0.9.1

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 (141) hide show
  1. package/README.md +93 -69
  2. package/modules/gates/README.md +24 -0
  3. package/modules/gates/generated-readonly/README.md +13 -0
  4. package/modules/gates/generated-readonly/check-generated.mjs +61 -0
  5. package/modules/gates/generated-readonly/local-pre-commit.line +1 -0
  6. package/modules/gates/node-layer/README.md +9 -0
  7. package/modules/gates/node-layer/check-node-layer.mjs +29 -0
  8. package/modules/gates/node-layer/local-pre-commit.line +1 -0
  9. package/modules/gates/py-import/README.md +9 -0
  10. package/modules/gates/py-import/check-py-import.mjs +30 -0
  11. package/modules/gates/py-import/local-pre-commit.line +1 -0
  12. package/modules/hosts/claude/agents/implementer.md +52 -0
  13. package/modules/hosts/claude/agents/independent-reviewer.md +49 -0
  14. package/modules/hosts/claude/agents/ui-verifier.md +48 -0
  15. package/modules/hosts/claude/commands/wf-build.md +79 -0
  16. package/modules/hosts/claude/commands/wf-deploy.md +57 -0
  17. package/modules/hosts/claude/commands/wf-design.md +42 -0
  18. package/modules/hosts/claude/commands/wf-gate-checklist.md +77 -0
  19. package/modules/hosts/claude/commands/wf-maintain.md +76 -0
  20. package/modules/hosts/claude/commands/wf-new-task.md +65 -0
  21. package/modules/hosts/claude/commands/wf-plan.md +41 -0
  22. package/modules/hosts/claude/commands/wf-review.md +47 -0
  23. package/modules/hosts/claude/commands/wf-source-sync-check.md +89 -0
  24. package/modules/hosts/claude/commands/wf-sync-hosts.md +66 -0
  25. package/modules/hosts/claude/commands/wf-test.md +66 -0
  26. package/modules/hosts/codex/agents/implementer.md +52 -0
  27. package/modules/hosts/codex/agents/independent-reviewer.md +49 -0
  28. package/modules/hosts/codex/agents/ui-verifier.md +48 -0
  29. package/modules/hosts/codex/commands/wf-build.md +79 -0
  30. package/modules/hosts/codex/commands/wf-deploy.md +57 -0
  31. package/modules/hosts/codex/commands/wf-design.md +42 -0
  32. package/modules/hosts/codex/commands/wf-gate-checklist.md +77 -0
  33. package/modules/hosts/codex/commands/wf-maintain.md +76 -0
  34. package/modules/hosts/codex/commands/wf-new-task.md +65 -0
  35. package/modules/hosts/codex/commands/wf-plan.md +41 -0
  36. package/modules/hosts/codex/commands/wf-review.md +47 -0
  37. package/modules/hosts/codex/commands/wf-source-sync-check.md +89 -0
  38. package/modules/hosts/codex/commands/wf-sync-hosts.md +66 -0
  39. package/modules/hosts/codex/commands/wf-test.md +66 -0
  40. package/modules/hosts/codex/skills/flow-kit/SKILL.md +14 -0
  41. package/modules/hosts/cursor/agents/implementer.md +52 -0
  42. package/modules/hosts/cursor/agents/independent-reviewer.md +49 -0
  43. package/modules/hosts/cursor/agents/ui-verifier.md +48 -0
  44. package/modules/hosts/cursor/commands/wf-build.md +79 -0
  45. package/modules/hosts/cursor/commands/wf-deploy.md +57 -0
  46. package/modules/hosts/cursor/commands/wf-design.md +42 -0
  47. package/modules/hosts/cursor/commands/wf-gate-checklist.md +77 -0
  48. package/modules/hosts/cursor/commands/wf-maintain.md +76 -0
  49. package/modules/hosts/cursor/commands/wf-new-task.md +65 -0
  50. package/modules/hosts/cursor/commands/wf-plan.md +41 -0
  51. package/modules/hosts/cursor/commands/wf-review.md +47 -0
  52. package/modules/hosts/cursor/commands/wf-source-sync-check.md +89 -0
  53. package/modules/hosts/cursor/commands/wf-sync-hosts.md +66 -0
  54. package/modules/hosts/cursor/commands/wf-test.md +66 -0
  55. package/modules/hosts/omp/agents/implementer.md +1 -1
  56. package/modules/hosts/omp/agents/independent-reviewer.md +1 -1
  57. package/modules/hosts/omp/agents/ui-verifier.md +1 -1
  58. package/modules/hosts/opencode/agents/implementer.md +1 -1
  59. package/modules/hosts/opencode/agents/independent-reviewer.md +1 -1
  60. package/modules/hosts/opencode/agents/ui-verifier.md +1 -1
  61. package/modules/hosts/opencode/commands/wf-build.md +3 -4
  62. package/modules/hosts/opencode/commands/wf-design.md +2 -2
  63. package/modules/hosts/opencode/commands/wf-maintain.md +1 -1
  64. package/modules/hosts/opencode/commands/wf-plan.md +3 -3
  65. package/modules/hosts/opencode/commands/wf-review.md +1 -1
  66. package/modules/hosts/opencode/commands/wf-source-sync-check.md +1 -1
  67. package/modules/hosts/opencode/commands/wf-test.md +3 -1
  68. package/modules/hosts/trae/agents/implementer.md +1 -1
  69. package/modules/hosts/trae/agents/independent-reviewer.md +1 -1
  70. package/modules/hosts/trae/agents/ui-verifier.md +1 -1
  71. package/modules/hosts/trae/commands/wf-build.md +3 -4
  72. package/modules/hosts/trae/commands/wf-design.md +2 -2
  73. package/modules/hosts/trae/commands/wf-maintain.md +1 -1
  74. package/modules/hosts/trae/commands/wf-plan.md +3 -3
  75. package/modules/hosts/trae/commands/wf-review.md +1 -1
  76. package/modules/hosts/trae/commands/wf-source-sync-check.md +1 -1
  77. package/modules/hosts/trae/commands/wf-test.md +3 -1
  78. package/modules/hosts/zcode/agents/implementer.md +1 -1
  79. package/modules/hosts/zcode/agents/independent-reviewer.md +1 -1
  80. package/modules/hosts/zcode/agents/ui-verifier.md +1 -1
  81. package/package.json +45 -45
  82. package/src/cli.mjs +16 -4
  83. package/src/doctor.mjs +49 -1
  84. package/src/fresh-init.test.mjs +36 -0
  85. package/src/gates.test.mjs +126 -0
  86. package/src/init.mjs +8 -1
  87. package/src/init.test.mjs +7 -1
  88. package/src/profiles.mjs +10 -4
  89. package/src/run-tests.mjs +3 -0
  90. package/src/stack-profile.test.mjs +89 -0
  91. package/src/sync-hosts.test.mjs +26 -6
  92. package/templates/AGENTS.md +4 -4
  93. package/templates/_agents/board/index.html +3 -3
  94. package/templates/_agents/commands/build.md +4 -5
  95. package/templates/_agents/commands/design.md +4 -4
  96. package/templates/_agents/commands/maintain.md +1 -1
  97. package/templates/_agents/commands/new-task.md +13 -2
  98. package/templates/_agents/commands/plan.md +4 -4
  99. package/templates/_agents/commands/review.md +1 -1
  100. package/templates/_agents/commands/source-sync-check.md +1 -1
  101. package/templates/_agents/commands/test.md +4 -2
  102. package/templates/_agents/hooks/commit-check.cjs +45 -24
  103. package/templates/_agents/lane-surfaces.txt +14 -0
  104. package/templates/_agents/roles/implementer.md +1 -1
  105. package/templates/_agents/roles/independent-reviewer.md +1 -1
  106. package/templates/_agents/roles/ui-verifier.md +1 -1
  107. package/templates/_agents/scripts/check-lane-surface.mjs +164 -0
  108. package/templates/_agents/scripts/check-lane-surface.test.mjs +200 -0
  109. package/templates/_agents/scripts/check-loop-rev.test.mjs +337 -0
  110. package/templates/_agents/scripts/check-loop.mjs +460 -268
  111. package/templates/_agents/scripts/check-loop.test.mjs +677 -17
  112. package/templates/_agents/scripts/check-metric-claims.mjs +275 -0
  113. package/templates/_agents/scripts/commit-check-trigger.test.mjs +64 -1
  114. package/templates/_agents/scripts/confirm-doc.mjs +159 -15
  115. package/templates/_agents/scripts/confirm-doc.test.mjs +362 -10
  116. package/templates/_agents/scripts/doctor.test.mjs +102 -1
  117. package/templates/_agents/scripts/fill-intent.mjs +8 -7
  118. package/templates/_agents/scripts/fill-intent.test.mjs +12 -3
  119. package/templates/_agents/scripts/fill-plan.mjs +27 -11
  120. package/templates/_agents/scripts/fill-plan.test.mjs +148 -8
  121. package/templates/_agents/scripts/fill-spec.mjs +18 -3
  122. package/templates/_agents/scripts/fill-spec.test.mjs +198 -1
  123. package/templates/_agents/scripts/gate-checklist.mjs +4 -1
  124. package/templates/_agents/scripts/gen-workflow-index.mjs +6 -2
  125. package/templates/_agents/scripts/gen-workflow-index.test.mjs +12 -0
  126. package/templates/_agents/scripts/policy.mjs +46 -0
  127. package/templates/_agents/scripts/pre-push.test.mjs +82 -0
  128. package/templates/_agents/scripts/solidify-task.mjs +107 -0
  129. package/templates/_agents/scripts/solidify-task.test.mjs +126 -0
  130. package/templates/_agents/scripts/stage-gates.mjs +256 -0
  131. package/templates/_agents/scripts/trust-mode.mjs +67 -0
  132. package/templates/_agents/scripts/wf-journal.mjs +71 -2
  133. package/templates/_agents/scripts/wf-journal.test.mjs +66 -0
  134. package/templates/_agents/scripts/workflow-board-server.mjs +18 -15
  135. package/templates/_agents/scripts/workflow-board-server.test.mjs +30 -1
  136. package/templates/_agents/workflows/pipeline-closing.md +1 -1
  137. package/templates/_githooks/pre-commit +33 -2
  138. package/templates/_githooks/pre-push +58 -10
  139. package/templates/workflow/README.md +22 -3
  140. package/templates/workflow/incidents/_TEMPLATE.md +4 -0
  141. package/templates/workflow/intents/_TEMPLATE.md +1 -0
@@ -0,0 +1,46 @@
1
+ // policy.mjs — check-loop 豁免锚的单源(policyVersion)
2
+ // 新豁免改 POLICIES 并升版本,不在 check-loop 里再写散落日期。
3
+ // kit.json 缺 policyVersion、或版本不在表内:回退版本 1(不启用版本 2 的 approved 锚)。
4
+ // audit 不在本表:缺省视为全量检查(已装仓库与无 kit 的测试夹具);init 新装显式写 false。
5
+ import fs from 'node:fs';
6
+ import path from 'node:path';
7
+
8
+ export const POLICIES = {
9
+ 1: {
10
+ moduleSince: '2026-09-22',
11
+ check14Since: '2026-09-23',
12
+ confirmDocsEffective: '2026-09-27',
13
+ confirmIncidentsEffective: '2026-09-28',
14
+ bindingTs: '2026-09-28',
15
+ },
16
+ // 版本 2 = 版本 1 的五个日期 + 检查 17 的 approved 锚 + 检查 19 的 stageGateSince + check14Since 后移至确认门实际上线日(2026-09-23 → 2026-09-26)。
17
+ // 漏抄日期键会改动其他检查的生效日。
18
+ 2: {
19
+ moduleSince: '2026-09-22',
20
+ check14Since: '2026-09-26',
21
+ confirmDocsEffective: '2026-09-27',
22
+ confirmIncidentsEffective: '2026-09-28',
23
+ bindingTs: '2026-09-28',
24
+ check17UnclosedAfter: '0.8.0',
25
+ stageGateSince: '2026-09-29', // 检查 19 逐阶段审计的生效日(2026-09-30 stage-gate-machine);锚 = 台账 ts 的 UTC 日期(复核 P2-3:本批 ts 2026-09-29T17:33Z 落在受审范围,顺序合规故静默);v1 无此键 → 该检查整体跳过
26
+ },
27
+ };
28
+
29
+ export function loadKitPolicy(root) {
30
+ let raw = null;
31
+ try {
32
+ raw = JSON.parse(fs.readFileSync(path.join(root, '.agents', 'kit.json'), 'utf8'));
33
+ } catch { /* 无 kit 或不可解析 */ }
34
+ const requested = raw && Number.isInteger(raw.policyVersion) ? raw.policyVersion : 1;
35
+ const policyVersion = Object.prototype.hasOwnProperty.call(POLICIES, requested) ? requested : 1;
36
+ return {
37
+ ...POLICIES[policyVersion],
38
+ policyVersion,
39
+ requestedVersion: requested,
40
+ audit: raw && typeof raw.audit === 'boolean' ? raw.audit : undefined,
41
+ };
42
+ }
43
+
44
+ export function auditEnabled(policy) {
45
+ return policy.audit !== false;
46
+ }
@@ -0,0 +1,82 @@
1
+ #!/usr/bin/env node
2
+ // pre-push.test.mjs — 加固门目标分支判定 fixture 套件(2026-10-01 v09-review-defects)
3
+ // 真仓端到端:临时仓库挂载真实 pre-push,.agents/scripts/check-loop.sh 以桩脚本记录 argv。
4
+ // 断言三态 refspec:
5
+ // main→main → argv 含 --hardening(加固门)
6
+ // experiment/x→main → argv 含 --hardening(修复回归:直推漏口)
7
+ // experiment/y→experiment/y → argv 不含 --hardening(advisory)
8
+ import fs from 'node:fs';
9
+ import os from 'node:os';
10
+ import path from 'node:path';
11
+ import { spawnSync } from 'node:child_process';
12
+ import { fileURLToPath } from 'node:url';
13
+
14
+ const SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url));
15
+ const PRE_PUSH = path.join(SCRIPT_DIR, '..', '..', '_githooks', 'pre-push');
16
+
17
+ let pass = 0;
18
+ let fail = 0;
19
+ const check = (desc, cond, detail = '') => {
20
+ if (cond) { pass++; console.log(`PASS ${desc}`); }
21
+ else { fail++; console.log(`FAIL ${desc}${detail ? '\n ' + detail : ''}`); }
22
+ };
23
+
24
+ const git = (cwd, args) => spawnSync('git', args, { cwd, encoding: 'utf8' });
25
+ const run = (cwd, args) => spawnSync('git', args, { cwd, encoding: 'utf8' });
26
+
27
+ const setup = () => {
28
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), 'prepush-test-'));
29
+ const remote = path.join(root, 'remote.git');
30
+ fs.mkdirSync(path.join(root, '.githooks'), { recursive: true });
31
+ fs.mkdirSync(path.join(root, '.agents', 'scripts'), { recursive: true });
32
+ // 挂载真实 pre-push(装户同款),stub 替换 check-loop 记录 argv
33
+ fs.copyFileSync(PRE_PUSH, path.join(root, '.githooks', 'pre-push'));
34
+ fs.writeFileSync(path.join(root, '.agents', 'scripts', 'check-loop.sh'), '#!/bin/sh\necho "$@" >> .stub-args\nexit 0\n');
35
+ fs.writeFileSync(path.join(root, 'a.txt'), 'a\n');
36
+ git(root, ['init', '-q']);
37
+ git(root, ['config', 'user.email', 't@t']);
38
+ git(root, ['config', 'user.name', 't']);
39
+ git(root, ['config', 'core.hooksPath', '.githooks']);
40
+ git(root, ['add', 'a.txt']);
41
+ git(root, ['commit', '-q', '-m', 'init']);
42
+ git(root, ['init', '-q', '--bare', remote]);
43
+ git(root, ['remote', 'add', 'origin', remote]);
44
+ return root;
45
+ };
46
+ const stubArgs = (root) => {
47
+ const p = path.join(root, '.stub-args');
48
+ return fs.existsSync(p) ? fs.readFileSync(p, 'utf8').split(/\n/).filter(Boolean) : [];
49
+ };
50
+
51
+ const root = setup();
52
+ const w = (f, c) => fs.writeFileSync(path.join(root, f), c);
53
+
54
+ // 1) main → main:应含 --hardening
55
+ git(root, ['checkout', '-q', '-b', 'main']);
56
+ let r = run(root, ['push', 'origin', 'HEAD:main']);
57
+ check('main→main 推送成功', r.status === 0, `${r.stdout}${r.stderr}`);
58
+ check('main→main argv 含 --hardening', (stubArgs(root)[0] || '').includes('--hardening'), stubArgs(root).join('|'));
59
+
60
+ // 2) experiment/x → main(回归漏口):本地分支在 experiment,目标 main → 必须含 --hardening
61
+ git(root, ['checkout', '-q', '-b', 'experiment/x']);
62
+ w('b.txt', 'b\n');
63
+ git(root, ['add', 'b.txt']);
64
+ git(root, ['commit', '-q', '-m', 'x']);
65
+ r = run(root, ['push', 'origin', 'HEAD:main']);
66
+ check('experiment/x→main 推送成功', r.status === 0, `${r.stdout}${r.stderr}`);
67
+ check('experiment/x→main argv 含 --hardening(直推漏口修复)', (stubArgs(root)[1] || '').includes('--hardening'), stubArgs(root).join('|'));
68
+
69
+ // 3) experiment/y → experiment/y:目标在探索泳道 → 不含 --hardening
70
+ git(root, ['fetch', '-q', 'origin']);
71
+ git(root, ['branch', 'experiment/y', 'origin/main']);
72
+ git(root, ['checkout', '-q', 'experiment/y']);
73
+ w('c.txt', 'c\n');
74
+ git(root, ['add', 'c.txt']);
75
+ git(root, ['commit', '-q', '-m', 'y']);
76
+ r = run(root, ['push', 'origin', 'HEAD:experiment/y']);
77
+ check('experiment→experiment 推送成功', r.status === 0, `${r.stdout}${r.stderr}`);
78
+ const line3 = stubArgs(root)[2] || '';
79
+ check('experiment→experiment argv 不含 --hardening', !line3.includes('--hardening'), stubArgs(root).join('|'));
80
+
81
+ console.log(`\n${fail ? `❌ ${fail} failed` : '✅ all passed'}(${pass} passed)`);
82
+ process.exit(fail ? 1 : 0);
@@ -0,0 +1,107 @@
1
+ #!/usr/bin/env node
2
+ // solidify-task: 将 L1 液态草稿固化为正式 workflow 文档(迁移 + 可选确认 + 索引更新)
3
+ // 用法:
4
+ // node .agents/scripts/solidify-task.mjs --topic <主题> [--root <仓库根>]
5
+ // # 只迁移 + 更新索引,绝不写确认台账;末尾打印确认指引
6
+ // node .agents/scripts/solidify-task.mjs --topic <主题> --delegated "<用户原话>"
7
+ // # 迁移后,以调用方显式传入的用户原话逐份 confirm-doc --delegated(quote 原样转发)
8
+ // node .agents/scripts/solidify-task.mjs --topic <主题> --auto
9
+ // # 迁移后,逐份 confirm-doc --auto(trust-mode=Trusted 才放行;Strict 下被拒并计数失败)
10
+ // 确认门契约(incidents/2026-10-01-v09-review-defects):--delegated 的 quote 必须来自用户在对话内的
11
+ // 明确原话,由调用方显式传入——本脚本永不编造用户话术。无确认来源时只迁移不落账,末尾打印指引。
12
+ import fs from 'node:fs';
13
+ import path from 'node:path';
14
+ import { spawnSync } from 'node:child_process';
15
+ import { fileURLToPath } from 'node:url';
16
+
17
+ const SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url));
18
+
19
+ function fail(msg) {
20
+ console.error(`❌ ${msg}`);
21
+ process.exit(1);
22
+ }
23
+
24
+ function parseArgs() {
25
+ const topicIdx = process.argv.indexOf('--topic');
26
+ const topic = topicIdx !== -1 ? process.argv[topicIdx + 1] : '';
27
+ if (!topic) fail('用法: node .agents/scripts/solidify-task.mjs --topic "主题" [--root <仓库根>] [--delegated "<用户原话>" | --auto]');
28
+ let root = process.cwd();
29
+ const rootIdx = process.argv.indexOf('--root');
30
+ if (rootIdx !== -1 && process.argv[rootIdx + 1]) root = process.argv[rootIdx + 1];
31
+ const delegatedIdx = process.argv.indexOf('--delegated');
32
+ const delegated = delegatedIdx !== -1 ? String(process.argv[delegatedIdx + 1] ?? '') : '';
33
+ const auto = process.argv.includes('--auto');
34
+ if (delegated && auto) fail('--delegated 与 --auto 互斥,只能选其一');
35
+ return { topic, root, delegated, auto };
36
+ }
37
+
38
+ const classify = (content) => {
39
+ if (content.includes('# INTENT')) return 'intents';
40
+ if (content.includes('# PLAN')) return 'plans';
41
+ if (content.includes('# SPEC')) return 'specs';
42
+ if (content.includes('# INCIDENT')) return 'incidents';
43
+ return null;
44
+ };
45
+
46
+ function run() {
47
+ const { topic, root, delegated, auto } = parseArgs();
48
+ const draftsDir = path.join(root, '.zcode', 'drafts');
49
+ if (!fs.existsSync(draftsDir)) fail(`草稿目录 ${draftsDir} 不存在`);
50
+ const files = fs.readdirSync(draftsDir).filter((f) => f.includes(topic) && f.endsWith('.md'));
51
+ if (!files.length) fail(`在 ${draftsDir} 中未找到主题为 ${topic} 的草稿文件`);
52
+
53
+ const today = new Date().toISOString().slice(0, 10);
54
+ const solidified = [];
55
+ for (const file of files) {
56
+ const content = fs.readFileSync(path.join(draftsDir, file), 'utf8');
57
+ const targetDir = classify(content);
58
+ if (!targetDir) continue;
59
+ // 统一强制 YYYY-MM-DD- 前缀;已有前缀的文件名原样保留
60
+ const finalName = /^\d{4}-\d{2}-\d{2}-/.test(file) ? file : `${today}-${file}`;
61
+ const targetPath = path.join(root, 'workflow', targetDir, finalName);
62
+ fs.mkdirSync(path.dirname(targetPath), { recursive: true });
63
+ fs.copyFileSync(path.join(draftsDir, file), targetPath);
64
+ solidified.push({ rel: `workflow/${targetDir}/${finalName}`, abs: targetPath, type: targetDir });
65
+ console.log(` 📦 迁移: ${file} -> ${targetDir}/${finalName}`);
66
+ }
67
+ if (!solidified.length) fail(`草稿中未找到可识别的 INTENT/PLAN/SPEC/INCIDENT 文档(主题 ${topic})`);
68
+
69
+ // 确认:仅当调用方显式提供确认来源时执行;否则只迁移,绝不落账。
70
+ let failed = 0;
71
+ if (delegated || auto) {
72
+ console.log('🛡️ 执行逐份确认...');
73
+ for (const item of solidified) {
74
+ const args = [path.join(SCRIPT_DIR, 'confirm-doc.mjs'), item.rel, '--root', root];
75
+ if (delegated) args.push('--delegated', delegated);
76
+ else args.push('--auto');
77
+ const res = spawnSync('node', args, { cwd: root, encoding: 'utf8' });
78
+ if (res.status !== 0) {
79
+ failed++;
80
+ console.error(` ❌ 确认失败 [${item.rel}]: ${(res.stderr || '').trim()}`);
81
+ } else {
82
+ console.log(` ✅ 已确认: ${item.rel}`);
83
+ }
84
+ }
85
+ console.log(`确认汇总:${solidified.length - failed}/${solidified.length} 成功${failed ? `,${failed} 失败` : ''}`);
86
+ } else {
87
+ console.log('⏭️ 未提供确认来源,仅迁移不落账。');
88
+ for (const item of solidified) {
89
+ console.log(` → 确认指引: node .agents/scripts/confirm-doc.mjs ${item.rel}(TTY 键入「可以」)`);
90
+ console.log(` 或(用户对话内明确确认后): node .agents/scripts/confirm-doc.mjs ${item.rel} --delegated "<用户原话>"`);
91
+ }
92
+ }
93
+
94
+ // 更新索引;失败同样传播退出码。
95
+ console.log('📖 更新工作流索引...');
96
+ const idx = spawnSync('node', [path.join(SCRIPT_DIR, 'gen-workflow-index.mjs')], { cwd: root, stdio: 'inherit' });
97
+ if (idx.status !== 0) {
98
+ console.error(' ❌ 索引更新失败');
99
+ process.exit(1);
100
+ }
101
+
102
+ if (failed) process.exit(1);
103
+ console.log(`\n✨ 任务 ${topic} 已成功固化!`);
104
+ console.log(`👉 建议执行: git add workflow/ .agents/confirmations.jsonl && git commit -m "docs(workflow): solidify L1 task ${topic}"`);
105
+ }
106
+
107
+ run();
@@ -0,0 +1,126 @@
1
+ #!/usr/bin/env node
2
+ // solidify-task.test.mjs — 快车道固化脚本 fixture 套件(2026-10-01 v09-review-defects)
3
+ // 验证确认门契约:无确认来源只迁移不落账;--delegated quote 原样转发;--auto Strict 下拒绝且失败退出;
4
+ // 互斥/缺草稿/非文档等边界。gen-workflow-index 无 --root 参数,spawn 时以 fixture 根为 cwd。
5
+ import fs from 'node:fs';
6
+ import os from 'node:os';
7
+ import path from 'node:path';
8
+ import { spawnSync } from 'node:child_process';
9
+ import { fileURLToPath } from 'node:url';
10
+
11
+ const SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url));
12
+ const SCRIPT = path.join(SCRIPT_DIR, 'solidify-task.mjs');
13
+
14
+ let pass = 0;
15
+ let fail = 0;
16
+ const check = (desc, cond, detail = '') => {
17
+ if (cond) { pass++; console.log(`PASS ${desc}`); }
18
+ else { fail++; console.log(`FAIL ${desc}${detail ? '\n ' + detail : ''}`); }
19
+ };
20
+
21
+ const ENUMS_FIXTURE = [
22
+ 'doc.status.all=draft approved done superseded cancelled',
23
+ 'doc.status.confirmed=approved done superseded cancelled',
24
+ 'doc.status.active=draft approved',
25
+ 'doc.status.terminal=done superseded cancelled',
26
+ 'doc.status.abandoned=superseded cancelled',
27
+ 'incident.status.all=open fixed closed',
28
+ 'incident.status.active=open',
29
+ 'level.all=L0 L1 L2 L3',
30
+ ].join('\n') + '\n';
31
+
32
+ const mkfix = () => {
33
+ const d = fs.mkdtempSync(path.join(os.tmpdir(), 'solidify-test-'));
34
+ for (const s of ['intents', 'plans', 'specs', 'incidents']) {
35
+ fs.mkdirSync(path.join(d, 'workflow', s), { recursive: true });
36
+ }
37
+ fs.mkdirSync(path.join(d, '.agents'), { recursive: true });
38
+ fs.mkdirSync(path.join(d, '.zcode', 'drafts'), { recursive: true });
39
+ fs.writeFileSync(path.join(d, '.agents', 'workflow-enums.txt'), ENUMS_FIXTURE);
40
+ fs.writeFileSync(path.join(d, '.agents', 'workflow-modules.txt'), '# modules\npipeline\nui\n');
41
+ return d;
42
+ };
43
+ const DRAFT = (name, eol = '\n') =>
44
+ `---${eol}状态: draft${eol}级别: L1${eol}日期: 2026-10-01${eol}模块: pipeline${eol}---${eol}# INTENT — ${name}${eol}`;
45
+ const run = (root, args) => {
46
+ const r = spawnSync(process.execPath, [SCRIPT, ...args], { cwd: root, encoding: 'utf8' });
47
+ return { code: r.status, out: `${r.stdout || ''}${r.stderr || ''}` };
48
+ };
49
+ const ledgerLines = (root) => {
50
+ const p = path.join(root, '.agents', 'confirmations.jsonl');
51
+ return fs.existsSync(p) ? fs.readFileSync(p, 'utf8').split(/\n/).filter(Boolean) : [];
52
+ };
53
+
54
+ // T1:默认只迁移不落账
55
+ {
56
+ const d = mkfix();
57
+ fs.writeFileSync(path.join(d, '.zcode', 'drafts', 'demo-task.md'), DRAFT('demo'));
58
+ const r = run(d, ['--topic', 'demo', '--root', d]);
59
+ check('T1 默认迁移成功 exit 0', r.code === 0, r.out);
60
+ check('T1 草稿已迁移到 intents 且日期前缀正确',
61
+ fs.existsSync(path.join(d, 'workflow', 'intents', '2026-10-01-demo-task.md')), '');
62
+ check('T1 零台账新增(无确认来源绝不落账)', ledgerLines(d).length === 0, JSON.stringify(ledgerLines(d)));
63
+ check('T1 输出含确认指引', r.out.includes('确认指引') && r.out.includes('--delegated'), r.out);
64
+ }
65
+ // T2:--delegated 原话原样转发
66
+ {
67
+ const d = mkfix();
68
+ fs.writeFileSync(path.join(d, '.zcode', 'drafts', 'demo-task.md'), DRAFT('demo'));
69
+ const r = run(d, ['--topic', 'demo', '--root', d, '--delegated', '固化了可以']);
70
+ check('T2 --delegated 流程 exit 0', r.code === 0, r.out);
71
+ const lines = ledgerLines(d);
72
+ check('T2 台账落 1 行', lines.length === 1, JSON.stringify(lines));
73
+ check('T2 quote 原样(脚本不编造话术)',
74
+ lines[0].includes('"quote":"固化了可以"') && lines[0].includes('chat-delegated'), lines[0]);
75
+ check('T2 目标文档状态已 approved',
76
+ fs.readFileSync(path.join(d, 'workflow', 'intents', '2026-10-01-demo-task.md'), 'utf8').includes('状态: approved'), '');
77
+ }
78
+ // T3:--auto 在 Strict(缺 trust-mode.json)下被拒 → 失败计数并 exit 1
79
+ {
80
+ const d = mkfix();
81
+ fs.writeFileSync(path.join(d, '.zcode', 'drafts', 'demo-task.md'), DRAFT('demo'));
82
+ const r = run(d, ['--topic', 'demo', '--root', d, '--auto']);
83
+ check('T3 --auto 在缺省 Strict 下 exit 1(不吞失败)', r.code === 1, r.out);
84
+ check('T3 输出含失败汇总', r.out.includes('确认汇总') && r.out.includes('失败'), r.out);
85
+ check('T3 台账零行(拒绝不落账)', ledgerLines(d).length === 0, JSON.stringify(ledgerLines(d)));
86
+ }
87
+ // T4:缺草稿 → exit 1
88
+ {
89
+ const d = mkfix();
90
+ fs.writeFileSync(path.join(d, '.zcode', 'drafts', 'other.md'), DRAFT('other'));
91
+ const r = run(d, ['--topic', 'nope', '--root', d]);
92
+ check('T4 缺主题草稿 exit 1 且带指引', r.code === 1 && r.out.includes('未找到主题'), r.out);
93
+ }
94
+ // T5:草稿非可识别文档 → exit 1
95
+ {
96
+ const d = mkfix();
97
+ fs.writeFileSync(path.join(d, '.zcode', 'drafts', 'demo-note.md'), '# 随手笔记\nnot a workflow doc\n');
98
+ const r = run(d, ['--topic', 'demo', '--root', d]);
99
+ check('T5 无可识别文档 exit 1', r.code === 1 && r.out.includes('未找到可识别'), r.out);
100
+ }
101
+ // T6:--delegated 与 --auto 互斥 → exit 1
102
+ {
103
+ const d = mkfix();
104
+ fs.writeFileSync(path.join(d, '.zcode', 'drafts', 'demo-task.md'), DRAFT('demo'));
105
+ const r = run(d, ['--topic', 'demo', '--root', d, '--delegated', 'x', '--auto']);
106
+ check('T6 互斥 exit 1', r.code === 1 && r.out.includes('互斥'), r.out);
107
+ }
108
+ // T7:失败路径退出码传播(确认失败后 exit 1,不吞)
109
+ {
110
+ const d = mkfix();
111
+ fs.writeFileSync(path.join(d, '.zcode', 'drafts', 'demo-task.md'), DRAFT('demo'));
112
+ // 让确认失败:用 --auto 触发 Strict 拒绝(缺 trust-mode.json 即 Strict)。
113
+ const r = run(d, ['--topic', 'demo', '--root', d, '--auto']);
114
+ check('T7 失败后 exit 1 且索引仍生成', r.code === 1 && fs.existsSync(path.join(d, 'workflow', 'INDEX.md')), r.out);
115
+ }
116
+ // T8:索引更新失败传播退出码(独立于确认失败路径——删除 workflow/plans 目录使 gen-workflow-index 失败)
117
+ {
118
+ const d = mkfix();
119
+ fs.writeFileSync(path.join(d, '.zcode', 'drafts', 'demo-task.md'), DRAFT('demo'));
120
+ fs.rmSync(path.join(d, 'workflow', 'plans'), { recursive: true });
121
+ const r = run(d, ['--topic', 'demo', '--root', d]);
122
+ check('T8 索引更新失败 exit 1 且输出指明', r.code === 1 && r.out.includes('索引更新失败'), r.out);
123
+ }
124
+
125
+ console.log(`\n${fail ? `❌ ${fail} failed` : '✅ all passed'}(${pass} passed)`);
126
+ process.exit(fail ? 1 : 0);
@@ -0,0 +1,256 @@
1
+ // stage-gates.mjs — 逐阶段前置判定的单源(fill-spec / fill-plan / confirm-doc 共享;2026-09-30 stage-gate-machine)
2
+ // 口径(与 check-loop 检查 19 内联实现互引——改一处须跟另一处):
3
+ // 入口「已确认」= 文件存在且 frontmatter 状态 ∈ {approved, done},且【机器事实源】满足其一:
4
+ // ① confirmations.jsonl 台账存在该 doc 的 stage ∈ {approved, done} 行(经 confirm-doc 才产生,防手改状态冒充);
5
+ // ② 存量口径:文档日期早于 policy.confirmDocsEffective,或行首含「流程: legacy」。
6
+ // incident 入口 = 文件存在 + 「时间线」小节内、行首列表条目含「用户确认」(原 maintain.md §3 约定;
7
+ // 2026-09-30 复核 P2-1 收窄为小节内行首条目;第三轮复核 P1-1 起接受日期前缀形态,与模板样例一致);
8
+ // 状态 ∈ {open, fixed, closed}(spec/plan 须先于 fixed 起草,故 open 即为有效态)。
9
+ // 入口「级别」须 ∈ L0-L3;缺失即拒(fail-closed)——**但库存量形态短路**(`流程: legacy` 或 日期早于
10
+ // 确认门生效日):跳过级别校验与 spec 档(2026-09-30 增量复核 N1——存量最可能缺级别,硬拒会锁死
11
+ // spec 起草与确认)。
12
+ // 注意:**库存量信号本身即等于「入口已确认」**(judgeDoc:`台账行 || 存量口径`,与检查 15 / 检查 1 /
13
+ // 配对门的既有口径一致——早于确认门生效日的自报日期即存量豁免,不要求另有台账行);此处「短路」
14
+ // 只额外豁免**级别前置与 spec 档**,不是对台账要求的二次豁免(P2-1:原注释把二者写反,已订正)。
15
+ // plan 门追加:入口级别(取入口 frontmatter 级别,不取工具 --level)为 L2/L3 时,同名 spec 须已确认(同口径)。
16
+ // 探索泳道豁免(2026-09-30 hybrid-governance-explore-hardening):同名 intent 仍为 draft 且风险泳道为
17
+ // L0/L1(级别优先;级别缺失或非法时回落 risk_level——fill-intent 双写同值)→「先起草后确认」是协作道
18
+ // 语义(check-loop 检查 19 同款豁免口径),放行 spec/plan 起草与确认;L2/L3 防御道维持入口确认前置。
19
+ // incident 入口不豁免(过目留痕本就轻量)。收口由 check-loop --hardening 加固门兜底(入 main 前须
20
+ // intent approved/done + spec/plan 在场)。
21
+ // 信任边界声明(沿 2026-09-28 audit-gate-hardening 口径):本地可写台账 / 手改状态仍可伪造——本模块只把
22
+ // 「顺手绕过」抬到「主动伪造」,不宣称通道关闭;事后对质靠 git 历史与台账。
23
+ // 设计先例:fill-* 已 import 同目录 workflow-enums.mjs;check-loop 检查 19 内联同口径(门禁脚本自包含、防兄弟依赖)。
24
+ import fs from 'node:fs';
25
+ import path from 'node:path';
26
+ import { spawnSync } from 'node:child_process';
27
+ import { loadKitPolicy } from './policy.mjs';
28
+
29
+ export const ENTRY_STATUSES = ['approved', 'done'];
30
+ export const INCIDENT_STATUSES = ['open', 'fixed', 'closed'];
31
+
32
+ // readFm:frontmatter 区(首 --- 至闭合 ---)取单字段值;无 frontmatter / 无该键 → ''
33
+ export function readFm(abs, key) {
34
+ let text;
35
+ try { text = fs.readFileSync(abs, 'utf8'); } catch { return ''; }
36
+ const lines = text.replace(/\r\n/g, '\n').split('\n');
37
+ if (!/^---\s*$/.test(lines[0] || '')) return '';
38
+ for (let i = 1; i < lines.length; i++) {
39
+ if (/^---\s*$/.test(lines[i])) break;
40
+ const m = lines[i].match(new RegExp(`^${key}:\\s*(.*)$`));
41
+ if (m) return m[1].trim();
42
+ }
43
+ return '';
44
+ }
45
+
46
+ function readBody(abs) {
47
+ try { return fs.readFileSync(abs, 'utf8'); } catch { return ''; }
48
+ }
49
+
50
+ // readLedger:confirmations.jsonl 追加式台账(坏行容忍跳过;与 check-loop 检查 15 同容错口径)
51
+ export function readLedger(root) {
52
+ const p = path.join(root, '.agents', 'confirmations.jsonl');
53
+ let text = '';
54
+ try { text = fs.readFileSync(p, 'utf8'); } catch { return []; }
55
+ const out = [];
56
+ for (const line of text.split('\n')) {
57
+ if (!line.trim()) continue;
58
+ try { out.push(JSON.parse(line)); } catch { /* 坏行跳过 */ }
59
+ }
60
+ return out;
61
+ }
62
+
63
+ // judgeDoc:状态枚举 + 机器事实源判定(供入口 / spec 复用)
64
+ // legacy:库存量信号(`流程: legacy` 标记 或 日期 < confirmDocsEffective)——所有调用方与门禁其余部分
65
+ // 同口径(配对门 / 检查 1 / 检查 15 / 检查 19);2026-09-30 复核 N1:存量入口须先短路,不得被
66
+ // 「缺级别」fail-closed 硬拒(存量最可能缺级别,硬拒会锁死 spec 起草/确认,违反「存量零新增」契约)。
67
+ function judgeDoc(root, abs, rel, statuses) {
68
+ const st = readFm(abs, '状态');
69
+ if (!statuses.includes(st)) {
70
+ return { ok: false, status: st, legacy: false, reason: `状态「${st || '缺失'}」不在 ${statuses.join(' / ')}` };
71
+ }
72
+ const policy = loadKitPolicy(root);
73
+ const body = readBody(abs);
74
+ const legacyByMarker = body.split(/\r?\n/).some((l) => /^流程: legacy/.test(l));
75
+ const d = readFm(abs, '日期') || readFm(abs, '发现');
76
+ const legacyByDate = /^\d{4}-\d{2}-\d{2}$/.test(d) && d < policy.confirmDocsEffective;
77
+ const legacy = legacyByMarker || legacyByDate;
78
+ const hasRow = readLedger(root).some((e) => e && e.doc === rel && (e.stage === 'approved' || e.stage === 'done'));
79
+ if (hasRow || legacy) return { ok: true, status: st, legacy };
80
+ return { ok: false, status: st, legacy: false, reason: `无确认台账行(门禁后的入口须经 confirm-doc,且台账无 ${rel} 的 approved / done 行)` };
81
+ }
82
+
83
+ // entryStorageLegacy:入口是否属「库存量」形态(`流程: legacy` 标记 或 日期早于确认门生效日)——
84
+ // 与 judgeDoc 的存量信号同口径,供门在**级别校验**处短路(2026-09-30 增量复核 N1)。注意:本短路
85
+ // **只豁免 level 前置**(存量多为门禁前建档、无级别字段),**不豁免台账要求**——`judgeDoc` 的
86
+ // hasRow||legacy 仍照常判定(门禁后无台账行即拒,不因日期早于生效日而放行)。
87
+ export function entryStorageLegacy(root, abs) {
88
+ const body = readBody(abs);
89
+ if (body.split(/\r?\n/).some((l) => /^流程: legacy/.test(l))) return true;
90
+ const d = readFm(abs, '日期') || readFm(abs, '发现');
91
+ return /^\d{4}-\d{2}-\d{2}$/.test(d) && d < loadKitPolicy(root).confirmDocsEffective;
92
+ }
93
+
94
+ // incidentMarked:「## 时间线」小节内、行首列表条目形态的「用户确认」(维持口径;正文叙述句不算——
95
+ // 2026-09-30 复核 P2-1 实证:全仓唯一历史命中为叙述句,非过目留痕)。
96
+ // 行首可选日期前缀(`- 2026-09-30 用户确认:…`)与可选加粗(`- **用户确认**:…`)均接受——
97
+ // 与 _TEMPLATE.md 时间线样例形态保持一致(2026-09-30 第三轮复核 P1-1:样例若不可解析即误导)。
98
+ export function incidentMarked(body) {
99
+ let inTimeline = false;
100
+ for (const l of String(body).replace(/\r\n/g, '\n').split('\n')) {
101
+ if (/^##\s/.test(l)) { inTimeline = /^##\s*时间线/.test(l); continue; }
102
+ if (inTimeline && MARK_RE.test(l)) return true;
103
+ }
104
+ return false;
105
+ }
106
+
107
+ // MARK_RE:行首列表条目 + 可选日期前缀(YYYY-MM-DD 或 <日期> 占位)+ 可选加粗围栏(词前 / 词后均可)
108
+ // + 「用户确认」+ **须紧跟冒号或行尾**(词边界)。
109
+ // 形态矩阵(2026-09-30 四/五轮复核收敛,8 正例 / 5 负例由 fill-spec.test S19/S20 钉死):
110
+ // 命中:`- 用户确认:…` / `- **用户确认**:…` / `- 2026-09-30 用户确认:…` / `- <日期> **用户确认**:…` / 行尾无冒号
111
+ // 拒绝:`- 2026-09-30 用户确认了方案` / `- 用户确认书已归档` / `- 用户确认。` / `- 待用户确认:…`
112
+ // 注:词前的 `\**` 与词后的 `\**` 必须都允许——只开一侧即产生结构性假阴性(第五轮复核 P1 实证:
113
+ // 加粗围栏形态被拒,与 _TEMPLATE.md 自述契约冲突)。
114
+ export const MARK_RE = /^\s*[-*]\s*(?:\d{4}-\d{2}-\d{2}|<日期>)?\s*\**用户确认\**(?=\s*[::]|\s*$)/;
115
+
116
+ // entryConfirmed:同主题入口判定(intent 优先;否则 incidents)——返回 {ok, kind, rel, level, status, reason?}
117
+ export function entryConfirmed(root, base) {
118
+ const intentRel = `workflow/intents/${base}.md`;
119
+ const intentAbs = path.join(root, intentRel);
120
+ if (fs.existsSync(intentAbs)) {
121
+ const r = judgeDoc(root, intentAbs, intentRel, ENTRY_STATUSES);
122
+ return { ...r, kind: 'intent', rel: intentRel, level: readFm(intentAbs, '级别'), risk: readFm(intentAbs, 'risk_level'), storageLegacy: entryStorageLegacy(root, intentAbs) };
123
+ }
124
+ const incRel = `workflow/incidents/${base}.md`;
125
+ const incAbs = path.join(root, incRel);
126
+ if (fs.existsSync(incAbs)) {
127
+ const st = readFm(incAbs, '状态');
128
+ const level = readFm(incAbs, '级别');
129
+ if (INCIDENT_STATUSES.includes(st) && incidentMarked(readBody(incAbs))) {
130
+ return { ok: true, kind: 'incident', rel: incRel, level, status: st, legacy: false };
131
+ }
132
+ const why = !INCIDENT_STATUSES.includes(st) ? `状态「${st || '缺失'}」非法` : '「时间线」小节缺「用户确认」条目(maintain.md §3)';
133
+ return { ok: false, kind: 'incident', rel: incRel, level, status: st, legacy: false, reason: `同名 incident 未过目确认——${why}` };
134
+ }
135
+ return { ok: false, kind: null, rel: null, level: '', status: '', reason: `未找到同名入口(workflow/intents 与 workflow/incidents 均无 ${base}.md)` };
136
+ }
137
+
138
+ // specConfirmed:plan 门的 L2/L3 追加要求
139
+ export function specConfirmed(root, base) {
140
+ const rel = `workflow/specs/${base}.md`;
141
+ const abs = path.join(root, rel);
142
+ if (!fs.existsSync(abs)) return { ok: false, reason: `缺同名 spec(${rel})` };
143
+ return judgeDoc(root, abs, rel, ENTRY_STATUSES);
144
+ }
145
+
146
+ // draftGateFor:起草门总判(kind = 'spec' | 'plan',fill 工具与 confirm-doc 逐阶段门共用)
147
+ export function draftGateFor(kind, root, base) {
148
+ const entry = entryConfirmed(root, base);
149
+ if (!entry.ok) {
150
+ // 探索泳道豁免(2026-09-30 hybrid-governance-explore-hardening,口径见头部注释):L0/L1 draft intent
151
+ // 放行起草——「先动手后确认」;泳道取级别优先、缺失/非法回落 risk_level(与 fill-intent 双写兼容,
152
+ // 级别合法但为 L2/L3 时不回落——fail-closed 就严)。
153
+ const lane = /^L[0-3]$/.test(entry.level || '') ? entry.level : entry.risk;
154
+ if (entry.kind === 'intent' && entry.status === 'draft' && (lane === 'L0' || lane === 'L1')) {
155
+ return { ok: true, entry, laneBypass: true };
156
+ }
157
+ return {
158
+ ok: false,
159
+ reason: `同主题入口未确认——${entry.reason}`,
160
+ hint: '先走 plan.md 完成入口确认(修复类走 maintain.md,incident 草稿过目后在时间线留「用户确认」条目)',
161
+ };
162
+ }
163
+ // 库存量形态短路(2026-09-30 增量复核 N1):**只额外豁免「级别前置」与「spec 档」**——
164
+ // 库存量入口(`流程: legacy` / 日期早于确认门生效日)多为门禁前建档、常缺「级别」字段,硬拒会锁死
165
+ // spec 起草与确认。注意:库存量信号**本身即等于入口已确认**(judgeDoc 的 `台账行 || 存量口径`,
166
+ // 与检查 1 / 15 / 配对门同口径)——台账豁免发生在 judgeDoc,不在本处;本处不做二次豁免
167
+ // (第三轮复核 P2-1:原句「台账要求不豁免」把两件事写反,已订正)。
168
+ if (!entry.storageLegacy && !/^L[0-3]$/.test(entry.level)) {
169
+ return {
170
+ ok: false,
171
+ reason: `入口缺合法「级别」字段(现「${entry.level || '缺失'}」,无法判定前置深度)`,
172
+ hint: '先给入口补「级别: L0-L3」字段,再继续',
173
+ };
174
+ }
175
+ if (kind === 'plan' && !entry.storageLegacy && (entry.level === 'L2' || entry.level === 'L3')) {
176
+ const sp = specConfirmed(root, base);
177
+ if (!sp.ok) {
178
+ return { ok: false, reason: `入口级别 ${entry.level},前置 spec 未确认——${sp.reason}`, hint: '先走 design.md 完成 spec 确认' };
179
+ }
180
+ }
181
+ return { ok: true, entry };
182
+ }
183
+
184
+ // confirmGateFor:确认门总判(confirm-doc 用;docRel = workflow/specs/... 或 workflow/plans/...)
185
+ export function confirmGateFor(root, docRel) {
186
+ const base = path.basename(docRel).replace(/\.md$/, '');
187
+ const kind = docRel.startsWith('workflow/specs/') ? 'spec' : 'plan';
188
+ return draftGateFor(kind, root, base);
189
+ }
190
+
191
+ // ---- done 前置门族(2026-09-30 confirm-gate-approved-history)----
192
+ // 背景:stage-gate-machine 关单后检查 14 报「确认态缺失」×2——approved→done 期间从未提交、首次提交即
193
+ // done 态(先 done 后提交),git 历史无 approved 中间态;检查 14 是事后审计(发现即定局),本族把
194
+ // 校验收口到 confirm-doc 的「→done」跳转——「approved 态还在工作区」的最后一刻,即唯一可拦截窗口。
195
+ // 命中判据单源:approvedTraceHit 命令行与 check-loop 检查 14 逐字一致(-G '^状态:[[:space:]]*approved'),
196
+ // 本模块为唯一实现、两调用方共享(2026-09-30 复核 N3 教训:双份字面量靠注释同步必漂移)。
197
+ // 注(2026-09-30 复核 P2-2 登记):gitRun 的 GIT 解析 / maxBuffer / 失败语义与 check-loop.mjs 的
198
+ // gitOut、gitReady 与检查 14 段级两条 rev-parse 为**同口径的两份实现**(未合并——检查 14 段结构不动
199
+ // 属本单非目标)——任一侧调整(如加 --no-pager)须同步另一侧,防段级门与文档级判据分叉。
200
+ const GIT = process.platform === 'win32' ? 'git.exe' : 'git';
201
+
202
+ function gitRun(root, args) {
203
+ const r = spawnSync(GIT, args, { cwd: root, encoding: 'utf8', maxBuffer: 32 * 1024 * 1024 });
204
+ if (r.error || r.status !== 0) return null;
205
+ return r.stdout || '';
206
+ }
207
+
208
+ // gitReady:git 环境可用(在仓库内且已有 HEAD)——与检查 14 段级两条 rev-parse 同口径
209
+ export function gitReady(root) {
210
+ return gitRun(root, ['rev-parse', '--git-dir']) !== null
211
+ && gitRun(root, ['rev-parse', '-q', '--verify', 'HEAD']) !== null;
212
+ }
213
+
214
+ // approvedTraceHit:命中判定单源——git 历史是否出现过行首「状态: approved」(经提交进入历史才算,
215
+ // 当前工作区未提交的 approved 不算——这正是「留痕」语义)。rel = 仓库根相对路径。
216
+ // 返回:'' = 从未出现;<sha> = 命中;null = git 调用失败(调用方按各自容错口径处理——检查 14 不报、
217
+ // done 门跳过,均沿 fail-open 边界;诚实边界声明见 spec)。
218
+ export function approvedTraceHit(root, rel) {
219
+ const r = gitRun(root, ['log', '-1', '--format=%H', '-G', '^状态:[[:space:]]*approved', '--', rel]);
220
+ return r === null ? null : r.trim();
221
+ }
222
+
223
+ // doneGateFor:confirm-doc「→done」前置门(三件套 intent/spec/plan 均适用;incidents 的 fixed/closed 不在本门)。
224
+ // 豁免与顺序:① 行首 `流程: legacy` ② 行含「存量确认态豁免(」声明(与检查 14 同标记,显式逃生口)
225
+ // ③ 非 git 环境 ④ git 调用失败 → 均跳过(不误拦)。未豁免且未命中 → 拒绝(调用方负责非零退出 +
226
+ // 零落账 + 零写盘 + 回退指引)。
227
+ export function doneGateFor(root, abs, rel) {
228
+ const body = readBody(abs);
229
+ if (body.split(/\r?\n/).some((l) => /^流程: legacy/.test(l))) return { ok: true, skipped: 'legacy' };
230
+ if (body.split(/\r?\n/).some((l) => l.includes('存量确认态豁免('))) return { ok: true, skipped: 'declared' };
231
+ if (!gitReady(root)) return { ok: true, skipped: 'no-git' };
232
+ const hit = approvedTraceHit(root, rel);
233
+ if (hit === null) return { ok: true, skipped: 'git-error' };
234
+ if (hit === '') {
235
+ return {
236
+ ok: false,
237
+ reason: 'git 历史中从未出现行首「状态: approved」——确认态未留痕(先 done 后提交 / 两跳同批提交)',
238
+ hint: '先把该文档的 approved 态提交(git add 该文件并 commit)后再重试关单',
239
+ };
240
+ }
241
+ return { ok: true, hit };
242
+ }
243
+
244
+ // resolveWorkspace:从输出路径推导工作区根与主题名;output 路径不含 workflow 段 → null(仓外草稿,不校验)
245
+ export function resolveWorkspace(outputPath, rootOverride) {
246
+ const abs = path.resolve(outputPath);
247
+ const base = path.basename(abs).replace(/\.md$/, '');
248
+ if (rootOverride) return { root: path.resolve(rootOverride), base };
249
+ const parts = abs.split(path.sep);
250
+ for (let i = parts.length - 2; i >= 0; i--) {
251
+ if (parts[i].toLowerCase() === 'workflow') return { root: parts.slice(0, i).join(path.sep) || path.sep, base };
252
+ }
253
+ return null;
254
+ }
255
+
256
+ export default { entryConfirmed, specConfirmed, draftGateFor, confirmGateFor, doneGateFor, gitReady, approvedTraceHit, resolveWorkspace, readFm, readLedger };