agentic-flow-kit 0.3.1 → 0.5.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 (87) hide show
  1. package/README.md +2 -0
  2. package/modules/hosts/omp/agents/implementer.md +45 -6
  3. package/modules/hosts/omp/agents/independent-reviewer.md +42 -5
  4. package/modules/hosts/omp/agents/ui-verifier.md +41 -6
  5. package/modules/hosts/opencode/agents/implementer.md +45 -1
  6. package/modules/hosts/opencode/agents/independent-reviewer.md +42 -1
  7. package/modules/hosts/opencode/agents/ui-verifier.md +41 -1
  8. package/modules/hosts/opencode/commands/build.md +55 -7
  9. package/modules/hosts/opencode/commands/deploy.md +51 -6
  10. package/modules/hosts/opencode/commands/design.md +34 -6
  11. package/modules/hosts/opencode/commands/maintain.md +71 -6
  12. package/modules/hosts/opencode/commands/new-task.md +59 -6
  13. package/modules/hosts/opencode/commands/plan.md +32 -6
  14. package/modules/hosts/opencode/commands/review.md +40 -6
  15. package/modules/hosts/opencode/commands/test.md +56 -8
  16. package/modules/hosts/trae/agents/implementer.md +44 -2
  17. package/modules/hosts/trae/agents/independent-reviewer.md +42 -1
  18. package/modules/hosts/trae/agents/ui-verifier.md +39 -3
  19. package/modules/hosts/trae/commands/wf-build.md +55 -7
  20. package/modules/hosts/trae/commands/wf-deploy.md +51 -6
  21. package/modules/hosts/trae/commands/wf-design.md +34 -6
  22. package/modules/hosts/trae/commands/wf-maintain.md +71 -6
  23. package/modules/hosts/trae/commands/wf-new-task.md +59 -6
  24. package/modules/hosts/trae/commands/wf-plan.md +32 -6
  25. package/modules/hosts/trae/commands/wf-review.md +40 -6
  26. package/modules/hosts/trae/commands/wf-test.md +56 -7
  27. package/modules/hosts/zcode/agents/implementer.md +45 -6
  28. package/modules/hosts/zcode/agents/independent-reviewer.md +42 -5
  29. package/modules/hosts/zcode/agents/ui-verifier.md +41 -5
  30. package/package.json +6 -3
  31. package/src/cli.mjs +15 -2
  32. package/src/doctor.mjs +140 -13
  33. package/src/init.test.mjs +25 -2
  34. package/src/pack.test.mjs +24 -0
  35. package/src/run-tests.mjs +4 -11
  36. package/src/sync-hosts.mjs +193 -0
  37. package/src/sync-hosts.test.mjs +170 -0
  38. package/src/sync.mjs +1 -0
  39. package/src/sync.test.mjs +10 -0
  40. package/templates/AGENTS.md +2 -2
  41. package/templates/_agents/commands/build.md +20 -0
  42. package/templates/_agents/commands/design.md +1 -0
  43. package/templates/_agents/commands/gate-checklist.md +91 -0
  44. package/templates/_agents/commands/maintain.md +1 -1
  45. package/templates/_agents/commands/plan.md +2 -1
  46. package/templates/_agents/commands/review.md +1 -0
  47. package/templates/_agents/commands/source-sync-check.md +103 -0
  48. package/templates/_agents/commands/sync-hosts.md +79 -0
  49. package/templates/_agents/commands/test.md +2 -1
  50. package/templates/_agents/notes/runtime-env.md +1 -1
  51. package/templates/_agents/rule-budgets.txt +1 -1
  52. package/templates/_agents/scripts/check-loop.mjs +517 -0
  53. package/templates/_agents/scripts/check-loop.sh +5 -808
  54. package/templates/_agents/scripts/check-loop.test.mjs +481 -0
  55. package/templates/_agents/scripts/doctor.test.mjs +298 -0
  56. package/templates/_agents/scripts/ensure-board.mjs +158 -0
  57. package/templates/_agents/scripts/fill-intent.mjs +59 -0
  58. package/templates/_agents/scripts/fill-intent.test.mjs +73 -0
  59. package/templates/_agents/scripts/fill-plan.mjs +60 -0
  60. package/templates/_agents/scripts/fill-plan.test.mjs +67 -0
  61. package/templates/_agents/scripts/fill-spec.mjs +52 -0
  62. package/templates/_agents/scripts/fill-spec.test.mjs +61 -0
  63. package/templates/_agents/scripts/gate-checklist.mjs +192 -0
  64. package/templates/_agents/scripts/gate-checklist.test.mjs +204 -0
  65. package/templates/_agents/scripts/gen-workflow-index.mjs +4 -2
  66. package/templates/_agents/scripts/kb-search.mjs +3 -1
  67. package/templates/_agents/scripts/source-sync-check.mjs +131 -0
  68. package/templates/_agents/scripts/source-sync-check.test.mjs +156 -0
  69. package/templates/_agents/scripts/verify.mjs +8 -10
  70. package/templates/_agents/scripts/verify.test.mjs +13 -1
  71. package/templates/_agents/scripts/wf-journal.mjs +158 -0
  72. package/templates/_agents/scripts/wf-journal.test.mjs +150 -0
  73. package/templates/_agents/scripts/workflow-board-server.mjs +18 -10
  74. package/templates/_agents/scripts/workflow-enums.mjs +68 -0
  75. package/templates/_agents/scripts/workflow-enums.test.mjs +110 -0
  76. package/templates/_agents/scripts/workflows-check.mjs +260 -0
  77. package/templates/_agents/scripts/workflows-check.test.mjs +267 -0
  78. package/templates/_agents/workflow-enums.txt +19 -0
  79. package/templates/_agents/workflows/_TEMPLATE.md +74 -0
  80. package/templates/_agents/workflows/pipeline-closing.md +29 -0
  81. package/templates/_agents/workflows/source-sync-repair.md +30 -0
  82. package/templates/_agents/workflows/steps//347/244/272/344/276/213-/351/203/250/347/275/262/351/252/214/350/257/201.md +10 -0
  83. package/templates/_agents/workflows//347/244/272/344/276/213-/345/271/266/350/241/214/345/256/236/347/216/260/350/257/204/345/256/241.md +16 -0
  84. package/templates/workflow/README.md +2 -2
  85. package/templates/_agents/cache/kb-index.json +0 -1
  86. package/templates/_agents/scripts/check-loop.test.sh +0 -1042
  87. package/templates/_agents/scripts/ensure-board.ps1 +0 -48
