agentic-flow-kit 0.7.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (148) hide show
  1. package/README.md +36 -12
  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/{build.md → wf-build.md} +3 -4
  62. package/modules/hosts/opencode/commands/{design.md → wf-design.md} +2 -2
  63. package/modules/hosts/opencode/commands/{maintain.md → wf-maintain.md} +1 -1
  64. package/modules/hosts/opencode/commands/{plan.md → wf-plan.md} +3 -3
  65. package/modules/hosts/opencode/commands/{review.md → wf-review.md} +1 -1
  66. package/modules/hosts/opencode/commands/{source-sync-check.md → wf-source-sync-check.md} +1 -1
  67. package/modules/hosts/opencode/commands/{sync-hosts.md → wf-sync-hosts.md} +1 -1
  68. package/modules/hosts/opencode/commands/{test.md → wf-test.md} +3 -1
  69. package/modules/hosts/trae/agents/implementer.md +1 -1
  70. package/modules/hosts/trae/agents/independent-reviewer.md +1 -1
  71. package/modules/hosts/trae/agents/ui-verifier.md +1 -1
  72. package/modules/hosts/trae/commands/wf-build.md +3 -4
  73. package/modules/hosts/trae/commands/wf-design.md +2 -2
  74. package/modules/hosts/trae/commands/wf-maintain.md +1 -1
  75. package/modules/hosts/trae/commands/wf-plan.md +3 -3
  76. package/modules/hosts/trae/commands/wf-review.md +1 -1
  77. package/modules/hosts/trae/commands/wf-source-sync-check.md +1 -1
  78. package/modules/hosts/trae/commands/wf-sync-hosts.md +1 -1
  79. package/modules/hosts/trae/commands/wf-test.md +3 -1
  80. package/modules/hosts/zcode/agents/implementer.md +1 -1
  81. package/modules/hosts/zcode/agents/independent-reviewer.md +1 -1
  82. package/modules/hosts/zcode/agents/ui-verifier.md +1 -1
  83. package/package.json +1 -1
  84. package/src/cli.mjs +16 -4
  85. package/src/doctor.mjs +58 -5
  86. package/src/fresh-init.test.mjs +36 -0
  87. package/src/gates.test.mjs +126 -0
  88. package/src/init.mjs +8 -1
  89. package/src/init.test.mjs +7 -1
  90. package/src/profiles.mjs +10 -4
  91. package/src/run-tests.mjs +3 -0
  92. package/src/stack-profile.test.mjs +89 -0
  93. package/src/sync-hosts.mjs +12 -6
  94. package/src/sync-hosts.test.mjs +40 -17
  95. package/templates/AGENTS.md +4 -4
  96. package/templates/_agents/board/index.html +3 -3
  97. package/templates/_agents/commands/build.md +4 -5
  98. package/templates/_agents/commands/design.md +4 -4
  99. package/templates/_agents/commands/maintain.md +1 -1
  100. package/templates/_agents/commands/new-task.md +13 -2
  101. package/templates/_agents/commands/plan.md +4 -4
  102. package/templates/_agents/commands/review.md +1 -1
  103. package/templates/_agents/commands/source-sync-check.md +1 -1
  104. package/templates/_agents/commands/sync-hosts.md +1 -1
  105. package/templates/_agents/commands/test.md +4 -2
  106. package/templates/_agents/hooks/commit-check.cjs +45 -24
  107. package/templates/_agents/lane-surfaces.txt +14 -0
  108. package/templates/_agents/roles/implementer.md +1 -1
  109. package/templates/_agents/roles/independent-reviewer.md +1 -1
  110. package/templates/_agents/roles/ui-verifier.md +1 -1
  111. package/templates/_agents/scripts/check-lane-surface.mjs +164 -0
  112. package/templates/_agents/scripts/check-lane-surface.test.mjs +200 -0
  113. package/templates/_agents/scripts/check-ledger.mjs +65 -0
  114. package/templates/_agents/scripts/check-ledger.test.mjs +127 -0
  115. package/templates/_agents/scripts/check-loop-rev.test.mjs +337 -0
  116. package/templates/_agents/scripts/check-loop.mjs +460 -268
  117. package/templates/_agents/scripts/check-loop.test.mjs +677 -17
  118. package/templates/_agents/scripts/check-metric-claims.mjs +275 -0
  119. package/templates/_agents/scripts/commit-check-trigger.test.mjs +64 -1
  120. package/templates/_agents/scripts/confirm-doc.mjs +159 -15
  121. package/templates/_agents/scripts/confirm-doc.test.mjs +362 -10
  122. package/templates/_agents/scripts/doctor.test.mjs +105 -4
  123. package/templates/_agents/scripts/fill-intent.mjs +8 -7
  124. package/templates/_agents/scripts/fill-intent.test.mjs +12 -3
  125. package/templates/_agents/scripts/fill-plan.mjs +27 -11
  126. package/templates/_agents/scripts/fill-plan.test.mjs +148 -8
  127. package/templates/_agents/scripts/fill-spec.mjs +18 -3
  128. package/templates/_agents/scripts/fill-spec.test.mjs +198 -1
  129. package/templates/_agents/scripts/gate-checklist.mjs +4 -1
  130. package/templates/_agents/scripts/gen-workflow-index.mjs +6 -2
  131. package/templates/_agents/scripts/gen-workflow-index.test.mjs +12 -0
  132. package/templates/_agents/scripts/policy.mjs +46 -0
  133. package/templates/_agents/scripts/solidify-task.mjs +101 -0
  134. package/templates/_agents/scripts/stage-gates.mjs +256 -0
  135. package/templates/_agents/scripts/trust-mode.mjs +67 -0
  136. package/templates/_agents/scripts/wf-journal.mjs +71 -2
  137. package/templates/_agents/scripts/wf-journal.test.mjs +66 -0
  138. package/templates/_agents/scripts/workflow-board-server.mjs +18 -15
  139. package/templates/_agents/scripts/workflow-board-server.test.mjs +30 -1
  140. package/templates/_agents/workflows/pipeline-closing.md +1 -1
  141. package/templates/_githooks/pre-commit +49 -2
  142. package/templates/_githooks/pre-push +56 -12
  143. package/templates/workflow/README.md +22 -3
  144. package/templates/workflow/incidents/_TEMPLATE.md +4 -0
  145. package/templates/workflow/intents/_TEMPLATE.md +1 -0
  146. /package/modules/hosts/opencode/commands/{deploy.md → wf-deploy.md} +0 -0
  147. /package/modules/hosts/opencode/commands/{gate-checklist.md → wf-gate-checklist.md} +0 -0
  148. /package/modules/hosts/opencode/commands/{new-task.md → wf-new-task.md} +0 -0