@@ -0,0 +1,131 @@
1
+ // flow-kit source-sync-check:装户面同步一致性检查(包源 templates/_agents/ vs 装副本 .agents/)
2
+ // 目的:包源改了 templates/_agents/ 但 .agents/ 装副本没刷新——这种"真空地带"sync.mjs 不扫、doctor.mjs §6.6 不扫
3
+ // 本工具扫 templates/_agents/ 下所有 .md / .mjs / .json / .txt(排除 cache/)vs .agents/ 对应路径
4
+ // 输出三类差异:缺失(包源有装副本无)/ 孤儿(包源无装副本有)/ 漂移(两者都有 sha 不一致)
5
+ // 排除装副本独有文件:kit.json / settings.json / hooks/commit-check.config.json(init 渲染产物,不属双源)
6
+ // 用法:node .agents/scripts/source-sync-check.mjs --diff
7
+ // node .agents/scripts/source-sync-check.mjs --json
8
+ // node .agents/scripts/source-sync-check.mjs --pkg-root <path> --target <path>
9
+ // 零依赖;B-b 决策「只报告不修复」
10
+ import fs from 'node:fs';
11
+ import path from 'node:path';
12
+ import { createHash } from 'node:crypto';
13
+
14
+ function fail(msg) { console.error('❌ ' + msg); process.exit(1); }
15
+
16
+ const VALID_EXTS = new Set(['.md', '.mjs', '.json', '.txt']);
17
+ // 装副本独有文件(init 渲染产物,不属双源结构)
18
+ const RENDER_OUTPUT_FILES = new Set(['kit.json', 'settings.json', 'hooks/commit-check.config.json']);
19
+ const TARGET_EXCLUDE = RENDER_OUTPUT_FILES; // 向后兼容别名
20
+
21
+ function sha256(p) {
22
+ try {
23
+ return createHash('sha256').update(fs.readFileSync(p)).digest('hex');
24
+ } catch {
25
+ return null;
26
+ }
27
+ }
28
+
29
+ // walkMd:递归扫目录,列出所有 validExt 文件(排除 cache/)
30
+ function walkMd(root) {
31
+ const out = [];
32
+ function walk(dir, relBase) {
33
+ if (!fs.existsSync(dir)) return;
34
+ for (const e of fs.readdirSync(dir, { withFileTypes: true })) {
35
+ const rel = relBase ? relBase + '/' + e.name : e.name;
36
+ const abs = path.join(dir, e.name);
37
+ // 排除 cache/ 目录(与 sync.mjs 既有约定一致)
38
+ if (rel === 'cache' || rel.startsWith('cache/')) continue;
39
+ if (e.isDirectory()) walk(abs, rel);
40
+ else if (VALID_EXTS.has(path.extname(e.name))) out.push(rel);
41
+ }
42
+ }
43
+ walk(root, '');
44
+ return out;
45
+ }
46
+
47
+ export function sourceSyncCheck({ pkgRoot, target }) {
48
+ const authorityRoot = path.join(pkgRoot, 'templates', '_agents');
49
+ const adapterRoot = path.join(target, '.agents');
50
+ const pkgFiles = walkMd(authorityRoot);
51
+ const tgtFiles = walkMd(adapterRoot).filter((f) => !TARGET_EXCLUDE.has(f));
52
+ const pkgSet = new Set(pkgFiles);
53
+ const tgtSet = new Set(tgtFiles);
54
+ const missing = []; // 包源有 / 装副本无
55
+ const orphan = []; // 包源无 / 装副本有
56
+ const drift = []; // 两者都有但 sha 不一致
57
+ for (const f of pkgFiles) {
58
+ if (!tgtSet.has(f)) { missing.push({ rel: f }); continue; }
59
+ const pkgSha = sha256(path.join(authorityRoot, f));
60
+ const tgtSha = sha256(path.join(adapterRoot, f));
61
+ if (pkgSha !== null && tgtSha !== null && pkgSha !== tgtSha) {
62
+ drift.push({ rel: f, pkgSha, tgtSha });
63
+ }
64
+ }
65
+ for (const f of tgtFiles) {
66
+ if (!pkgSet.has(f)) orphan.push({ rel: f });
67
+ }
68
+ return {
69
+ pkgRoot, target,
70
+ pkgCount: pkgFiles.length,
71
+ tgtCount: tgtFiles.length,
72
+ missing: missing.sort((a, b) => a.rel.localeCompare(b.rel)),
73
+ orphan: orphan.sort((a, b) => a.rel.localeCompare(b.rel)),
74
+ drift: drift.sort((a, b) => a.rel.localeCompare(b.rel)),
75
+ };
76
+ }
77
+
78
+ function printDiff(result) {
79
+ console.log('▶ flow-kit source-sync-check --diff');
80
+ console.log(' 包源 (templates/_agents/) ' + result.pkgCount + ' 份 | 装副本 (.agents/) ' + result.tgtCount + ' 份');
81
+ if (!result.missing.length && !result.orphan.length && !result.drift.length) {
82
+ console.log(' 无差异 ✅');
83
+ return;
84
+ }
85
+ if (result.missing.length) {
86
+ console.log(' 缺失(包源有 / 装副本无)' + result.missing.length + ':');
87
+ for (const m of result.missing) console.log(' - ' + m.rel);
88
+ }
89
+ if (result.orphan.length) {
90
+ console.log(' 孤儿(包源无 / 装副本有)' + result.orphan.length + ':');
91
+ for (const o of result.orphan) console.log(' - ' + o.rel);
92
+ }
93
+ if (result.drift.length) {
94
+ console.log(' 漂移(包源装副本都有但 sha 不一致)' + result.drift.length + ':');
95
+ for (const d of result.drift) console.log(' - ' + d.rel + ' pkg=' + d.pkgSha.slice(0, 8) + '… tgt=' + d.tgtSha.slice(0, 8) + '…');
96
+ }
97
+ }
98
+
99
+ const isMain = process.argv[1] && process.argv[1].endsWith('source-sync-check.mjs');
100
+ if (isMain) {
101
+ let jsonMode = false, diffMode = false;
102
+ let pkgRoot = null, target = null;
103
+ for (let i = 0; i < process.argv.length - 2; i++) {
104
+ const a = process.argv[2 + i];
105
+ if (a === '--json') jsonMode = true;
106
+ else if (a === '--diff') diffMode = true;
107
+ else if (a === '--pkg-root') pkgRoot = process.argv[2 + i + 1];
108
+ else if (a === '--target') target = process.argv[2 + i + 1];
109
+ }
110
+ if (!pkgRoot) pkgRoot = process.cwd();
111
+ if (!target) target = process.cwd();
112
+ const authorityRoot = path.join(pkgRoot, 'templates', '_agents');
113
+ const adapterRoot = path.join(target, '.agents');
114
+ if (!fs.existsSync(authorityRoot)) fail('找不到包源:' + authorityRoot);
115
+ if (!fs.existsSync(adapterRoot)) fail('找不到装副本:' + adapterRoot);
116
+ const result = sourceSyncCheck({ pkgRoot, target });
117
+ if (jsonMode) {
118
+ process.stdout.write(JSON.stringify({
119
+ pkgCount: result.pkgCount,
120
+ tgtCount: result.tgtCount,
121
+ missing: result.missing.map((m) => m.rel),
122
+ orphan: result.orphan.map((o) => o.rel),
123
+ drift: result.drift.map((d) => ({ rel: d.rel, pkgSha: d.pkgSha.slice(0, 8), tgtSha: d.tgtSha.slice(0, 8) })),
124
+ }, null, 2) + '\n');
125
+ } else {
126
+ printDiff(result);
127
+ }
128
+ // diff/json 模式总 exit 0;差异存在不视作错误(人类阅读用;B-b 决策「只报告不修复」)
129
+ process.exit(0);
130
+ }
131
+ export default sourceSyncCheck;
@@ -0,0 +1,156 @@
1
+ // source-sync-check.test.mjs — 装户面同步一致性检查工具测试(2026-09-25 source-sync-check)
2
+ // 方法:fixture mini 包源(templates/_agents/) + 装副本(.agents/),调 sourceSyncCheck() 拿三类差异
3
+ // + 实际仓库扫描做 baseline
4
+ // 判据:①fixture 三类齐全 ②cache/ 排除 ③kit.json/settings.json 排除 ④sha 一致 ⑤baseline ≥ 100 ⑥JSON 可解析
5
+ // 用法:node .agents/scripts/source-sync-check.test.mjs
6
+ import fs from 'node:fs';
7
+ import path from 'node:path';
8
+ import os from 'node:os';
9
+ import { fileURLToPath } from 'node:url';
10
+ import { sourceSyncCheck } from './source-sync-check.mjs';
11
+
12
+ let pass = 0, failCount = 0;
13
+ function check(name, cond, detail = '') {
14
+ if (cond) { pass++; console.log('PASS ' + name); }
15
+ else { failCount++; console.log('FAIL ' + name + (detail ? '——' + detail : '')); }
16
+ }
17
+
18
+ const SRC_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..', '..');
19
+
20
+ // ---- fixture:mini 包源 + 装副本,3 类差异各造 1 个 ----
21
+ function mkFixture() {
22
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), 'fk-ssc-'));
23
+ const pkgRoot = path.join(root, 'pkg');
24
+ const target = path.join(root, 'target');
25
+ const pkgBase = path.join(pkgRoot, 'templates', '_agents');
26
+ const tgtBase = path.join(target, '.agents');
27
+ fs.mkdirSync(path.join(pkgBase, 'commands'), { recursive: true });
28
+ fs.mkdirSync(path.join(pkgBase, 'scripts'), { recursive: true });
29
+ fs.mkdirSync(path.join(pkgBase, 'cache'), { recursive: true }); // 应排除
30
+ fs.mkdirSync(path.join(tgtBase, 'commands'), { recursive: true });
31
+ fs.mkdirSync(path.join(tgtBase, 'scripts'), { recursive: true });
32
+ fs.mkdirSync(path.join(tgtBase, 'hooks'), { recursive: true }); // 孤儿
33
+ fs.mkdirSync(path.join(tgtBase, 'cache'), { recursive: true }); // 装副本 cache 也应排除
34
+ // 一致文件(cmdA.md):包源 + 装副本 同内容
35
+ fs.writeFileSync(path.join(pkgBase, 'commands', 'cmdA.md'), '# cmdA v1\n');
36
+ fs.writeFileSync(path.join(tgtBase, 'commands', 'cmdA.md'), '# cmdA v1\n');
37
+ // 缺失(cmdB.md):包源有 / 装副本无
38
+ fs.writeFileSync(path.join(pkgBase, 'commands', 'cmdB.md'), '# cmdB v1\n');
39
+ // 孤儿(orphan-real.json):装副本 hooks/ 有 / 包源无(普通孤儿,应被报告)
40
+ fs.writeFileSync(path.join(tgtBase, 'hooks', 'orphan-real.json'), '{}\n');
41
+ // 白名单孤儿(commit-check.config.json):装副本 hooks/ 有 / 包源无,但 init 渲染产物不应报孤儿
42
+ fs.writeFileSync(path.join(tgtBase, 'hooks', 'commit-check.config.json'), '{}\n');
43
+ // 装副本独有(应排除):kit.json / settings.json
44
+ fs.writeFileSync(path.join(tgtBase, 'kit.json'), '{}\n');
45
+ fs.writeFileSync(path.join(tgtBase, 'settings.json'), '{}\n');
46
+ // 漂移(scriptX.mjs):包源 v1 / 装副本 v2
47
+ fs.writeFileSync(path.join(pkgBase, 'scripts', 'scriptX.mjs'), 'export const X = 1;\n');
48
+ fs.writeFileSync(path.join(tgtBase, 'scripts', 'scriptX.mjs'), 'export const X = 2;\n');
49
+ // cache/ 排除:包源 + 装副本 都有,但不应被计入
50
+ fs.writeFileSync(path.join(pkgBase, 'cache', 'foo.md'), 'cache-pkg\n');
51
+ fs.writeFileSync(path.join(tgtBase, 'cache', 'foo.md'), 'cache-tgt\n');
52
+ return { pkgRoot, target };
53
+ }
54
+
55
+ // ---- 场景 1:fixture 三类差异齐全(缺失 1 / 孤儿 1 / 漂移 1)----
56
+ {
57
+ const { pkgRoot, target } = mkFixture();
58
+ const r = sourceSyncCheck({ pkgRoot, target });
59
+ check('S1 fixture 缺失 1(cmdB.md)', r.missing.length === 1 && r.missing[0].rel === 'commands/cmdB.md', JSON.stringify(r.missing));
60
+ check('S1 fixture 孤儿 1(orphan-real.json)', r.orphan.length === 1 && r.orphan[0].rel === 'hooks/orphan-real.json', JSON.stringify(r.orphan));
61
+ check('S1 fixture 漂移 1(scriptX.mjs)', r.drift.length === 1 && r.drift[0].rel === 'scripts/scriptX.mjs', JSON.stringify(r.drift));
62
+ check('S1 fixture 白名单项(commit-check.config.json)不在孤儿', !r.orphan.some((o) => o.rel === 'hooks/commit-check.config.json'));
63
+ }
64
+
65
+ // ---- 场景 2:cache/ 目录排除 ----
66
+ {
67
+ const { pkgRoot, target } = mkFixture();
68
+ const r = sourceSyncCheck({ pkgRoot, target });
69
+ check('S2 cache/ 排除(缺失/孤儿/漂移不含 cache/foo.md)',
70
+ !r.missing.some((m) => m.rel.includes('cache/')) &&
71
+ !r.orphan.some((o) => o.rel.includes('cache/')) &&
72
+ !r.drift.some((d) => d.rel.includes('cache/')));
73
+ }
74
+
75
+ // ---- 场景 3:装副本独有(kit.json / settings.json)排除 ----
76
+ {
77
+ const { pkgRoot, target } = mkFixture();
78
+ const r = sourceSyncCheck({ pkgRoot, target });
79
+ check('S3 装副本独有(kit.json / settings.json)排除',
80
+ !r.orphan.some((o) => o.rel === 'kit.json' || o.rel === 'settings.json'));
81
+ }
82
+
83
+ // ---- 场景 4:sha 一致文件不出现在漂移列表 ----
84
+ {
85
+ const { pkgRoot, target } = mkFixture();
86
+ const r = sourceSyncCheck({ pkgRoot, target });
87
+ check('S4 一致文件 cmdA.md 不在漂移', !r.drift.some((d) => d.rel === 'commands/cmdA.md'));
88
+ }
89
+
90
+ // ---- 场景 5:实际仓库扫描 baseline(包源 ≥ 30 份 / 装副本 ≥ 30 份)----
91
+ {
92
+ const pkgRoot = SRC_ROOT;
93
+ const target = SRC_ROOT;
94
+ const r = sourceSyncCheck({ pkgRoot, target });
95
+ check('S5 实际仓库 包源 ≥ 30 份', r.pkgCount >= 30, '实际 ' + r.pkgCount);
96
+ check('S5 实际仓库 装副本 ≥ 30 份', r.tgtCount >= 30, '实际 ' + r.tgtCount);
97
+ // 装副本 kit.json / settings.json 不应在孤儿
98
+ check('S5 实际仓库 装副本独有(kit.json / settings.json)排除',
99
+ !r.orphan.some((o) => o.rel === 'kit.json' || o.rel === 'settings.json'));
100
+ }
101
+
102
+ // ---- 场景 6:--json 输出可解析(CLI 子流程)----
103
+ {
104
+ const { pkgRoot, target } = mkFixture();
105
+ // 直接调 sourceSyncCheck + 序列化模拟 --json 输出
106
+ const r = sourceSyncCheck({ pkgRoot, target });
107
+ const jsonOut = JSON.stringify({
108
+ pkgCount: r.pkgCount, tgtCount: r.tgtCount,
109
+ missing: r.missing.map((m) => m.rel),
110
+ orphan: r.orphan.map((o) => o.rel),
111
+ drift: r.drift.map((d) => ({ rel: d.rel, pkgSha: d.pkgSha.slice(0, 8), tgtSha: d.tgtSha.slice(0, 8) })),
112
+ }, null, 2);
113
+ let parsed = null;
114
+ try { parsed = JSON.parse(jsonOut); } catch {}
115
+ check('S6 --json 输出可解析', parsed && typeof parsed === 'object' && Array.isArray(parsed.missing));
116
+ }
117
+
118
+ // ---- 场景 8:白名单 RHO 文件过滤(hooks/commit-check.config.json + kit.json + settings.json 都不报孤儿)----
119
+ {
120
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), 'fk-ssc-rho-'));
121
+ const pkgRoot = path.join(root, 'pkg');
122
+ const target = path.join(root, 'target');
123
+ const pkgBase = path.join(pkgRoot, 'templates', '_agents');
124
+ const tgtBase = path.join(target, '.agents');
125
+ fs.mkdirSync(pkgBase, { recursive: true });
126
+ fs.mkdirSync(path.join(tgtBase, 'hooks'), { recursive: true });
127
+ fs.mkdirSync(tgtBase, { recursive: true });
128
+ // 3 个白名单项都装副本独有
129
+ fs.writeFileSync(path.join(tgtBase, 'hooks', 'commit-check.config.json'), '{}\n');
130
+ fs.writeFileSync(path.join(tgtBase, 'kit.json'), '{}\n');
131
+ fs.writeFileSync(path.join(tgtBase, 'settings.json'), '{}\n');
132
+ // 1 个非白名单孤儿作对照(应被报告)
133
+ fs.writeFileSync(path.join(tgtBase, 'hooks', 'real-orphan.json'), '{}\n');
134
+ const r = sourceSyncCheck({ pkgRoot, target });
135
+ check('S8 白名单 3 项(commit-check.config.json / kit.json / settings.json)不报孤儿',
136
+ !r.orphan.some((o) => o.rel === 'hooks/commit-check.config.json' || o.rel === 'kit.json' || o.rel === 'settings.json'));
137
+ check('S8 非白名单孤儿(real-orphan.json)仍报孤儿',
138
+ r.orphan.length === 1 && r.orphan[0].rel === 'hooks/real-orphan.json', JSON.stringify(r.orphan));
139
+ }
140
+
141
+ // ---- 场景 9:实际仓库 hooks/commit-check.config.json 不在孤儿 ----
142
+ {
143
+ const r = sourceSyncCheck({ pkgRoot: SRC_ROOT, target: SRC_ROOT });
144
+ check('S9 实际仓库 hooks/commit-check.config.json 不在孤儿',
145
+ !r.orphan.some((o) => o.rel === 'hooks/commit-check.config.json'), JSON.stringify(r.orphan));
146
+ }
147
+
148
+ // ---- 场景 7:空目录 / 不存在的目录不崩 ----
149
+ {
150
+ const empty = fs.mkdtempSync(path.join(os.tmpdir(), 'fk-ssc-empty-'));
151
+ const r = sourceSyncCheck({ pkgRoot: empty, target: empty });
152
+ check('S7 空目录 fixture 不崩(0/0)', r.pkgCount === 0 && r.tgtCount === 0);
153
+ }
154
+
155
+ console.log('\n合计: PASS ' + pass + ' / FAIL ' + failCount);
156
+ process.exit(failCount ? 1 : 0);
@@ -1,9 +1,10 @@
1
1
  #!/usr/bin/env node
2
- // verify.mjs — 关单固定编排(2026-09-24 intent closeout-verify-script)
3
- // 用途:关单前一键过门——按写死顺序执行 npm test → check-loop.sh,逐项输出 pass/fail。
2
+ // verify.mjs — 关单固定编排(2026-09-24 intent closeout-verify-script;2026-09-26 check-loop-node 第 2 步直连 node)
3
+ // 用途:关单前一键过门——按写死顺序执行 npm test → check-loop,逐项输出 pass/fail。
4
4
  // 语义:只编排不裁决——两条命令都是既有门禁原样跑,本脚本不新增校验、不读配置、无状态;
5
- // 任一步非零即 fail-fast 退出 1,失败原因经 stdio inherit 原样透传不吞;
6
- // 缺 sh 环境 fail-closed 明确报错(关单门不可跳过,不像 run-tests 那样跳过并提示)。
5
+ // 任一步非零即 fail-fast 退出 1,失败原因经 stdio inherit 原样透传不吞。
6
+ // check-loop 为 node 实现(经 check-loop.mjs 直跑——node 即本脚本运行时必然可用,
7
+ // 旧「缺 sh 环境 fail-closed」分支随 sh 依赖消失退役)。
7
8
  // 测试:node templates/_agents/scripts/verify.test.mjs(--test-cmd 注入假命令 + CHECK_LOOP_ROOT 注入夹具,不触真实 npm test 与 workflow)