@@ -0,0 +1,126 @@
1
+ #!/usr/bin/env node
2
+ // 三个可选门禁:临时 git 仓库里暂存违规 / 干净文件,断言退出码。
3
+ import { spawnSync } from 'node:child_process';
4
+ import fs from 'node:fs';
5
+ import os from 'node:os';
6
+ import path from 'node:path';
7
+ import { fileURLToPath } from 'node:url';
8
+
9
+ const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
10
+ const git = process.platform === 'win32' ? 'git.exe' : 'git';
11
+ let pass = 0;
12
+ let fail = 0;
13
+ const check = (name, cond, detail = '') => {
14
+ if (cond) { pass++; console.log(`PASS ${name}`); }
15
+ else { fail++; console.log(`FAIL ${name}${detail ? `\n ${detail}` : ''}`); }
16
+ };
17
+
18
+ function repo() {
19
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'fk-gate-'));
20
+ spawnSync(git, ['init', '-q'], { cwd: dir });
21
+ spawnSync(git, ['config', 'user.email', 't@t'], { cwd: dir });
22
+ spawnSync(git, ['config', 'user.name', 't'], { cwd: dir });
23
+ return dir;
24
+ }
25
+ function stage(dir, rel, text) {
26
+ const abs = path.join(dir, rel);
27
+ fs.mkdirSync(path.dirname(abs), { recursive: true });
28
+ fs.writeFileSync(abs, text);
29
+ spawnSync(git, ['add', '--', rel], { cwd: dir });
30
+ }
31
+ function run(script, dir, env = {}) {
32
+ return spawnSync(process.execPath, [path.join(ROOT, script)], {
33
+ cwd: dir, encoding: 'utf8', env: { ...process.env, ...env },
34
+ });
35
+ }
36
+
37
+ {
38
+ const dir = repo();
39
+ stage(dir, 'src/app.mjs', "import { x } from '../tests/helper.mjs';\n");
40
+ const bad = run('modules/gates/node-layer/check-node-layer.mjs', dir);
41
+ check('node-layer 引用 tests → exit 1', bad.status === 1 && bad.stderr.includes('src/app.mjs'), bad.stderr);
42
+ fs.rmSync(dir, { recursive: true, force: true });
43
+ }
44
+ {
45
+ const dir = repo();
46
+ stage(dir, 'src/app.mjs', "import { x } from './util.mjs';\n");
47
+ const ok = run('modules/gates/node-layer/check-node-layer.mjs', dir);
48
+ check('node-layer 只引 src 内文件 → exit 0', ok.status === 0, ok.stderr);
49
+ fs.rmSync(dir, { recursive: true, force: true });
50
+ }
51
+ {
52
+ const dir = repo();
53
+ stage(dir, 'src/app.mjs', "import { x } from '../tests/helper.mjs';\n");
54
+ fs.writeFileSync(path.join(dir, 'src/app.mjs'), "import { x } from './util.mjs';\n");
55
+ const bad = run('modules/gates/node-layer/check-node-layer.mjs', dir);
56
+ check('node-layer 暂存违规、工作区已改回 → exit 1', bad.status === 1 && bad.stderr.includes('src/app.mjs'), bad.stderr);
57
+ fs.rmSync(dir, { recursive: true, force: true });
58
+ }
59
+ {
60
+ const dir = repo();
61
+ stage(dir, 'src/app.mjs', "import { x } from './util.mjs';\n");
62
+ fs.writeFileSync(path.join(dir, 'src/app.mjs'), "import { x } from '../tests/helper.mjs';\n");
63
+ const ok = run('modules/gates/node-layer/check-node-layer.mjs', dir);
64
+ check('node-layer 暂存干净、工作区违规 → exit 0', ok.status === 0, ok.stderr);
65
+ fs.rmSync(dir, { recursive: true, force: true });
66
+ }
67
+ {
68
+ const dir = repo();
69
+ stage(dir, 'pkg/mod.py', 'from tests.unit import helper\n');
70
+ const bad = run('modules/gates/py-import/check-py-import.mjs', dir);
71
+ check('py-import 业务文件 import tests → exit 1', bad.status === 1 && bad.stderr.includes('pkg/mod.py'), bad.stderr);
72
+ fs.rmSync(dir, { recursive: true, force: true });
73
+ }
74
+ {
75
+ const dir = repo();
76
+ stage(dir, 'tests/test_mod.py', 'from tests.unit import helper\n');
77
+ const ok = run('modules/gates/py-import/check-py-import.mjs', dir);
78
+ check('py-import 测试目录自身 → exit 0', ok.status === 0, ok.stderr);
79
+ fs.rmSync(dir, { recursive: true, force: true });
80
+ }
81
+ {
82
+ const dir = repo();
83
+ stage(dir, 'pkg/mod.py', 'from tests.unit import helper\n');
84
+ fs.writeFileSync(path.join(dir, 'pkg/mod.py'), 'import pkg.local\n');
85
+ const bad = run('modules/gates/py-import/check-py-import.mjs', dir);
86
+ check('py-import 暂存违规、工作区已改回 → exit 1', bad.status === 1 && bad.stderr.includes('pkg/mod.py'), bad.stderr);
87
+ fs.rmSync(dir, { recursive: true, force: true });
88
+ }
89
+ {
90
+ const dir = repo();
91
+ stage(dir, 'pkg/mod.py', 'import pkg.local\n');
92
+ fs.writeFileSync(path.join(dir, 'pkg/mod.py'), 'from tests.unit import helper\n');
93
+ const ok = run('modules/gates/py-import/check-py-import.mjs', dir);
94
+ check('py-import 暂存干净、工作区违规 → exit 0', ok.status === 0, ok.stderr);
95
+ fs.rmSync(dir, { recursive: true, force: true });
96
+ }
97
+ {
98
+ const dir = repo();
99
+ const body = 'before\n<!-- GENERATED:BEGIN -->\nsecret\n<!-- GENERATED:END -->\nafter\n';
100
+ stage(dir, 'doc.md', body);
101
+ spawnSync(git, ['-c', 'user.email=t@t', '-c', 'user.name=t', 'commit', '-qm', 'base'], { cwd: dir });
102
+ const next = body.replace('secret', 'edited');
103
+ stage(dir, 'doc.md', next);
104
+ const bad = run('modules/gates/generated-readonly/check-generated.mjs', dir);
105
+ check('generated 区被改 → exit 1', bad.status === 1 && bad.stderr.includes('GENERATED'), bad.stderr);
106
+ const allowed = run('modules/gates/generated-readonly/check-generated.mjs', dir, { FLOW_KIT_ALLOW_GENERATED: '1' });
107
+ check('FLOW_KIT_ALLOW_GENERATED=1 → exit 0', allowed.status === 0, allowed.stderr);
108
+ fs.writeFileSync(path.join(dir, 'doc.md'), 'before\nafter\n');
109
+ const still = run('modules/gates/generated-readonly/check-generated.mjs', dir);
110
+ check('generated 暂存改区、工作区删掉标记 → exit 1', still.status === 1 && still.stderr.includes('GENERATED'), still.stderr);
111
+ fs.rmSync(dir, { recursive: true, force: true });
112
+ }
113
+ {
114
+ const dir = repo();
115
+ const body = 'before\n<!-- GENERATED:BEGIN -->\nsecret\n<!-- GENERATED:END -->\nafter\n';
116
+ stage(dir, 'doc.md', body);
117
+ spawnSync(git, ['-c', 'user.email=t@t', '-c', 'user.name=t', 'commit', '-qm', 'base'], { cwd: dir });
118
+ stage(dir, 'doc.md', body.replace('before', 'before2'));
119
+ fs.writeFileSync(path.join(dir, 'doc.md'), body.replace('secret', 'edited'));
120
+ const ok = run('modules/gates/generated-readonly/check-generated.mjs', dir);
121
+ check('generated 暂存改区外、工作区改区内 → exit 0', ok.status === 0, ok.stderr);
122
+ fs.rmSync(dir, { recursive: true, force: true });
123
+ }
124
+
125
+ console.log(`\n合计: PASS ${pass} / FAIL ${fail}`);
126
+ process.exit(fail ? 1 : 0);
package/src/init.mjs CHANGED
@@ -77,6 +77,10 @@ export function hasAgentsSkeleton(text) {
77
77
  return String(text || '').includes(SKELETON_MARKER);
78
78
  }
79
79
  // mergeAgents:原内容在上(尾部空白折叠),空行分隔,含标记的完整骨架在下