8
9
  // 用法:node .agents/scripts/verify.mjs [--test-cmd "<shell 命令>"](--test-cmd 仅供测试注入)
9
10
  import { spawnSync } from 'node:child_process';
@@ -13,7 +14,7 @@ import { fileURLToPath } from 'node:url';
13
14
  const SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url));
14
15
  const args = process.argv.slice(2);
15
16
  const testCmd = args.includes('--test-cmd') ? args[args.indexOf('--test-cmd') + 1] : 'npm test';
16
- const checkLoop = path.join(SCRIPT_DIR, 'check-loop.sh');
17
+ const checkLoop = path.join(SCRIPT_DIR, 'check-loop.mjs');
17
18
 
18
19
  console.log(`[verify] ▶ 1/2 ${testCmd}`);
19
20
  const t = spawnSync(testCmd, { shell: true, stdio: 'inherit' });
@@ -24,12 +25,9 @@ if (t.status !== 0) {
24
25
  console.log('[verify] ✅ 1/2 测试通过');
25
26
 
26
27
  console.log('[verify] ▶ 2/2 check-loop');
27
- const c = spawnSync('sh', [checkLoop], { stdio: 'inherit' });
28
+ const c = spawnSync(process.execPath, [checkLoop], { stdio: 'inherit' });
28
29
  if (c.status !== 0) {
29
- const hint = c.error?.code === 'ENOENT'
30
- ? '环境无 sh(装 Git Bash / WSL)——关单门不可跳过,不静默放行'
31
- : '按上方输出修复后原路重跑';
32
- console.error(`[verify] ❌ 2/2 check-loop 未通过(exit ${c.status ?? c.error?.code})——${hint}`);
30
+ console.error(`[verify] ❌ 2/2 check-loop 未通过(exit ${c.status ?? c.error?.code})——按上方输出修复后原路重跑`);
33
31
  process.exit(1);
34
32
  }
35
33
  console.log('[verify] ✅ 2/2 闭环校验通过');
@@ -19,10 +19,22 @@ const check = (desc, cond, detail = '') => {
19
19
  else { fail++; console.log(`FAIL ${desc}${detail ? `\n${detail}` : ''}`); }
20
20
  };
21
21
 
22
- // 空闭环夹具(四目录齐全、无文档 → check-loop 无断档)
22
+ // 空闭环夹具(四目录齐全、无文档 → check-loop 无断档;枚举单源随 fixture 内联——check-loop 启动段 fail-loud 要求)
23
23
  const mkfix = () => {
24
24
  const d = fs.mkdtempSync(path.join(os.tmpdir(), 'verify-fix-'));
25
25
  for (const s of ['intents', 'specs', 'plans', 'incidents']) fs.mkdirSync(path.join(d, 'workflow', s), { recursive: true });
26
+ fs.mkdirSync(path.join(d, '.agents'), { recursive: true });
27
+ fs.writeFileSync(path.join(d, '.agents', 'workflow-enums.txt'), [
28
+ 'doc.status.all=draft approved done superseded cancelled',
29
+ 'doc.status.confirmed=approved done superseded cancelled',
30
+ 'doc.status.active=draft approved',
31
+ 'doc.status.terminal=done superseded cancelled',
32
+ 'doc.status.abandoned=superseded cancelled',
33
+ 'incident.status.all=open fixed closed',
34
+ 'incident.status.active=open',
35
+ 'level.all=L0 L1 L2 L3',
36
+ '',
37
+ ].join('\n'), 'utf8');
26
38
  return d;
27
39
  };
28
40
  // 配对断裂夹具(L1 intent 无同名 plan → hard-block)
@@ -0,0 +1,158 @@
1
+ // wf-journal — 编排执行 run journal(2026-09-26 orchestration-journal-human,审查改进方向 4)
2
+ // 用途:宿主 AI 按 .agents/workflows/_TEMPLATE.md 纪律,每个 stage 每次尝试出结果(pass/fail/blocked)
3
+ // 记一行 JSONL;中止后重跑先看 status——已 pass 不重跑,从就绪层续(断点续跑与重试计量)。
4
+ // 存储:.agents/cache/orchestration-runs.jsonl(本地运行态缓存,已 gitignore,可丢弃——丢失仅失去
5
+ // 续跑记忆,不影响任何门禁;status 对空 journal 天然全量输出不误报)。
6
+ // 行 schema:{ ts, run, wf, stage, form?, status, attempt, note? }
7
+ // run = <wf>@<YYYYMMDD-HHMM>;同 run 同 stage 多行按末次状态生效(重试后过 = pass),attempt 取最大。
8
+ // 语义口径(与 _TEMPLATE.md 四形态一致;stages 表解析复用 workflows-check.mjs 的 parseStages 单源)。
9
+ // 用法:
10
+ // node .agents/scripts/wf-journal.mjs add --wf <编排名> --stage <id> --status <pass|fail|blocked>
11
+ // [--form role|step|gate|human] [--attempt N] [--note <文本>] [--run <runId>] [--new-run]
12
+ // [--journal <文件>](默认 .agents/cache/orchestration-runs.jsonl,按 cwd)
13
+ // node .agents/scripts/wf-journal.mjs status --wf <编排名> [--run <runId>] [--journal <文件>] [--wf-root <目录>]
14
+ // 测试:node templates/_agents/scripts/wf-journal.test.mjs(fixture + 真实仓库场景)
15
+ import fs from 'node:fs';
16
+ import path from 'node:path';
17
+ import { parseStages } from './workflows-check.mjs';
18
+
19
+ function fail(msg) { console.error('❌ ' + msg); process.exit(1); }
20
+
21
+ const DEFAULT_JOURNAL = () => path.join(process.cwd(), '.agents', 'cache', 'orchestration-runs.jsonl');
22
+ const DEFAULT_WF_ROOT = () => path.join(process.cwd(), '.agents', 'workflows');
23
+ const STATUSES = ['pass', 'fail', 'blocked'];
24
+ const FORMS = ['role', 'step', 'gate', 'human'];
25
+
26
+ // 读 journal 原始行(文件不存在/空 → [];目录不存在自动可建由 add 处理)
27
+ export function readJournalLines(journalFile) {
28
+ try {
29
+ return fs.readFileSync(journalFile, 'utf8').split(/\r?\n/).filter((l) => l.trim());
30
+ } catch {
31
+ return [];
32
+ }
33
+ }
34
+
35
+ // replayRun:重放一个 run 的记录 → { state: {stage: {status, attempt, form, note, ts}}, total, bad }
36
+ // 末次状态生效;attempt 取出现过的最大值;坏行跳过(计数返回,调用方决定告警口径)
37
+ export function replayRun(rawLines, run) {
38
+ const state = {};
39
+ let total = 0;
40
+ let bad = 0;
41
+ for (const line of rawLines) {
42
+ let j;
43
+ try { j = JSON.parse(line); } catch { bad++; continue; }
44
+ if (!j || typeof j !== 'object' || j.run !== run) continue;
45
+ if (!j.stage || !STATUSES.includes(j.status)) { bad++; continue; }
46
+ total++;
47
+ const prev = state[j.stage];
48
+ state[j.stage] = {
49
+ status: j.status,
50
+ attempt: Math.max(prev?.attempt ?? 0, Number(j.attempt) || 1),
51
+ ...(j.form ? { form: j.form } : {}),
52
+ ...(j.note ? { note: j.note } : {}),
53
+ ts: j.ts,
54
+ };
55
+ }
56
+ return { state, total, bad };
57
+ }
58
+
59
+ // latestRun:journal 中该 wf 最后一条的 run id(无记录 → null)
60
+ export function latestRun(rawLines, wf) {
61
+ let run = null;
62
+ for (const line of rawLines) {
63
+ try {
64
+ const j = JSON.parse(line);
65
+ if (j && j.wf === wf && j.run) run = j.run;
66
+ } catch { /* 坏行不参与 run 推断 */ }
67
+ }
68
+ return run;
69
+ }
70
+
71
+ // planStages:stages(parseStages 产物)× 状态 → { passed, ready, pending, blocked }
72
+ // passed = 末次 pass;ready = 依赖全 pass 且自身未 pass(末次 fail 标注重试就绪);blocked = 自身末次 blocked;pending = 其余
73
+ export function planStages(stages, state) {
74
+ const passed = [];
75
+ const ready = [];
76
+ const pending = [];
77
+ const blocked = [];
78
+ const passedSet = new Set(stages.filter((s) => state[s.id]?.status === 'pass').map((s) => s.id));
79
+ for (const s of stages) {
80
+ const st = state[s.id];
81
+ if (st?.status === 'pass') { passed.push({ id: s.id }); continue; }
82
+ if (st?.status === 'blocked') { blocked.push({ id: s.id, note: st.note || '' }); continue; }
83
+ const deps = s.after ? s.after.split(/[,,、\s]+/).filter(Boolean) : [];
84
+ if (deps.every((d) => passedSet.has(d))) {
85
+ ready.push({ id: s.id, ...(st?.status === 'fail' ? { retry: st.attempt } : {}) });
86
+ } else {
87
+ pending.push({ id: s.id, ...(deps.some((d) => state[d] && state[d].status !== 'pass') && (state[deps.find((d) => state[d] && state[d].status !== 'pass')]?.status === 'blocked') ? { depBlocked: true } : {}) });
88
+ }
89
+ }
90
+ return { passed, ready, pending, blocked };
91
+ }
92
+
93
+ const pad = (n) => String(n).padStart(2, '0');
94
+ const runIdOf = (wf, d = new Date()) =>
95
+ `${wf}@${d.getFullYear()}${pad(d.getMonth() + 1)}${pad(d.getDate())}-${pad(d.getHours())}${pad(d.getMinutes())}`;
96
+
97
+ function parseArgs(argv) {
98
+ const out = { cmd: argv[0] };
99
+ for (let i = 1; i < argv.length; i++) {
100
+ const a = argv[i];
101
+ if (a.startsWith('--')) out[a.slice(2)] = argv[++i] ?? true;
102
+ else fail(`未知参数:${a}(用法见文件头注释)`);
103
+ }
104
+ return out;
105
+ }
106
+
107
+ const isMain = process.argv[1] && process.argv[1].endsWith('wf-journal.mjs');
108
+ if (isMain) {
109
+ const a = parseArgs(process.argv.slice(2));
110
+ const journalFile = a.journal || DEFAULT_JOURNAL();
111
+ if (a.cmd === 'add') {
112
+ if (!a.wf || !a.stage || !a.status) fail('add 必填:--wf / --stage / --status(pass|fail|blocked)');
113
+ if (!STATUSES.includes(a.status)) fail(`--status 须为 ${STATUSES.join('|')}(现 ${a.status})`);
114
+ if (a.form && !FORMS.includes(a.form)) fail(`--form 须为 ${FORMS.join('|')}(现 ${a.form})`);
115
+ const lines = readJournalLines(journalFile);
116
+ const run = a.run || (a['new-run'] ? runIdOf(a.wf) : latestRun(lines, a.wf) || runIdOf(a.wf));
117
+ const attempt = a.attempt
118
+ ? Number(a.attempt)
119
+ : lines.filter((l) => { try { const j = JSON.parse(l); return j.run === run && j.stage === a.stage; } catch { return false; } }).length + 1;
120
+ const row = {
121
+ ts: new Date().toISOString(), run, wf: a.wf, stage: a.stage,
122
+ status: a.status, attempt,
123
+ ...(a.form ? { form: a.form } : {}), ...(a.note ? { note: a.note } : {}),
124
+ };
125
+ fs.mkdirSync(path.dirname(journalFile), { recursive: true });
126
+ fs.appendFileSync(journalFile, JSON.stringify(row) + '\n');
127
+ console.log(`✅ journal += ${a.wf}/${a.stage} ${a.status}(attempt ${attempt},run ${run})`);
128
+ process.exit(0);
129
+ }
130
+ if (a.cmd === 'status') {
131
+ if (!a.wf) fail('status 必填:--wf(编排名)');
132
+ const wfRoot = a['wf-root'] || DEFAULT_WF_ROOT();
133
+ const wfFile = path.join(wfRoot, `${a.wf}.md`);
134
+ if (!fs.existsSync(wfFile)) fail(`找不到编排脚本:${wfFile}`);
135
+ const stages = parseStages(fs.readFileSync(wfFile, 'utf8'));
136
+ if (!stages) fail(`编排脚本缺 stages 表:${wfFile}`);
137
+ const lines = readJournalLines(journalFile);
138
+ const run = a.run || latestRun(lines, a.wf);
139
+ console.log(`▶ wf-journal status — ${a.wf}${run ? ` @ ${run}` : '(无 journal 记录,按首轮输出)'}`);
140
+ if (!run) {
141
+ const { ready, pending } = planStages(stages, {});
142
+ console.log(` ▶ 就绪(第 0 层,${ready.length}):${ready.map((s) => s.id).join('、') || '无'}`);
143
+ console.log(` ⏸ 待定(${pending.length}):${pending.map((s) => s.id).join('、') || '无'}`);
144
+ process.exit(0);
145
+ }
146
+ const { state, total, bad } = replayRun(lines, run);
147
+ if (bad) console.error(`⚠️ journal 坏行 ${bad} 条已跳过(缓存可丢弃,不影响门禁)`);
148
+ const plan = planStages(stages, state);
149
+ console.log(` (${total} 条记录)`);
150
+ console.log(` ✅ 已完成(${plan.passed.length}):${plan.passed.map((s) => s.id).join('、') || '无'}`);
151
+ console.log(` ▶ 就绪(${plan.ready.length}):${plan.ready.map((s) => (s.retry ? `${s.id}(重试就绪×${s.retry})` : s.id)).join('、') || '无'}`);
152
+ console.log(` ⏸ 待定(${plan.pending.length}):${plan.pending.map((s) => s.id + (s.depBlocked ? '(依赖中止)' : '')).join('、') || '无'}`);
153
+ console.log(` ⛔ 中止(${plan.blocked.length}):${plan.blocked.map((s) => `${s.id}${s.note ? `(${s.note})` : ''}`).join('、') || '无'}`);
154
+ process.exit(0);
155
+ }
156
+ fail(`未知命令:${a.cmd ?? ''}(add | status,用法见文件头注释)`);
157
+ }
158
+ export default { replayRun, planStages, latestRun, readJournalLines };