80
+ export function menuMark(i) {
81
+ return String(i + 1);
82
+ }
83
+
80
84
  export function mergeAgents(existing, incoming) {
81
85
  const head = String(existing || '').replace(/\s+$/, '');
82
86
  const tail = String(incoming || '').trim();
@@ -90,7 +94,7 @@ async function interactive() {
90
94
  const hostNames = Object.keys(HOSTS);
91
95
  const stackNames = Object.keys(STACKS);
92
96
  const stackLabel = (n) => (n === 'node' ? 'node(ts/js)' : n);
93
- const menu = (names) => ' ' + names.map((n, i) => dim(`${'①②③④⑤⑥'[i]} ${stackLabel(n)}`)).join(dim(' '));
97
+ const menu = (names) => ' ' + names.map((n, i) => dim(`${menuMark(i)} ${stackLabel(n)}`)).join(dim(' '));
94
98
 
95
99
  console.log(bold(cyan('▶ flow-kit init')) + dim(' · AI 闭环工作流 + wiki 知识层脚手架'));
96
100
  console.log(dim(' 安装到当前目录:.agents/ · .githooks/ · workflow/ · wiki/ · AGENTS.md(已存在的文件保守跳过;AGENTS.md 无工作流骨架时文末追加补齐)'));
@@ -311,6 +315,9 @@ export async function init(args, pkgRoot) {
311
315
  fs.writeFileSync(kitPath, `${JSON.stringify({
312
316
  kit: 'agentic-flow-kit',
313
317
  version: pkg.version,
318
+ policyVersion: 2,
319
+ // 新装默认只跑硬规则。缺省(旧台账无此字段)在 check-loop 里仍是全量,避免升级后卫生检查突然消失。
320
+ audit: false,
314
321
  createdAt: new Date().toISOString(),
315
322
  options: { hosts, stack: opt.stack, boardPort: String(opt.boardPort) },
316
323
  managed,
package/src/init.test.mjs CHANGED
@@ -4,7 +4,8 @@
4
4
  // 断言:①ts/js 别名归一 node;②序号/名称/混输/全角逗号/空白分隔解析正确(去重保序);
5
5
  // ③非法输入返回 null(就地重问信号);④空输入回默认(EOF/直接回车安全回退);⑥装户面含 runner/workflows。
6
6
  // 用法:node src/init.test.mjs
7
- import { normalizeStack, parseChoices, hasAgentsSkeleton, mergeAgents } from './init.mjs';
7
+ import { normalizeStack, parseChoices, hasAgentsSkeleton, mergeAgents, menuMark } from './init.mjs';
8
+ import { HOSTS as HOST_REG } from './profiles.mjs';
8
9
  import { renderTree, scriptTrusted, listTree } from './render.mjs';
9
10
  import { sha256 } from './render.mjs';
10
11
  import fs from 'node:fs';
@@ -38,6 +39,11 @@ check('栈别名 ts + map 归一 → node', eq(parseChoices('ts', STACKS, 'none'
38
39
 
39
40
  // ---- ③ 非法输入 → null(就地重问) ----
40
41
  check('越界序号 9 → null', parseChoices('9', HOSTS, 'zcode') === null);
42
+ {
43
+ const realHosts = Object.keys(HOST_REG);
44
+ const n = String(realHosts.length);
45
+ check('菜单号覆盖全部宿主,序号 7 选中最后一项', menuMark(realHosts.length - 1) === n && eq(parseChoices(n, realHosts, 'zcode'), [realHosts[realHosts.length - 1]]));
46
+ }
41
47
  check('未知名称 bad → null', parseChoices('zcode,bad', HOSTS, 'zcode') === null);
42
48
  check('栈别名未传 map 不归一(ts → null)', parseChoices('ts', STACKS, 'none') === null);
43
49
 
package/src/profiles.mjs CHANGED
@@ -2,13 +2,19 @@
2
2
  // stack 决定门禁配置三处:commit-check 的条件构建(builds)与质量检测(checks,秒级确定性
3
3
  // 检查——lint/类型/vet;测试不放提交门,关单在 test.md 阶段门)、settings.json 的自检验
4
4
  // 命令权限(allow)、AGENTS.md「项目适配区」命令预填;不改工作流流程本身。
5
- // checks.when = 配置文件存在才启用(如 tsconfig.json / go.mod / ruff.toml)——没有对应
6
- // 配置的项目自动跳过,不 fail-closed 误拦。none = 不配置(项目日后自填)。
5
+ // checks.when / builds.when = 条件才启用。文件路径或尾部 * 通配表示配置文件存在;
6
+ // pkg:scripts.build 表示 package.json 里该脚本为非空字符串。没有对应配置的项目自动跳过,
7
+ // 不 fail-closed 误拦。none = 不配置(项目日后自填)。
7
8
  export const HOSTS = {
8
9
  zcode: { dir: '.zcode', localOnly: true },
9
10
  opencode: { dir: '.opencode', localOnly: false, commandPrefix: 'wf-' },
10
11
  trae: { dir: '.trae', localOnly: false, commandPrefix: 'wf-' },
11
12
  omp: { dir: '.omp', localOnly: true },
13
+ // 2026-09-29 店面:序号仍把 zcode 放第一(交互菜单 1–4 不变)。claude / cursor 的 commands 由宿主自动加载;
14
+ // codex 自动读仓库根 AGENTS.md 与 .codex/skills/,commands 与 agents 是同形薄转发,供显式引用。
15
+ claude: { dir: '.claude', localOnly: false, commandPrefix: 'wf-' },
16
+ cursor: { dir: '.cursor', localOnly: false, commandPrefix: 'wf-' },
17
+ codex: { dir: '.codex', localOnly: false, commandPrefix: 'wf-' },
12
18
  };
13
19
  // commandPrefix = 该宿主的命令层薄适配文件名(= 宿主命令名)统一前缀,2026-09-28 opencode-cmd-wf-prefix
14
20
  // 与 trae 对齐后单源:命令名不再裸占宿主顶层命名空间(撞宿主内置/第三方命令无从规避);缺该字段的宿主
@@ -33,7 +39,7 @@ export const STACKS = {
33
39
  build: 'npm run build',
34
40
  test: 'npm test',
35
41
  typecheck: 'npx tsc --noEmit(或 npm run typecheck)',
36
- builds: [{ name: 'build', command: 'npm run build', ext: ['.ts', '.tsx', '.vue', '.js', '.jsx', '.mjs', '.cjs'] }],
42
+ builds: [{ name: 'build', command: 'npm run build', ext: ['.ts', '.tsx', '.vue', '.js', '.jsx', '.mjs', '.cjs'], when: ['pkg:scripts.build'] }],
37
43
  checks: [
38
44
  { name: 'typecheck', command: 'npx tsc --noEmit', ext: ['.ts', '.tsx'], when: ['tsconfig.json'] },
39
45
  { name: 'lint', command: 'npx eslint .', ext: ['.ts', '.tsx', '.js', '.jsx', '.vue'], when: ESLINT_CONFIGS },
@@ -44,7 +50,7 @@ export const STACKS = {
44
50
  build: '(Python 无统一构建命令,按项目填)',
45
51
  test: 'python -m pytest',
46
52
  typecheck: '(可选:mypy)',
47
- builds: [{ name: 'tests', command: 'python -m pytest', ext: ['.py'] }],
53
+ builds: [],
48
54
  checks: [{ name: 'lint', command: 'ruff check .', ext: ['.py'], when: ['ruff.toml', '.ruff.toml'] }],
49
55
  allow: ['python -m pytest', 'pytest', 'ruff check'],
50
56
  },
package/src/run-tests.mjs CHANGED
@@ -12,6 +12,9 @@ const nodeSuites = [
12
12
  'src/sync.test.mjs',
13
13
  'src/init.test.mjs',
14
14
  'src/pack.test.mjs',
15
+ 'src/gates.test.mjs',
16
+ 'src/fresh-init.test.mjs',
17
+ 'src/stack-profile.test.mjs',
15
18
  // 宿主门禁模块回归(2026-09-27 host-gates-p1:dotnet-ca BRE 豁免/fail-closed + trae 强推 token 化)
16
19
  'src/gate-dotnet-ca.test.mjs',
17
20
  'src/trae-hooks.test.mjs',
@@ -0,0 +1,89 @@
1
+ #!/usr/bin/env node
2
+ // 栈提交门契约:node 的构建只在 package.json scripts.build 为非空字符串时跑;
3
+ // python 的提交门不再带 pytest;flow-kit --help 的门禁列表等于 modules/gates 目录。
4
+ // 集成段用临时 git 仓跑真实 init,再对装出的 commit-check.cjs 暂存一个源文件。
5
+ import { spawnSync } from 'node:child_process';
6
+ import fs from 'node:fs';
7
+ import os from 'node:os';
8
+ import path from 'node:path';
9
+ import { fileURLToPath } from 'node:url';
10
+ import { HELP } from './cli.mjs';
11
+ import { commitCheckConfig, settingsJson } from './profiles.mjs';
12
+
13
+ const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
14
+ const git = process.platform === 'win32' ? 'git.exe' : 'git';
15
+ let pass = 0;
16
+ let fail = 0;
17
+ const check = (name, cond, detail = '') => {
18
+ if (cond) { pass++; console.log(`PASS ${name}`); }
19
+ else { fail++; console.log(`FAIL ${name}${detail ? `\n ${detail}` : ''}`); }
20
+ };
21
+
22
+ const gates = fs.readdirSync(path.join(ROOT, 'modules', 'gates'), { withFileTypes: true })
23
+ .filter((d) => d.isDirectory())
24
+ .map((d) => d.name)
25
+ .sort();
26
+ const gateList = gates.join(' | ');
27
+ check('help 当前列表等于 modules/gates 目录', HELP.includes(`当前:${gateList}`), gateList);
28
+ check('help 含四套已发布门禁',
29
+ ['dotnet-ca', 'generated-readonly', 'node-layer', 'py-import'].every((n) => HELP.includes(n)));
30
+ check('help 不再把当前门禁写成只有 dotnet-ca',
31
+ !HELP.includes('当前:dotnet-ca;') && !HELP.includes('当前:dotnet-ca)'));
32
+
33
+ const nodeCfg = JSON.parse(commitCheckConfig('node'));
34
+ check('node 构建命令带 pkg:scripts.build',
35
+ nodeCfg.builds.some((b) => b.command === 'npm run build' && Array.isArray(b.when) && b.when.includes('pkg:scripts.build')));
36
+ const pyText = commitCheckConfig('python');
37
+ const pyCfg = JSON.parse(pyText);
38
+ check('python builds 为空', Array.isArray(pyCfg.builds) && pyCfg.builds.length === 0, pyText);
39
+ check('python 提交门配置不含 pytest', !pyText.includes('pytest'), pyText);
40
+ check('python 质量检查仍是 ruff',
41
+ pyCfg.checks.some((c) => String(c.command).includes('ruff') && (c.when || []).includes('ruff.toml')));
42
+ check('python 的 test 阶段权限仍允许 pytest', settingsJson('python').includes('pytest'));
43
+
44
+ const dirs = [];
45
+ function initStack(stack, port) {
46
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), `fk-stack-${stack}-`));
47
+ dirs.push(dir);
48
+ spawnSync(git, ['init', '-q'], { cwd: dir });
49
+ const r = spawnSync(process.execPath, [
50
+ path.join(ROOT, 'bin', 'flow-kit.mjs'), 'init',
51
+ '--stack', stack, '--hosts', 'claude', '--board-port', String(port), '--dir', dir,
52
+ ], { encoding: 'utf8', timeout: 180000 });
53
+ return { dir, status: r.status, out: `${r.stdout || ''}${r.stderr || ''}` };
54
+ }
55
+
56
+ function runCommitCheck(dir, rel, content) {
57
+ fs.writeFileSync(path.join(dir, rel), content);
58
+ spawnSync(git, ['add', '--', rel], { cwd: dir });
59
+ const r = spawnSync(process.execPath, [path.join(dir, '.agents', 'hooks', 'commit-check.cjs')], {
60
+ cwd: dir, encoding: 'utf8', timeout: 60000,
61
+ });
62
+ return { status: r.status, out: `${r.stdout || ''}${r.stderr || ''}` };
63
+ }
64
+
65
+ const nodeInit = initStack('node', 8942);
66
+ check('init --stack node 退出 0', nodeInit.status === 0, `exit=${nodeInit.status}\n${nodeInit.out.split('\n').slice(-25).join('\n')}`);
67
+ if (nodeInit.status === 0) {
68
+ const installed = fs.readFileSync(path.join(nodeInit.dir, '.agents', 'hooks', 'commit-check.config.json'), 'utf8');
69
+ check('装出的 node 配置含 pkg:scripts.build', installed.includes('pkg:scripts.build'), installed);
70
+ const hook = runCommitCheck(nodeInit.dir, 'a.js', 'console.log(1)\n');
71
+ check('无 scripts.build 时暂存 js:SKIP 且退出 0',
72
+ hook.status === 0 && hook.out.includes('SKIP') && hook.out.includes('pkg:scripts.build') && !hook.out.includes('BLOCK:'),
73
+ hook.out);
74
+ }
75
+
76
+ const pyInit = initStack('python', 8943);
77
+ check('init --stack python 退出 0', pyInit.status === 0, `exit=${pyInit.status}\n${pyInit.out.split('\n').slice(-25).join('\n')}`);
78
+ if (pyInit.status === 0) {
79
+ const installed = fs.readFileSync(path.join(pyInit.dir, '.agents', 'hooks', 'commit-check.config.json'), 'utf8');
80
+ check('装出的 python 配置不含 pytest', !installed.includes('pytest'), installed);
81
+ const hook = runCommitCheck(pyInit.dir, 'a.py', 'x = 1\n');
82
+ check('暂存 py:不跑 pytest 且退出 0',
83
+ hook.status === 0 && !hook.out.includes('pytest') && hook.out.includes('COMMIT_CHECK_OK'),
84
+ hook.out);
85
+ }
86
+
87
+ for (const dir of dirs) fs.rmSync(dir, { recursive: true, force: true });
88
+ console.log(`\n合计: PASS ${pass} / FAIL ${fail}`);
89
+ process.exit(fail ? 1 : 0);
@@ -1,8 +1,9 @@
1
1
  // flow-kit sync-hosts:跨宿主适配层正文段漂移检查 + 单向同步(B-b 方案,2026-09-25)
2
2
  // 目的:改权威源(templates/_agents/{commands,roles}/*.md)正文要同步到 N 份薄适配
3
3
  // (modules/hosts/<h>/{agents,commands}/*.md)的对应正文段;薄适配 frontmatter 保留宿主特化字段
4
- // (trae commands 加 name: wf-X、opencode/zcode/omp 各自原描述),不在同步范围。
4
+ // (commands 层 trae 侧另有 name: wf-X,opencode/zcode/omp 各自原描述),不在同步范围。
5
5
  // B-b 语义:sha 比对与 apply 只动正文段,frontmatter 双方各自维护。
6
+ // 映射单源在 profiles.mjs#HOSTS.commandPrefix(命令层薄适配文件名前缀;2026-09-28 opencode 对齐 trae 的 wf- 前缀)。
6
7
  // 算法:diffHosts 抽为 pure function(authorityRoot + adaptersRoot 两个根);
7
8
  // sync-hosts CLI 在 pkgRoot 跑(包源视角);doctor §7.x 在 target 跑(装户视角)——同源逻辑。
8
9
  import fs from 'node:fs';
@@ -41,14 +42,17 @@ function listMd(dir) {
41
42
  return fs.readdirSync(dir).filter((f) => f.endsWith('.md'));
42
43
  }
43
44
 
44
- // pairsFor:权威源文件 → 薄适配 rel 列表(与 sync-hosts 早版同映射;trae commands 加 wf- 前缀)
45
+ // pairsFor:权威源文件 → 薄适配 rel 列表(映射单源见 profiles.mjs#HOSTS.commandPrefix)
46
+ // commands 权威源 → 带 commandPrefix 的宿主各一份(<dir>/commands/<prefix><name>.md)
47
+ // roles 权威源 → 全部宿主 agents 各一份
45
48
  function pairsFor(authRel) {
46
49
  const out = [];
47
50
  const norm = authRel.split(path.sep).join('/');
48
51
  if (norm.startsWith('commands/') && norm.endsWith('.md')) {
49
52
  const name = norm.slice('commands/'.length, -'.md'.length);
50
- out.push({ adapterRel: `opencode/commands/${name}.md` });
51
- out.push({ adapterRel: `trae/commands/wf-${name}.md` });
53
+ for (const [h, v] of Object.entries(HOSTS)) {
54
+ if (v.commandPrefix) out.push({ adapterRel: `${h}/commands/${v.commandPrefix}${name}.md` });
55
+ }
52
56
  } else if (norm.startsWith('roles/') && norm.endsWith('.md')) {
53
57
  const name = norm.slice('roles/'.length, -'.md'.length);
54
58
  for (const h of Object.keys(HOSTS)) out.push({ adapterRel: `${h}/agents/${name}.md` });
@@ -104,8 +108,10 @@ export function diffHosts({ authorityRoot, adaptersRoot }) {
104
108
  const adapterRel = `${h}/${sub}/${f}`;
105
109
  if (adapterSeen.has(adapterRel)) continue;
106
110
  let authGuess;
107
- if (h === 'trae' && sub === 'commands' && f.startsWith('wf-')) {
108
- authGuess = `commands/${f.slice('wf-'.length)}`;
111
+ const prefix = HOSTS[h] && HOSTS[h].commandPrefix;
112
+ if (sub === 'commands' && prefix && f.startsWith(prefix)) {
113
+ // 命令层薄适配带宿主 commandPrefix(2026-09-28:opencode 对齐 trae 后两宿主同口径)
114
+ authGuess = `commands/${f.slice(prefix.length)}`;
109
115
  } else if (sub === 'agents') {
110
116
  authGuess = `roles/${f}`;
111
117
  } else {
@@ -10,6 +10,7 @@ import path from 'node:path';
10
10
  import os from 'node:os';
11
11
  import { spawnSync } from 'node:child_process';
12
12
  import { fileURLToPath, pathToFileURL } from 'node:url';
13
+ import { diffHosts } from './sync-hosts.mjs';
13
14
 
14
15
  const SRC_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)));
15
16
 
@@ -31,12 +32,12 @@ function mkFixture() {
31
32
  `---\ndescription: Test 阶段\nstage: Test\n---\n\n# Test · v1\n\n权威源正文 test v1\n`);
32
33
  W(path.join(root, 'templates/_agents/roles/implementer.md'),
33
34
  `---\ndescription: Implementer 角色契约\n---\n\n# Implementer · v1\n\n权威源正文 implementer v1\n`);
34
- // 薄适配:commands 权威源 → opencode + trae(trae 加 wf- 前缀 + name)
35
- W(path.join(root, 'modules/hosts/opencode/commands/build.md'),
35
+ // 薄适配:commands 权威源 → opencode + trae(两宿主同为 wf- 前缀;trae 另有 name 字段)
36
+ W(path.join(root, 'modules/hosts/opencode/commands/wf-build.md'),
36
37
  `---\ndescription: Build · opencode 风格\n---\n\n# Build · v1\n\n权威源正文 build v1\n`);
37
38
  W(path.join(root, 'modules/hosts/trae/commands/wf-build.md'),
38
39
  `---\nname: wf-build\ndescription: Build · trae 风格\n---\n\n# Build · v1\n\n权威源正文 build v1\n`);
39
- W(path.join(root, 'modules/hosts/opencode/commands/test.md'),
40
+ W(path.join(root, 'modules/hosts/opencode/commands/wf-test.md'),
40
41
  `---\ndescription: Test · opencode 风格\n---\n\n# Test · v1\n\n权威源正文 test v1\n`);
41
42
  W(path.join(root, 'modules/hosts/trae/commands/wf-test.md'),
42
43
  `---\nname: wf-test\ndescription: Test · trae 风格\n---\n\n# Test · v1\n\n权威源正文 test v1\n`);
@@ -49,8 +50,18 @@ function mkFixture() {
49
50
  `---\nname: implementer\ndescription: opencode 英文风格\n---\n\n# Implementer · v1\n\n权威源正文 implementer v1\n`);
50
51
  W(path.join(root, 'modules/hosts/trae/agents/implementer.md'),
51
52
  `---\nname: implementer\ndescription: trae 英文风格\n---\n\n# Implementer · v1\n\n权威源正文 implementer v1\n`);
52
- // 孤儿薄适配:权威源无对应文件
53
+ for (const h of ['claude', 'cursor', 'codex']) {
54
+ for (const cmd of ['build', 'test']) {
55
+ const title = cmd === 'build' ? 'Build' : 'Test';
56
+ W(path.join(root, 'modules/hosts', h, 'commands', `wf-${cmd}.md`),
57
+ `---\ndescription: ${title} · ${h}\n---\n\n# ${title} · v1\n\n权威源正文 ${cmd} v1\n`);
58
+ }
59
+ W(path.join(root, 'modules/hosts', h, 'agents/implementer.md'),
60
+ `---\nname: implementer\ndescription: ${h}\n---\n\n# Implementer · v1\n\n权威源正文 implementer v1\n`);
61
+ }
62
+ // 孤儿薄适配:权威源无对应文件(两种形态——带 wf- 前缀与不带前缀)
53
63
  W(path.join(root, 'modules/hosts/opencode/commands/orphan.md'), '# orphan\n');
64
+ W(path.join(root, 'modules/hosts/opencode/commands/wf-legacy.md'), '# legacy\n');
54
65
  return root;
55
66
  }
56
67
 
@@ -69,7 +80,7 @@ syncHosts(process.argv.slice(2), root);
69
80
  const fx = mkFixture();
70
81
  const r = runSyncHosts(fx, ['--diff']);
71
82
  const out = r.stdout + r.stderr;
72
- check('S1 全对齐:正文对齐 8 对(2 commands×2 适配 + 1 role×4 宿主)', /正文对齐:\s*8\s*对/.test(out), out);
83
+ check('S1 全对齐:正文对齐 17 对(2 commands×5 适配 + 1 role×7 宿主)', /正文对齐:\s*17\s*对/.test(out), out);
73
84
  check('S1 全对齐:报告无漂移', /无漂移/.test(out), out);
74
85
  check('S1 全对齐:exit 0', r.status === 0, `status=${r.status}`);
75
86
  }
@@ -83,10 +94,10 @@ syncHosts(process.argv.slice(2), root);
83
94
  `---\ndescription: Test 阶段\nstage: Test\n---\n\n# Test · v2\n\n权威源正文 test v2\n`);
84
95
  const r = runSyncHosts(fx, ['--diff']);
85
96
  const out = r.stdout + r.stderr;
86
- check('S2 权威源改了 → 正文漂移 4(build×2 + test×2)', /正文段漂移(4/.test(out), out);
87
- check('S2 列出 opencode/commands/build.md', out.includes('commands/build.md → opencode/commands/build.md'), out);
97
+ check('S2 权威源改了 → 正文漂移 10(build×5 + test×5)', /正文段漂移(10/.test(out), out);
98
+ check('S2 列出 opencode/commands/wf-build.md(opencode wf- 前缀映射)', out.includes('commands/build.md → opencode/commands/wf-build.md'), out);
88
99
  check('S2 列出 trae/commands/wf-build.md(trae wf- 前缀映射)', out.includes('commands/build.md → trae/commands/wf-build.md'), out);
89
- check('S2 正文对齐 = 4(1 role × 4 宿主 implementer 未改)', /正文对齐:\s*4\s*对/.test(out), out);
100
+ check('S2 正文对齐 = 7(1 role × 7 宿主 implementer 未改)', /正文对齐:\s*7\s*对/.test(out), out);
90
101
  check('S2 exit 0(diff 模式不阻断)', r.status === 0);
91
102
  }
92
103
 
@@ -97,10 +108,10 @@ syncHosts(process.argv.slice(2), root);
97
108
  `---\ndescription: Build 阶段\nstage: Build\n---\n\n# Build · v2\n\n权威源正文 build v2\n`);
98
109
  const r = runSyncHosts(fx, ['--apply']);
99
110
  const out = r.stdout + r.stderr;
100
- check('S3 apply:报告同步 2 份(opencode + trae)', /按权威源正文覆盖薄适配正文段\s*2\s*份/.test(out), out);
101
- check('S3 apply:opencode 正文已同步到 v2', R(path.join(fx, 'modules/hosts/opencode/commands/build.md')).includes('权威源正文 build v2'));
111
+ check('S3 apply:报告同步 5 份(带 commandPrefix 的宿主)', /按权威源正文覆盖薄适配正文段\s*5\s*份/.test(out), out);
112
+ check('S3 apply:opencode 正文已同步到 v2', R(path.join(fx, 'modules/hosts/opencode/commands/wf-build.md')).includes('权威源正文 build v2'));
102
113
  check('S3 apply:trae 正文已同步到 v2', R(path.join(fx, 'modules/hosts/trae/commands/wf-build.md')).includes('权威源正文 build v2'));
103
- check('S3 apply:opencode frontmatter 保留(仍无 name 字段)', !R(path.join(fx, 'modules/hosts/opencode/commands/build.md')).match(/^name:/m));
114
+ check('S3 apply:opencode frontmatter 保留(仍无 name 字段——前缀由文件名承载)', !R(path.join(fx, 'modules/hosts/opencode/commands/wf-build.md')).match(/^name:/m));
104
115
  check('S3 apply:trae frontmatter 保留(仍含 name: wf-build)', R(path.join(fx, 'modules/hosts/trae/commands/wf-build.md')).match(/^name:\s*wf-build/m) !== null);
105
116
  check('S3 apply:权威源不动', R(path.join(fx, 'templates/_agents/commands/build.md')).includes('权威源正文 build v2'));
106
117
  check('S3 apply:再次 diff 应显示对齐 8', true); // 二次扫描由 sync-hosts 内部做
@@ -114,7 +125,7 @@ syncHosts(process.argv.slice(2), root);
114
125
  `---\nname: wf-build\ndescription: trae 自定义描述\n---\n\n# Build · v1\n\n权威源正文 build v1\n`);
115
126
  const r = runSyncHosts(fx, ['--diff']);
116
127
  const out = r.stdout + r.stderr;
117
- check('S4 frontmatter 差异不计正文段漂移', /正文对齐:\s*8\s*对/.test(out) && /无漂移/.test(out), out);
128
+ check('S4 frontmatter 差异不计正文段漂移', /正文对齐:\s*17\s*对/.test(out) && /无漂移/.test(out), out);
118
129
  }
119
130
 
120
131
  // ============ 场景 5:薄适配正文手改 → drift 列出,apply 覆盖(不污染权威源)============
@@ -136,12 +147,12 @@ syncHosts(process.argv.slice(2), root);
136
147
  // ============ 场景 6:薄适配文件不存在 → authorityMissing 列出,apply 不创建 ============
137
148
  {
138
149
  const fx = mkFixture();
139
- fs.rmSync(path.join(fx, 'modules/hosts/opencode/commands/build.md'));
150
+ fs.rmSync(path.join(fx, 'modules/hosts/opencode/commands/wf-build.md'));
140
151
  const r = runSyncHosts(fx, ['--diff']);
141
152
  const out = r.stdout + r.stderr;
142
- check('S6 薄适配缺失 → authorityMissing 列', /权威源声明但薄适配缺失(/.test(out) && out.includes('opencode/commands/build.md'), out);
153
+ check('S6 薄适配缺失 → authorityMissing 列', /权威源声明但薄适配缺失(/.test(out) && out.includes('opencode/commands/wf-build.md'), out);
143
154
  const r2 = runSyncHosts(fx, ['--apply']);
144
- check('S6 apply 不自动创建薄适配(仍缺失)', !fs.existsSync(path.join(fx, 'modules/hosts/opencode/commands/build.md')));
155
+ check('S6 apply 不自动创建薄适配(仍缺失)', !fs.existsSync(path.join(fx, 'modules/hosts/opencode/commands/wf-build.md')));
145
156
  }
146
157
 
147
158
  // ============ 场景 7:--json 机器可读输出 ============
@@ -152,7 +163,7 @@ syncHosts(process.argv.slice(2), root);
152
163
  let parsed = null;
153
164
  try { parsed = JSON.parse(out); } catch {}
154
165
  check('S7 --json 输出可解析', parsed && typeof parsed === 'object' && Array.isArray(parsed.drift));
155
- check('S7 --json 含 inSync = 8', parsed && parsed.inSync === 8, parsed ? JSON.stringify(parsed) : 'null');
166
+ check('S7 --json 含 inSync = 17', parsed && parsed.inSync === 17, parsed ? JSON.stringify({ inSync: parsed && parsed.inSync, missing: parsed && parsed.authorityMissing }) : 'null');
156
167
  }
157
168
 
158
169
  // ============ 场景 8:孤儿薄适配(权威源无对应文件)→ adapterOrphans 列,apply 不删 ============
@@ -162,8 +173,20 @@ syncHosts(process.argv.slice(2), root);
162
173
  const r = runSyncHosts(fx, ['--diff']);
163
174
  const out = r.stdout + r.stderr;
164
175
  check('S8 孤儿薄适配 → adapterOrphans 列', /孤儿薄适配(/.test(out) && out.includes('opencode/commands/orphan.md'), out);
176
+ check('S8 opencode 孤儿带 wf- 前缀 → 反推权威源去掉前缀', out.includes('opencode/commands/wf-legacy.md(推测权威源:commands/legacy.md)'), out);
177
+ check('S8 opencode 孤儿不带前缀 → 原样反推 commands/<file>', out.includes('opencode/commands/orphan.md(推测权威源:commands/orphan.md)'), out);
165
178
  const r2 = runSyncHosts(fx, ['--apply']);
166
- check('S8 apply 不删孤儿薄适配', fs.existsSync(path.join(fx, 'modules/hosts/opencode/commands/orphan.md')));
179
+ check('S8 apply 不删孤儿薄适配', fs.existsSync(path.join(fx, 'modules/hosts/opencode/commands/orphan.md')) && fs.existsSync(path.join(fx, 'modules/hosts/opencode/commands/wf-legacy.md')));
180
+ }
181
+
182
+ {
183
+ const pkg = path.resolve(SRC_ROOT, '..');
184
+ const r = diffHosts({
185
+ authorityRoot: path.join(pkg, 'templates', '_agents'),
186
+ adaptersRoot: path.join(pkg, 'modules', 'hosts'),
187
+ });
188
+ const sample = r.authorityMissing.slice(0, 3).map((x) => x.adapterRel).concat(r.drift.slice(0, 3).map((x) => x.adapterRel)).join('、');
189
+ check('实仓薄适配无缺失无正文漂移', r.authorityMissing.length === 0 && r.drift.length === 0, `missing=${r.authorityMissing.length} drift=${r.drift.length} ${sample}`);
167
190
  }
168
191
 
169
192
  console.log(`\n合计: PASS ${pass} / FAIL ${failCount}`);
@@ -11,7 +11,7 @@
11
11
 
12
12
  1. **新需求 / 功能**:先立 `workflow/intents/` intent(复制 `_TEMPLATE.md`),对话内与用户确认后开工。
13
13
  2. **修复类任务**:先查 `workflow/incidents/` 同类历史;L1 以上立 incident(修复中即 intent 等价入口,L2/L3 与 spec/plan 同名配对)。
14
- 3. **级别判断**(就高不就低):实现级(页面 / 交互 / 样式)→ L1;改「规则 / 契约」→ L2;改「数据与运行时结构」(schema / 迁移 SQL / DI 链 / 认证与中间件管线)→ L3。细则见 `.agents/commands/new-task.md`。
14
+ 3. **级别判断与风险泳道**(就高不就低):L0 例行;L1 实现级;L2 规则/契约;L3 数据与运行时结构。L0/L1 协作道(轻确认+异步审计);L2/L3 防御道(同步确认门:spec/plan 确认后方可动手,L3 加独立复核)。见 `.agents/commands/new-task.md`。
15
15
 
16
16
  ### 阶段路由
17
17
 
@@ -23,11 +23,11 @@
23
23
 
24
24
  ### 门禁与提交
25
25
 
26
- - `.githooks/`(经 `core.hooksPath` 挂载,本地 clone 后执行 `git config core.hooksPath .githooks`):pre-commit(闭环配对 / wiki 台账 / 规则面预算 / 敏感信息扫描与条件构建)、pre-push(闭环断档扫描)、commit-msg、post-commit、pre-merge-commit。
26
+ - `.githooks/`(经 `core.hooksPath` 挂载,本地 clone 后执行 `git config core.hooksPath .githooks`):pre-commit(闭环配对 / 泳道完整性 / wiki 台账 / 规则面预算 / 双源一致性 / managed 台账快检 / 敏感信息扫描与条件构建)、pre-push(闭环断档扫描)、commit-msg、post-commit、pre-merge-commit。
27
27
  - **`git commit` / `git merge` / `git push` 三处一律禁 `--no-verify`**——被拦说明产出不合规,按提示修完原路重试。
28
- - 项目专属门禁挂 `.agents/hooks/local-pre-commit`。
28
+ - 项目专属门禁挂 `.agents/hooks/local-pre-commit`。装户五条硬规则(配对、验收、确认留痕、敏感信息、双源台账)与 `audit` 档见 `workflow/README.md`「硬规则」。
29
29
  - 提交遵循 Conventional Commits 中文(feat / fix / docs / style / refactor / perf);L1+ 入口文档 / spec / plan 随代码同一提交;确认(approved)后立即 `docs(*)` 单独提交留痕。
30
- - **确认门(2026-09-27 起)**:intent/spec/plan 的 approved/done 与 incident 的 fixed/closed(2026-09-28 起,open→fixed / fixed→closed 两跳、无单跳)唯一入口 = `node .agents/scripts/confirm-doc.mjs <path>`,放弃态走显式 `--to superseded|cancelled`(cancelled 自 draft/approved/open/fixed;superseded 自 approved/done/fixed/closed——四终态同样内容绑定),两形态:① 用户终端亲手运行键入「可以」(TTY,AI 会话内被拒);② **对话委托代录**——用户在对话内明确确认后,AI 跑 `confirm-doc.mjs <path> --delegated "<用户原话>"` 逐件代录(一次一份,「逐件确认」口径——多份并录被拒),台账如实记 `source: chat-delegated` + 原话供对质,永不伪装 TTY 行(check-loop 15 指纹+台账对账拦截,与 source 无关)。
30
+ - **确认门(2026-09-27 起)**:intent/spec/plan 的 approved/done 与 incident 的 fixed/closed(两跳、无单跳)唯一入口 = `node .agents/scripts/confirm-doc.mjs <path>`,放弃态走显式 `--to superseded|cancelled`(合法前态见脚本提示;四终态同样内容绑定),两形态:① 用户终端亲手运行键入「可以」(TTY,AI 会话内被拒);② **对话委托代录**——用户在对话内明确确认后,AI 跑 `confirm-doc.mjs <path> --delegated "<用户原话>"` 逐件代录(默认一次一份,多份并录被拒),台账如实记 `source: chat-delegated` + 原话供对质,永不伪装 TTY 行(check-loop 15 指纹+台账对账拦截,与 source 无关);L0/L1 协作道可 `--batch` 多份一次代录(台账 `brief:true`,15 并录告警豁免;L2/L3 仍逐份)。
31
31
 
32
32
  ### 检索、看板与量化
33
33
 
@@ -232,7 +232,7 @@ const badge = (c) =>
232
232
  acceptBadge(c) + alertBadge(c) + ageBadge(c) +
233
233
  `<span class="date">${esc(c.date)}</span>`;
234
234
 
235
- // —— 任务族聚合:同 slug 一卡;head 取入口文档(intent 优先、其次 incident),孤儿用首文档 ——
235
+ // —— 任务族聚合:同 slug 一卡(只用于展示;告警配对在服务端按完整文件名)。head 取入口文档(intent 优先、其次 incident),孤儿用首文档 ——
236
236
  const TYPE_ORDER = { intents: 0, incidents: 1, plans: 2, specs: 3 };
237
237
  const TYPE_ABBR = { intents: 'IN', incidents: 'IC', plans: 'PL', specs: 'SP' };
238
238
  const EXPANDED = new Set(); // 展开中的任务 slug(SSE 刷新后保持)
@@ -319,7 +319,7 @@ async function refreshBoard() {
319
319
  `<div class="doc-item" data-file="${esc(d.file)}" title="打开文档">` +
320
320
  `<span class="type" style="background:${TYPE_COLOR[d.type]}">${TYPE_LABEL[d.type]}</span>` +
321
321
  `<span class="chip">${esc(d.status)}</span>${alertBadge(d)}` +
322
- `<span class="doc-title">${esc(d.title)}</span></div>`).join('')}</div></div>`;
322
+ `<span class="doc-title">${esc(d.date)} · ${esc(d.title)}</span></div>`).join('')}</div></div>`;
323
323
  };
324
324
  let html = COLS.map((c) => {
325
325
  const items = groups[c.key].map((fam) => cardHtml(fam)).join('');
@@ -342,7 +342,7 @@ function renderDrawer() {
342
342
  $('#d-title').textContent = CURRENT.title;
343
343
  const related = CARDS.filter((c) => c.slug === CURRENT.slug && c.file !== CURRENT.file);
344
344
  $('#d-related').innerHTML = related.map((c) =>
345
- `<a data-file="${esc(c.file)}">${TYPE_LABEL[c.type]}${c.status !== CURRENT.status ? ' · ' + esc(c.status) : ''}</a>`).join('');
345
+ `<a data-file="${esc(c.file)}">${TYPE_LABEL[c.type]} · ${esc(c.date)}${c.status !== CURRENT.status ? ' · ' + esc(c.status) : ''}</a>`).join('');
346
346
  document.querySelectorAll('#d-tabs button').forEach((b) => b.classList.toggle('on', b.dataset.tab === TAB));
347
347
  if (TAB === 'doc') loadDoc(); else loadHist();
348
348
  }
@@ -25,14 +25,13 @@ next: .agents/commands/test.md
25
25
 
26
26
  ### 起草 plan
27
27
 
28
- 1. 确认同名入口文档(approved intent 或已确认的 incident)+ spec(若 L2/L3)均已确认;否则拒绝,回上一阶段
28
+ 1. 确认同名入口文档(approved intent 或已确认的 incident)+ spec(若 L2/L3)均已确认;否则回上一阶段(fill/confirm 未过即拒)
29
29
  2. 起草 plan:
30
30
  - 复制 `workflow/plans/_TEMPLATE.md` → `workflow/plans/YYYY-MM-DD-<主题>.md`(与入口文档同名)
31
31
  - L2/L3 按 4 节填写:任务拆解(每个任务有判据) / 风险评估 / 执行顺序 / 遗留项
32
- - L1 默认极简两节:改动面(逐条文件+做什么) / 验证方式(一行静态门+按需 UI 实测);仅多文件多步骤时再加任务拆解/执行顺序
33
- - 仍须与入口文档同名落盘
34
- 3. 停下,输出 plan 草稿全文给用户过目,一句"可以"即确认。**逐件确认**:入口文档 / spec / plan 各自的确认点不得并作一次,plan 草稿须全文过目(不接受摘要代替)。**逐件配逐件原话**:`--delegated` 每次传当次实际放行的措辞,不复用同句——复用同句使台账 quote 失去分辨力(事后对质看不出放行的是哪一件),且相邻同 quote 连跑触发 check-loop 15 并录告警(见 workflow/papercuts.md 2026-09-28)
35
- **确认落态唯一入口**(2026-09-27 起):用户在终端跑 `node .agents/scripts/confirm-doc.mjs <path>` 键入「可以」(draft→approved / approved→done;对话内明确确认后 AI 可 `--delegated "<原话>"` 代录,台账如实记 source/quote——**逐件调用 + 逐件原话**(一次一份,build.md「逐件确认」口径;多份并录被 confirm-doc 拒绝。--delegated 每次传当次实际放行的措辞,不复用同句——复用同句使台账 quote 失去分辨力,且相邻同 quote 连跑会触发并录告警))。check-loop 15 如实口径:缺记录/指纹不配对 → hard-block 拦截;伪造台账本地不可机器防,留痕供事后对质;done 内容绑定(2026-09-28 起)——关单编辑(勾验/回填)先于 done 确认、confirm-doc 是最后一次写入,此后修订走 superseded 或新 intent。
32
+ - L1 使用 Quick-Plan 极简三节:改动方案 / 约束与风险(原 Spec 核心) / 验证计划;多文件多步骤时可补任务拆解/执行顺序
33
+ 3. 停下,输出 plan 草稿全文给用户过目,一句"可以"即确认。**逐件确认**:入口文档 / spec / plan 各自的确认点不得并作一次,plan 草稿须全文过目(不接受摘要代替)。**逐件配逐件原话**:`--delegated` 每次传当次实际放行的措辞,不复用同句——复用同句使台账 quote 失去分辨力(事后对质看不出放行的是哪一件);原因与根治见 workflow/papercuts.md 2026-09-28
34
+ **确认落态唯一入口**(2026-09-27 起):用户在终端跑 `node .agents/scripts/confirm-doc.mjs <path>` 键入「可以」(draft→approved / approved→done;对话内明确确认后 AI 可 `--delegated "<原话>"` 代录,台账如实记 source/quote——**逐件调用 + 逐件原话**(一次一份,build.md「逐件确认」口径;多份并录被 confirm-doc 拒绝。--delegated 每次传当次实际放行的措辞,不复用同句——复用同句使台账 quote 失去分辨力))。check-loop 15 如实口径:缺记录/指纹不配对 → hard-block 拦截;伪造台账本地不可机器防,留痕供事后对质;done 内容绑定(2026-09-28 起)——关单编辑(勾验/回填)先于 done 确认、confirm-doc 是最后一次写入,此后修订走 superseded 或新 intent。
36
35
  4. 确认后 → plan 状态 draft → approved,并在 plan 内补「确认与复核」节(确认结果 + 日期)。确认环节须在文档留痕:`approved` 是确认的机器可见态,`done` 只在关单出现,禁从 `draft` 直跳 `done`;确认后立即 `docs(workflow)` 单独提交留痕(根 `AGENTS.md` 提交约定;check-loop 检查 14 口径)
37
36
 
38
37
  ### 进计划模式列改动
@@ -18,13 +18,13 @@ next: .agents/commands/build.md
18
18
 
19
19
  # Design · 起 spec
20
20
 
21
- > L2/L3 强制;L1 可省略(直接跳 `next: build.md`)。
21
+ > L2/L3 强制;L1 走快车道(直接跳 `next: build.md` 起草 Quick-Plan)。
22
22
 
23
23
  ## 执行
24
24
 
25
- 1. 确认同名入口文档已批准:新需求为 approved intent,修复为已确认的 incident 草稿;否则拒绝,分别回 `plan.md` / `maintain.md`
25
+ 1. 确认同名入口文档已批准:新需求为 approved intent,修复为已确认的 incident 草稿;否则拒绝,分别回 `plan.md` / `maintain.md`(2026-09-30 起机器强制:fill-spec / confirm-doc 对前置未过直接拒绝 exit 2、不落盘不记账)
26
26
  2. 级别判断(对照 AGENTS.md 第 3 条):
27
- - **L1**(实现级改动,未命中 L2/L3)→ 可省略 spec,直接跳 `next: build.md`
27
+ - **L1**(实现级改动,未命中 L2/L3)→ 走快车道,省略 spec,直接跳 `next: build.md` 起草 Quick-Plan
28
28
  - **L2**(改规则 / 契约:编码权威 / 共享契约 / 既有接口语义 / 全局横切口径,闭集见 `new-task.md` §级别判断)→ 必须有 spec
29
29
  - **L3**(数据与运行时结构:schema / 迁移 SQL / DI 链 / 认证与中间件管线)→ 必须有 spec + **新会话独立复核**
30
30
  3. 起草 spec:
@@ -51,6 +51,6 @@ next: .agents/commands/build.md
51
51
  ## 确认后
52
52
 
53
53
  - spec 状态 draft → approved
54
- **确认落态唯一入口**(2026-09-27 起):用户在终端跑 `node .agents/scripts/confirm-doc.mjs <path>` 键入「可以」(draft→approved / approved→done;对话内明确确认后 AI 可 `--delegated "<原话>"` 代录,台账如实记 source/quote——**逐件调用 + 逐件原话**(一次一份,build.md「逐件确认」口径;多份并录被 confirm-doc 拒绝。--delegated 每次传当次实际放行的措辞,不复用同句——复用同句使台账 quote 失去分辨力,且相邻同 quote 连跑会触发并录告警))。check-loop 15 如实口径:缺记录/指纹不配对 → hard-block 拦截;伪造台账本地不可机器防,留痕供事后对质;done 内容绑定(2026-09-28 起)——关单编辑(勾验/回填)先于 done 确认、confirm-doc 是最后一次写入,此后修订走 superseded 或新 intent。
54
+ **确认落态唯一入口**(2026-09-27 起):用户在终端跑 `node .agents/scripts/confirm-doc.mjs <path>` 键入「可以」(draft→approved / approved→done;对话内明确确认后 AI 可 `--delegated "<原话>"` 代录,台账如实记 source/quote——**逐件调用 + 逐件原话**(一次一份,build.md「逐件确认」口径;多份并录被 confirm-doc 拒绝。--delegated 每次传当次实际放行的措辞,不复用同句——复用同句使台账 quote 失去分辨力))。check-loop 15 如实口径:缺记录/指纹不配对 → hard-block 拦截;伪造台账本地不可机器防,留痕供事后对质;done 内容绑定(2026-09-28 起)——关单编辑(勾验/回填)先于 done 确认、confirm-doc 是最后一次写入,此后修订走 superseded 或新 intent。
55
55
  - 立即 `docs(workflow)` 单独提交留痕(根 `AGENTS.md` 提交约定;check-loop 检查 14 口径)
56
56
  - 进 `next: .agents/commands/build.md`