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.
- package/README.md +93 -69
- package/modules/gates/README.md +24 -0
- package/modules/gates/generated-readonly/README.md +13 -0
- package/modules/gates/generated-readonly/check-generated.mjs +61 -0
- package/modules/gates/generated-readonly/local-pre-commit.line +1 -0
- package/modules/gates/node-layer/README.md +9 -0
- package/modules/gates/node-layer/check-node-layer.mjs +29 -0
- package/modules/gates/node-layer/local-pre-commit.line +1 -0
- package/modules/gates/py-import/README.md +9 -0
- package/modules/gates/py-import/check-py-import.mjs +30 -0
- package/modules/gates/py-import/local-pre-commit.line +1 -0
- package/modules/hosts/claude/agents/implementer.md +52 -0
- package/modules/hosts/claude/agents/independent-reviewer.md +49 -0
- package/modules/hosts/claude/agents/ui-verifier.md +48 -0
- package/modules/hosts/claude/commands/wf-build.md +79 -0
- package/modules/hosts/claude/commands/wf-deploy.md +57 -0
- package/modules/hosts/claude/commands/wf-design.md +42 -0
- package/modules/hosts/claude/commands/wf-gate-checklist.md +77 -0
- package/modules/hosts/claude/commands/wf-maintain.md +76 -0
- package/modules/hosts/claude/commands/wf-new-task.md +65 -0
- package/modules/hosts/claude/commands/wf-plan.md +41 -0
- package/modules/hosts/claude/commands/wf-review.md +47 -0
- package/modules/hosts/claude/commands/wf-source-sync-check.md +89 -0
- package/modules/hosts/claude/commands/wf-sync-hosts.md +66 -0
- package/modules/hosts/claude/commands/wf-test.md +66 -0
- package/modules/hosts/codex/agents/implementer.md +52 -0
- package/modules/hosts/codex/agents/independent-reviewer.md +49 -0
- package/modules/hosts/codex/agents/ui-verifier.md +48 -0
- package/modules/hosts/codex/commands/wf-build.md +79 -0
- package/modules/hosts/codex/commands/wf-deploy.md +57 -0
- package/modules/hosts/codex/commands/wf-design.md +42 -0
- package/modules/hosts/codex/commands/wf-gate-checklist.md +77 -0
- package/modules/hosts/codex/commands/wf-maintain.md +76 -0
- package/modules/hosts/codex/commands/wf-new-task.md +65 -0
- package/modules/hosts/codex/commands/wf-plan.md +41 -0
- package/modules/hosts/codex/commands/wf-review.md +47 -0
- package/modules/hosts/codex/commands/wf-source-sync-check.md +89 -0
- package/modules/hosts/codex/commands/wf-sync-hosts.md +66 -0
- package/modules/hosts/codex/commands/wf-test.md +66 -0
- package/modules/hosts/codex/skills/flow-kit/SKILL.md +14 -0
- package/modules/hosts/cursor/agents/implementer.md +52 -0
- package/modules/hosts/cursor/agents/independent-reviewer.md +49 -0
- package/modules/hosts/cursor/agents/ui-verifier.md +48 -0
- package/modules/hosts/cursor/commands/wf-build.md +79 -0
- package/modules/hosts/cursor/commands/wf-deploy.md +57 -0
- package/modules/hosts/cursor/commands/wf-design.md +42 -0
- package/modules/hosts/cursor/commands/wf-gate-checklist.md +77 -0
- package/modules/hosts/cursor/commands/wf-maintain.md +76 -0
- package/modules/hosts/cursor/commands/wf-new-task.md +65 -0
- package/modules/hosts/cursor/commands/wf-plan.md +41 -0
- package/modules/hosts/cursor/commands/wf-review.md +47 -0
- package/modules/hosts/cursor/commands/wf-source-sync-check.md +89 -0
- package/modules/hosts/cursor/commands/wf-sync-hosts.md +66 -0
- package/modules/hosts/cursor/commands/wf-test.md +66 -0
- package/modules/hosts/omp/agents/implementer.md +1 -1
- package/modules/hosts/omp/agents/independent-reviewer.md +1 -1
- package/modules/hosts/omp/agents/ui-verifier.md +1 -1
- package/modules/hosts/opencode/agents/implementer.md +1 -1
- package/modules/hosts/opencode/agents/independent-reviewer.md +1 -1
- package/modules/hosts/opencode/agents/ui-verifier.md +1 -1
- package/modules/hosts/opencode/commands/wf-build.md +3 -4
- package/modules/hosts/opencode/commands/wf-design.md +2 -2
- package/modules/hosts/opencode/commands/wf-maintain.md +1 -1
- package/modules/hosts/opencode/commands/wf-plan.md +3 -3
- package/modules/hosts/opencode/commands/wf-review.md +1 -1
- package/modules/hosts/opencode/commands/wf-source-sync-check.md +1 -1
- package/modules/hosts/opencode/commands/wf-test.md +3 -1
- package/modules/hosts/trae/agents/implementer.md +1 -1
- package/modules/hosts/trae/agents/independent-reviewer.md +1 -1
- package/modules/hosts/trae/agents/ui-verifier.md +1 -1
- package/modules/hosts/trae/commands/wf-build.md +3 -4
- package/modules/hosts/trae/commands/wf-design.md +2 -2
- package/modules/hosts/trae/commands/wf-maintain.md +1 -1
- package/modules/hosts/trae/commands/wf-plan.md +3 -3
- package/modules/hosts/trae/commands/wf-review.md +1 -1
- package/modules/hosts/trae/commands/wf-source-sync-check.md +1 -1
- package/modules/hosts/trae/commands/wf-test.md +3 -1
- package/modules/hosts/zcode/agents/implementer.md +1 -1
- package/modules/hosts/zcode/agents/independent-reviewer.md +1 -1
- package/modules/hosts/zcode/agents/ui-verifier.md +1 -1
- package/package.json +45 -45
- package/src/cli.mjs +16 -4
- package/src/doctor.mjs +49 -1
- package/src/fresh-init.test.mjs +36 -0
- package/src/gates.test.mjs +126 -0
- package/src/init.mjs +8 -1
- package/src/init.test.mjs +7 -1
- package/src/profiles.mjs +10 -4
- package/src/run-tests.mjs +3 -0
- package/src/stack-profile.test.mjs +89 -0
- package/src/sync-hosts.test.mjs +26 -6
- package/templates/AGENTS.md +4 -4
- package/templates/_agents/board/index.html +3 -3
- package/templates/_agents/commands/build.md +4 -5
- package/templates/_agents/commands/design.md +4 -4
- package/templates/_agents/commands/maintain.md +1 -1
- package/templates/_agents/commands/new-task.md +13 -2
- package/templates/_agents/commands/plan.md +4 -4
- package/templates/_agents/commands/review.md +1 -1
- package/templates/_agents/commands/source-sync-check.md +1 -1
- package/templates/_agents/commands/test.md +4 -2
- package/templates/_agents/hooks/commit-check.cjs +45 -24
- package/templates/_agents/lane-surfaces.txt +14 -0
- package/templates/_agents/roles/implementer.md +1 -1
- package/templates/_agents/roles/independent-reviewer.md +1 -1
- package/templates/_agents/roles/ui-verifier.md +1 -1
- package/templates/_agents/scripts/check-lane-surface.mjs +164 -0
- package/templates/_agents/scripts/check-lane-surface.test.mjs +200 -0
- package/templates/_agents/scripts/check-loop-rev.test.mjs +337 -0
- package/templates/_agents/scripts/check-loop.mjs +460 -268
- package/templates/_agents/scripts/check-loop.test.mjs +677 -17
- package/templates/_agents/scripts/check-metric-claims.mjs +275 -0
- package/templates/_agents/scripts/commit-check-trigger.test.mjs +64 -1
- package/templates/_agents/scripts/confirm-doc.mjs +159 -15
- package/templates/_agents/scripts/confirm-doc.test.mjs +362 -10
- package/templates/_agents/scripts/doctor.test.mjs +102 -1
- package/templates/_agents/scripts/fill-intent.mjs +8 -7
- package/templates/_agents/scripts/fill-intent.test.mjs +12 -3
- package/templates/_agents/scripts/fill-plan.mjs +27 -11
- package/templates/_agents/scripts/fill-plan.test.mjs +148 -8
- package/templates/_agents/scripts/fill-spec.mjs +18 -3
- package/templates/_agents/scripts/fill-spec.test.mjs +198 -1
- package/templates/_agents/scripts/gate-checklist.mjs +4 -1
- package/templates/_agents/scripts/gen-workflow-index.mjs +6 -2
- package/templates/_agents/scripts/gen-workflow-index.test.mjs +12 -0
- package/templates/_agents/scripts/policy.mjs +46 -0
- package/templates/_agents/scripts/pre-push.test.mjs +82 -0
- package/templates/_agents/scripts/solidify-task.mjs +107 -0
- package/templates/_agents/scripts/solidify-task.test.mjs +126 -0
- package/templates/_agents/scripts/stage-gates.mjs +256 -0
- package/templates/_agents/scripts/trust-mode.mjs +67 -0
- package/templates/_agents/scripts/wf-journal.mjs +71 -2
- package/templates/_agents/scripts/wf-journal.test.mjs +66 -0
- package/templates/_agents/scripts/workflow-board-server.mjs +18 -15
- package/templates/_agents/scripts/workflow-board-server.test.mjs +30 -1
- package/templates/_agents/workflows/pipeline-closing.md +1 -1
- package/templates/_githooks/pre-commit +33 -2
- package/templates/_githooks/pre-push +58 -10
- package/templates/workflow/README.md +22 -3
- package/templates/workflow/incidents/_TEMPLATE.md +4 -0
- package/templates/workflow/intents/_TEMPLATE.md +1 -0
|
@@ -76,7 +76,7 @@ flow-kit doctor # 10/0/0
|
|
|
76
76
|
- 不改 managed 台账跟踪逻辑(`sync.mjs` 不动)
|
|
77
77
|
- 不改 owned 漂移校验(`doctor.mjs` §6.6 不动)
|
|
78
78
|
- 不反向同步——装副本独自有文件(如 hooks/commit-check.config.json)可能是装户配置,不应被删
|
|
79
|
-
-
|
|
79
|
+
- 不检查注册表内宿主 `modules/hosts/` 薄适配漂移(那是 `flow-kit sync-hosts` 范围)
|
|
80
80
|
|
|
81
81
|
## 当前已识别的真实漂点(首次实测,2026-09-25)
|
|
82
82
|
|
|
@@ -74,10 +74,12 @@ node bin/flow-kit.mjs doctor # §6.5 delegations + §6.6 o
|
|
|
74
74
|
- 失败项已修复并复测通过
|
|
75
75
|
- **关单**(日常闭环点,不依赖是否上 prod):
|
|
76
76
|
- 关单前一键过门:`node .agents/scripts/verify.mjs`(npm test + check-loop 固定编排,任一失败非零退出,非绿不关单)
|
|
77
|
-
- 新需求:逐条勾验入口 intent
|
|
78
|
-
|
|
77
|
+
- 新需求:逐条勾验入口 intent「验收标准」,每条补「证据:」(例如:粘贴 CI 运行结果、UI 截图路径、或具体的 commit SHA),intent $\rightarrow$ done;同名 spec/Quick-Plan(若有)仍 approved $\rightarrow$ done。**注意:勾选但缺证据会被 check-loop 拦截(hard-block)**
|
|
78
|
+
- **approved 留痕提交**(2026-09-30 起):done 确认前确认**本次关单文档**的 approved 态已进 git 历史(越早越好——approved 后即应提交;若尚未,先补提交再 done;门为逐份校验,三件套均关单时逐份各自满足)——confirm-doc 的 done 前置门硬校验(历史未出现「状态: approved」→ 拒绝落账);done 后再提交 done 态与其余改动
|
|
79
|
+
**确认落态唯一入口**(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。
|
|
79
80
|
- 修复:防复发验证已落地,incident → closed(经确认门 confirm-doc:open→fixed / fixed→closed 两跳、无单跳;2026-09-28 起 check-loop 15 对账——关单编辑先于 closed 确认,口径同 done 内容绑定)
|
|
80
81
|
- **同族收尾**:入口置终态时,同名 plan 一并置 `done`(spec 见上条),不留 `approved` 孤儿(口径同看板「入口已 done,本 plan 未终态」告警)
|
|
81
82
|
- 主智能体自做的 L1+ 新需求在 `workflow/delegations.md`「自做任务结果表」记一行(修复类不重复记)
|
|
83
|
+
- **委派快照回写**:关单后若 `workflow/delegations.md` §月度聚合快照缺当月行或 doctor §6.5 报陈旧 → 跑 `node .agents/scripts/agg-delegations.cjs` 并回贴当月快照行
|
|
82
84
|
- L1:本阶段结束,不进 deploy
|
|
83
85
|
- L2/L3:仅当用户确认要上 prod 时进 `.agents/commands/deploy.md`
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// commit-check: git commit 前置验证(agentic-flow-kit 通用版)
|
|
2
2
|
// 1) 敏感信息扫描(暂存区新增行含疑似密钥 -> BLOCK)
|
|
3
|
-
// 2) 条件构建验证(按 .agents/hooks/commit-check.config.json 的 builds 配置;--full
|
|
4
|
-
// 3) 质量检测(config.checks:lint / 类型检查 / vet 等秒级确定性检查;--full
|
|
3
|
+
// 2) 条件构建验证(按 .agents/hooks/commit-check.config.json 的 builds 配置;--full 不做 ext/prefix 裁剪,仍尊重 when)
|
|
4
|
+
// 3) 质量检测(config.checks:lint / 类型检查 / vet 等秒级确定性检查;--full 不做 ext/prefix 裁剪,仍尊重 when)
|
|
5
5
|
// config 由 flow-kit init 按技术栈生成初值,项目可自行增改;缺失或无 builds/checks 时跳过对应段。
|
|
6
6
|
// 配置示例(.agents/hooks/commit-check.config.json):
|
|
7
7
|
// {
|
|
@@ -23,8 +23,11 @@
|
|
|
23
23
|
// - 只有 ext:按扩展名全局触发(跨目录,适合语言型触发——仓库里该扩展名基本只属于该构建);
|
|
24
24
|
// - 只有 prefix:该前缀下任何文件触发(目录型触发——如 monorepo 子包,仓库其他位置的相同扩展名不触发);
|
|
25
25
|
// - 两者同配为 OR 并集而非 AND 交集——要「前缀内的特定扩展名」请只配 prefix(目录内文档变更多跑一次构建,方向安全)。
|
|
26
|
-
// checks
|
|
27
|
-
//
|
|
26
|
+
// when(builds 与 checks 共用):数组任一满足才启用,无 when 字段 = 无条件启用。
|
|
27
|
+
// - 路径或尾部 * 一层通配(如 "*.sln"):相对仓库根做存在性判断;
|
|
28
|
+
// - pkg:<点分路径>(如 pkg:scripts.build):读该项 cwd(缺省仓库根)的 package.json,
|
|
29
|
+
// 沿路径取值,非空字符串才算满足。
|
|
30
|
+
// 没有对应配置的项目自动跳过并提示,不 fail-closed 误拦。--full 同样尊重 when。
|
|
28
31
|
const { execSync } = require('child_process');
|
|
29
32
|
const path = require('path');
|
|
30
33
|
const fs = require('fs');
|
|
@@ -78,17 +81,50 @@ if (hits.length) {
|
|
|
78
81
|
|
|
79
82
|
// --- 2) 条件构建验证 ---
|
|
80
83
|
// --full:合并提交(pre-merge-commit / 冲突解决后手动 commit)使用——合并结果是没人验证过的新状态,
|
|
81
|
-
// 按 ext/prefix
|
|
84
|
+
// 按 ext/prefix 裁剪可能漏掉需要重跑的一侧,故不做路径裁剪。when 仍生效:没有对应能力就不跑。
|
|
82
85
|
// 引擎自身文件(.agents/、.githooks/)不参与构建匹配——工作流引擎不是项目构建产物。
|
|
83
86
|
const FULL = process.argv.includes('--full');
|
|
84
87
|
const stagedForBuild = staged.filter(f => !f.startsWith('.agents/') && !f.startsWith('.githooks/'));
|
|
85
|
-
|
|
88
|
+
|
|
89
|
+
// whenSatisfied:任一谓词满足即启用。pkg: 读 cwd 下的 package.json;其余谓词相对仓库根。
|
|
90
|
+
function whenItem(w, cwd) {
|
|
91
|
+
if (typeof w === 'string' && w.startsWith('pkg:')) {
|
|
92
|
+
const keys = w.slice(4).split('.').filter(Boolean);
|
|
93
|
+
if (!keys.length) return false;
|
|
94
|
+
const pkgPath = path.join(cwd || '.', 'package.json');
|
|
95
|
+
let pkg;
|
|
96
|
+
try { pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8')); } catch (e) { return false; }
|
|
97
|
+
let cur = pkg;
|
|
98
|
+
for (const k of keys) {
|
|
99
|
+
if (cur == null || typeof cur !== 'object' || !Object.prototype.hasOwnProperty.call(cur, k)) return false;
|
|
100
|
+
cur = cur[k];
|
|
101
|
+
}
|
|
102
|
+
return typeof cur === 'string' && cur.trim() !== '';
|
|
103
|
+
}
|
|
104
|
+
const star = String(w).indexOf('*');
|
|
105
|
+
if (star < 0) return fs.existsSync(w);
|
|
106
|
+
const suffix = w.slice(star + 1);
|
|
107
|
+
const dir = path.dirname(w.slice(0, star)) || '.';
|
|
108
|
+
try { return fs.readdirSync(dir).some((f) => f.endsWith(suffix)); } catch (e) { return false; }
|
|
109
|
+
}
|
|
110
|
+
function whenSatisfied(when, cwd) {
|
|
111
|
+
if (!when) return true;
|
|
112
|
+
const list = Array.isArray(when) ? when : [when];
|
|
113
|
+
return list.some((w) => whenItem(w, cwd));
|
|
114
|
+
}
|
|
115
|
+
function skipWhen(kind, item) {
|
|
116
|
+
const shown = Array.isArray(item.when) ? item.when.join(' / ') : item.when;
|
|
117
|
+
console.log(`SKIP: ${item.name}(未找到 ${shown},跳过${kind}——配置好后自动启用)`);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
let consideredBuild = false;
|
|
86
121
|
for (const b of (config.builds || [])) {
|
|
87
122
|
if (!b || !b.command) continue;
|
|
88
123
|
const exts = Array.isArray(b.ext) ? b.ext : [];
|
|
89
124
|
const match = FULL || stagedForBuild.some(f => exts.some(e => f.endsWith(e)) || (b.prefix ? f.startsWith(b.prefix) : false));
|
|
90
125
|
if (!match) continue;
|
|
91
|
-
|
|
126
|
+
consideredBuild = true;
|
|
127
|
+
if (!whenSatisfied(b.when, b.cwd)) { skipWhen('构建', b); continue; }
|
|
92
128
|
const r = sh(b.command, b.cwd);
|
|
93
129
|
if (!r.ok) {
|
|
94
130
|
if (b.lockPattern && new RegExp(b.lockPattern).test(r.out)) {
|
|
@@ -101,25 +137,13 @@ for (const b of (config.builds || [])) {
|
|
|
101
137
|
}
|
|
102
138
|
console.log(`OK: ${b.name} 构建通过(${b.command})`);
|
|
103
139
|
}
|
|
104
|
-
if (!
|
|
140
|
+
if (!consideredBuild) {
|
|
105
141
|
console.log(config.builds && config.builds.length
|
|
106
142
|
? 'SKIP: 无匹配的源码变更,跳过构建验证'
|
|
107
143
|
: 'SKIP: 未配置 builds(.agents/hooks/commit-check.config.json),跳过构建验证');
|
|
108
144
|
}
|
|
109
145
|
|
|
110
146
|
// --- 3) 质量检测(checks:lint / 类型检查 / vet 等秒级确定性检查;测试不放这里——关单在 test.md 阶段门) ---
|
|
111
|
-
// whenSatisfied:任一路径存在即启用;元素支持尾部 * 一层通配(如 *.sln = 仓库根存在 .sln 结尾文件)
|
|
112
|
-
function whenSatisfied(when) {
|
|
113
|
-
if (!when) return true;
|
|
114
|
-
const list = Array.isArray(when) ? when : [when];
|
|
115
|
-
return list.some((w) => {
|
|
116
|
-
const star = w.indexOf('*');
|
|
117
|
-
if (star < 0) return fs.existsSync(w);
|
|
118
|
-
const suffix = w.slice(star + 1);
|
|
119
|
-
const dir = path.dirname(w.slice(0, star)) || '.';
|
|
120
|
-
try { return fs.readdirSync(dir).some((f) => f.endsWith(suffix)); } catch (e) { return false; }
|
|
121
|
-
});
|
|
122
|
-
}
|
|
123
147
|
let ranCheck = false;
|
|
124
148
|
for (const c of (config.checks || [])) {
|
|
125
149
|
if (!c || !c.command) continue;
|
|
@@ -127,10 +151,7 @@ for (const c of (config.checks || [])) {
|
|
|
127
151
|
const match = FULL || stagedForBuild.some(f => exts.some(e => f.endsWith(e)) || (c.prefix ? f.startsWith(c.prefix) : false));
|
|
128
152
|
if (!match) continue;
|
|
129
153
|
ranCheck = true;
|
|
130
|
-
if (!whenSatisfied(c.when)) {
|
|
131
|
-
console.log(`SKIP: ${c.name}(未找到 ${Array.isArray(c.when) ? c.when.join(' / ') : c.when},跳过质量检查——配置好后自动启用)`);
|
|
132
|
-
continue;
|
|
133
|
-
}
|
|
154
|
+
if (!whenSatisfied(c.when, c.cwd)) { skipWhen('质量检查', c); continue; }
|
|
134
155
|
const r = sh(c.command, c.cwd);
|
|
135
156
|
if (!r.ok) {
|
|
136
157
|
console.log(`BLOCK: ${c.name} 质量检查未通过(${c.command})\n` + r.out.slice(0, 1200));
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# L2 触达面清单(单源:本文件——check-lane-surface.mjs 检查 A 共读,pre-commit「泳道完整性」门禁)
|
|
2
|
+
# 格式:每行一条 ERE(grep -E 兼容),匹配暂存文件的仓库相对路径(/ 分隔)。
|
|
3
|
+
# # 注释与空行忽略;本文件缺失或清单为空 = 检查跳过(装户无感升级)。
|
|
4
|
+
# 正则语法错误行 → 门禁 fail-closed 阻断(防静默空洞,修配置后重试)。
|
|
5
|
+
# 语义:命中任一模式 = 本次改动触及「一处定义、多处消费」的契约 / 规则面(L2 触达面,
|
|
6
|
+
# 判据见 .agents/commands/new-task.md §级别判断)。此时须有活跃(draft/approved / open/fixed)
|
|
7
|
+
# L2/L3 入口罩着;活跃入口全为 L0/L1 会被 pre-commit「触达面判低」拦截(三选修法见门禁消息)。
|
|
8
|
+
# 无任何活跃入口时仅 advisory——docs / bootstrap 豁免类改动(new-task.md §intent 豁免)不受阻。
|
|
9
|
+
# 本仓默认面(引擎 / 契约面,行为承载目录——commands/roles 纯提示文本属 docs 级不列入;按项目自行改):
|
|
10
|
+
^templates/
|
|
11
|
+
^\.agents/(scripts|hooks)/
|
|
12
|
+
^\.githooks/
|
|
13
|
+
^bin/
|
|
14
|
+
^modules/
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Implementer
|
|
2
2
|
|
|
3
|
-
> 公共角色契约(本文件):`.agents/roles/implementer.md
|
|
3
|
+
> 公共角色契约(本文件):`.agents/roles/implementer.md`。宿主薄适配(各宿主目录下 `agents/implementer.md`)正文段同源本文件,frontmatter 各自保留宿主特化字段(check-loop 检查项 6 按本引用校验适配层不断线)。
|
|
4
4
|
|
|
5
5
|
## 职责
|
|
6
6
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Independent Reviewer
|
|
2
2
|
|
|
3
|
-
> 公共角色契约(本文件):`.agents/roles/independent-reviewer.md
|
|
3
|
+
> 公共角色契约(本文件):`.agents/roles/independent-reviewer.md`。宿主薄适配(各宿主目录下 `agents/independent-reviewer.md`)正文段同源本文件,frontmatter 各自保留宿主特化字段(check-loop 检查项 6 按本引用校验适配层不断线)。
|
|
4
4
|
|
|
5
5
|
## 职责
|
|
6
6
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# UI Verifier
|
|
2
2
|
|
|
3
|
-
> 公共角色契约(本文件):`.agents/roles/ui-verifier.md
|
|
3
|
+
> 公共角色契约(本文件):`.agents/roles/ui-verifier.md`。宿主薄适配(各宿主目录下 `agents/ui-verifier.md`)正文段同源本文件,frontmatter 各自保留宿主特化字段(check-loop 检查项 6 按本引用校验适配层不断线)。
|
|
4
4
|
|
|
5
5
|
## 职责
|
|
6
6
|
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// check-lane-surface.mjs — 泳道完整性门禁(pre-commit 增量;2026-10-01 drift-hardening)
|
|
3
|
+
// 检查 A「触达面判低」:暂存 diff 命中项目 L2 触达面(.agents/lane-surfaces.txt,ERE 逐行;
|
|
4
|
+
// # 注释与空行忽略;文件缺失或清单为空 = 检查跳过,装户无感)时——
|
|
5
|
+
// 存在活跃 L2/L3 入口 → 放行(归因歧义为已知边界:机器不猜「哪个入口在干这批改动」);
|
|
6
|
+
// 活跃入口全为 L0/L1(含级别缺失)→ hard 拦,点名最新 L0/L1 入口 + 三选修法;
|
|
7
|
+
// 无任何活跃入口 → 仅 advisory(docs / bootstrap 豁免类改动的合法形态)。
|
|
8
|
+
// 活跃 = intents draft/approved + incidents open/fixed(incident≡intent 等价口径)。
|
|
9
|
+
// experiment/* 分支降级 advisory(泳道哲学:PoC 免闭环纪律,转正由 pre-push 加固门兜底)。
|
|
10
|
+
// 检查 B「探索标记缺失」:experiment/* 分支暂存的 workflow/intents/*.md(排除 _TEMPLATE)
|
|
11
|
+
// frontmatter 无「阶段: exploring」且无「状态: exploring」(口径同 check-loop 加固门)→ hard 拦。
|
|
12
|
+
// 标记是加固门覆盖率的前提且补写零成本——advisory 免的是纪律负担,不免标记本身。
|
|
13
|
+
// 判据原则(incidents/2026-09-28-batch-ledger-audit 教训):只用一手事实——暂存文件清单、
|
|
14
|
+
// 当前分支名、工作区 frontmatter 字段;不用时间戳 / 字符串相等反推过程事实。
|
|
15
|
+
// 边界:git 调用一律 -c core.quotepath=off(非 ASCII 路径教训,同 check-pairing-incremental.sh);
|
|
16
|
+
// frontmatter 读取容忍 CRLF(显式按 \r?\n 切分);detached HEAD 分支不可判 → A 按 strict、
|
|
17
|
+
// B 跳过(沿 pairing 门先例);配置行正则语法错误 → fail-closed(防静默空洞)。
|
|
18
|
+
// 已知边界:commit-time 门禁,CI / pre-push 复跑面无对应全量检查(push 时刻无暂存 diff 一手事实,
|
|
19
|
+
// 树级反推违背判据原则)——沿增量门禁 pre-commit-only 先例,声明于 spec 风险评估 R2。
|
|
20
|
+
// 用法:node check-lane-surface.mjs [--root <dir>] [--branch <名>] [--staged-file <清单文件>]
|
|
21
|
+
// (--root/--branch/--staged-file 为测试注入位;缺省自 git 发现。staged-file 每行一个仓库相对路径)
|
|
22
|
+
// 退出码:0 放行(含 advisory);1 阻断(stderr 出 [标签] 消息与修法)
|
|
23
|
+
import fs from 'node:fs';
|
|
24
|
+
import path from 'node:path';
|
|
25
|
+
import { spawnSync } from 'node:child_process';
|
|
26
|
+
|
|
27
|
+
const argv = process.argv.slice(2);
|
|
28
|
+
const argOf = (k) => {
|
|
29
|
+
const i = argv.indexOf(k);
|
|
30
|
+
return i >= 0 && i + 1 < argv.length ? argv[i + 1] : '';
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const gitOut = (cwd, gitArgs) => {
|
|
34
|
+
const r = spawnSync('git', ['-c', 'core.quotepath=off', ...gitArgs], { cwd, encoding: 'utf8' });
|
|
35
|
+
return r.status === 0 ? String(r.stdout || '') : '';
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const ROOT = argOf('--root') || gitOut(process.cwd(), ['rev-parse', '--show-toplevel']).trim() || process.cwd();
|
|
39
|
+
// detached HEAD 时 abbrev-ref 返回 'HEAD' → 不匹配 experiment/* → A strict / B 跳过
|
|
40
|
+
const BRANCH = argOf('--branch') || gitOut(ROOT, ['rev-parse', '--abbrev-ref', 'HEAD']).trim();
|
|
41
|
+
const stagedArg = argOf('--staged-file');
|
|
42
|
+
const STAGED = stagedArg
|
|
43
|
+
? fs.readFileSync(stagedArg, 'utf8').split(/\r?\n/).filter(Boolean)
|
|
44
|
+
: gitOut(ROOT, ['diff', '--cached', '--name-only', '--diff-filter=ACMR']).split(/\r?\n/).filter(Boolean);
|
|
45
|
+
|
|
46
|
+
const isExperiment = /^experiment\//.test(BRANCH);
|
|
47
|
+
|
|
48
|
+
// frontmatter 读取(CRLF 容忍;口径同 check-loop fmGet:--- 包夹、键行前缀匹配)
|
|
49
|
+
const fmOf = (file) => {
|
|
50
|
+
const fm = {};
|
|
51
|
+
let lines;
|
|
52
|
+
try {
|
|
53
|
+
lines = fs.readFileSync(file, 'utf8').split(/\r?\n/);
|
|
54
|
+
} catch {
|
|
55
|
+
return fm;
|
|
56
|
+
}
|
|
57
|
+
if (!/^---\s*$/.test(lines[0] || '')) return fm;
|
|
58
|
+
for (let i = 1; i < lines.length; i++) {
|
|
59
|
+
if (/^---\s*$/.test(lines[i])) break;
|
|
60
|
+
const m = lines[i].match(/^([^:]+):\s*(.*)$/);
|
|
61
|
+
if (m) fm[m[1].trim()] = m[2].trim();
|
|
62
|
+
}
|
|
63
|
+
return fm;
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
// 触达面配置:缺失/空 → [](检查 A 跳过);语法错误 → fail-closed
|
|
67
|
+
const loadPatterns = () => {
|
|
68
|
+
const file = path.join(ROOT, '.agents', 'lane-surfaces.txt');
|
|
69
|
+
if (!fs.existsSync(file)) return [];
|
|
70
|
+
const bad = [];
|
|
71
|
+
const pats = fs.readFileSync(file, 'utf8')
|
|
72
|
+
.split(/\r?\n/)
|
|
73
|
+
.map((l) => l.trim())
|
|
74
|
+
.filter((l) => l && !l.startsWith('#'))
|
|
75
|
+
.map((l) => {
|
|
76
|
+
try {
|
|
77
|
+
return new RegExp(l);
|
|
78
|
+
} catch {
|
|
79
|
+
bad.push(l);
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
})
|
|
83
|
+
.filter(Boolean);
|
|
84
|
+
if (bad.length) {
|
|
85
|
+
console.error(`泳道完整性(增量)— BLOCK:\n [面清单语法错误] .agents/lane-surfaces.txt 存在非法 ERE 行:${bad.join(' | ')}\n 修法:修正或删除该行后重试(fail-closed,防静默空洞)`);
|
|
86
|
+
process.exit(1);
|
|
87
|
+
}
|
|
88
|
+
return pats;
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
// 活跃入口收集(incident≡intent 等价;日期键 intent=日期 / incident=发现)
|
|
92
|
+
const ACTIVE = {
|
|
93
|
+
intent: new Set(['draft', 'approved']),
|
|
94
|
+
incident: new Set(['open', 'fixed']),
|
|
95
|
+
};
|
|
96
|
+
const collectActive = () => {
|
|
97
|
+
const out = [];
|
|
98
|
+
for (const [dir, kind, dateKey] of [['intents', 'intent', '日期'], ['incidents', 'incident', '发现']]) {
|
|
99
|
+
const dirPath = path.join(ROOT, 'workflow', dir);
|
|
100
|
+
if (!fs.existsSync(dirPath)) continue;
|
|
101
|
+
for (const f of fs.readdirSync(dirPath).sort()) {
|
|
102
|
+
if (!f.endsWith('.md') || f === '_TEMPLATE.md') continue;
|
|
103
|
+
const fm = fmOf(path.join(dirPath, f));
|
|
104
|
+
if (!ACTIVE[kind].has(fm['状态'] || '')) continue;
|
|
105
|
+
out.push({ rel: `workflow/${dir}/${f}`, base: f.replace(/\.md$/, ''), level: fm['级别'] || '', date: fm[dateKey] || '', kind });
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return out;
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
const blocks = [];
|
|
112
|
+
const advisories = [];
|
|
113
|
+
|
|
114
|
+
// --- 检查 A:触达面判低 ---
|
|
115
|
+
const patterns = loadPatterns();
|
|
116
|
+
const hits = patterns.length ? STAGED.filter((p) => patterns.some((re) => re.test(p))) : [];
|
|
117
|
+
if (hits.length) {
|
|
118
|
+
const active = collectActive();
|
|
119
|
+
const hasHigh = active.some((e) => e.level === 'L2' || e.level === 'L3');
|
|
120
|
+
if (!hasHigh) {
|
|
121
|
+
const low = active.filter((e) => e.level !== 'L2' && e.level !== 'L3');
|
|
122
|
+
if (low.length) {
|
|
123
|
+
const newest = [...low].sort((a, b) => (b.date || '').localeCompare(a.date || '') || b.rel.localeCompare(a.rel))[0];
|
|
124
|
+
const text = [
|
|
125
|
+
`[触达面判低] 暂存命中 L2 触达面,但活跃入口全为 L0/L1(含级别缺失):`,
|
|
126
|
+
` 命中: ${hits.join(' ')}`,
|
|
127
|
+
` 关联入口: ${newest.rel}(${newest.level || '级别缺失'}${newest.date ? `,${newest.date}` : ''};活跃 L0/L1 共 ${low.length} 件)`,
|
|
128
|
+
` 修法(三选一): ① 升级该入口「级别/risk_level」至 L2/L3 并补同名 spec(.agents/commands/design.md)`,
|
|
129
|
+
` ② 该入口已完成则关单(done)或显式放弃(superseded/cancelled)——不留活跃残留`,
|
|
130
|
+
` ③ 面清单误配则修 .agents/lane-surfaces.txt`,
|
|
131
|
+
].join('\n');
|
|
132
|
+
if (isExperiment) advisories.push(`(advisory·experiment 泳道,不阻断;转正由 pre-push 加固门兜底)\n${text}`);
|
|
133
|
+
else blocks.push(text);
|
|
134
|
+
} else {
|
|
135
|
+
advisories.push(`[触达面判低](advisory)暂存命中 L2 触达面(${hits.length} 个文件),但当前无任何活跃入口——若非 docs / bootstrap 豁免类改动(.agents/commands/new-task.md §intent 豁免),请先立入口文档`);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// --- 检查 B:探索标记缺失(仅 experiment/* 分支)---
|
|
141
|
+
if (isExperiment) {
|
|
142
|
+
const missing = STAGED
|
|
143
|
+
.filter((p) => /^workflow\/intents\/[^/]+\.md$/.test(p) && path.basename(p) !== '_TEMPLATE.md')
|
|
144
|
+
.filter((p) => {
|
|
145
|
+
const fm = fmOf(path.join(ROOT, p));
|
|
146
|
+
return fm['阶段'] !== 'exploring' && fm['状态'] !== 'exploring';
|
|
147
|
+
});
|
|
148
|
+
if (missing.length) {
|
|
149
|
+
blocks.push([
|
|
150
|
+
`[探索标记缺失] experiment/* 分支暂存的 intent 缺「阶段: exploring」标记:`,
|
|
151
|
+
...missing.map((p) => ` ${p}`),
|
|
152
|
+
` 修法(二选一): ① frontmatter 补一行「阶段: exploring」(advisory 免纪律不免标记——标记是 pre-push 加固门覆盖率的前提)`,
|
|
153
|
+
` ② 该件不属探索——换正式分支走标准泳道`,
|
|
154
|
+
].join('\n'));
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
for (const a of advisories) console.error(`泳道完整性(增量)— advisory:\n${a}\n`);
|
|
159
|
+
if (blocks.length) {
|
|
160
|
+
for (const b of blocks) console.error(`泳道完整性(增量)— BLOCK:\n${b}\n`);
|
|
161
|
+
console.error('pre-commit: 泳道完整性未通过,提交已阻止(spec: workflow/specs/2026-10-01-drift-hardening.md)');
|
|
162
|
+
process.exit(1);
|
|
163
|
+
}
|
|
164
|
+
process.exit(0);
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// check-lane-surface.test.mjs — 泳道完整性门禁 fixture 套件(2026-10-01 drift-hardening)
|
|
3
|
+
// 方法:现场构造临时根(workflow/{intents,incidents} + .agents/lane-surfaces.txt),经
|
|
4
|
+
// --root/--branch/--staged-file 三注入位 spawn 脚本,断言 exit code 与输出关键词。
|
|
5
|
+
// 场景集对齐同名 spec「功能行为」表 S1-S6 + 平台边界(CRLF / 非 ASCII / detached /
|
|
6
|
+
// 配置语法 / 状态活性 / incident 等价),另含 1 个 git 真仓端到端(quotepath=off 实路径)。
|
|
7
|
+
// 用法:node templates/_agents/scripts/check-lane-surface.test.mjs(npm test 随跑)
|
|
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 SCRIPT = path.join(SCRIPT_DIR, 'check-lane-surface.mjs');
|
|
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 mkfix = () => {
|
|
25
|
+
const d = fs.mkdtempSync(path.join(os.tmpdir(), 'lane-surface-test-'));
|
|
26
|
+
for (const s of ['intents', 'incidents']) fs.mkdirSync(path.join(d, 'workflow', s), { recursive: true });
|
|
27
|
+
fs.mkdirSync(path.join(d, '.agents'), { recursive: true });
|
|
28
|
+
return d;
|
|
29
|
+
};
|
|
30
|
+
const w = (root, rel, content) => fs.writeFileSync(path.join(root, rel), content);
|
|
31
|
+
// eol='\r\n' 时整文件按 CRLF 写(平台边界:frontmatter 读取容忍性)
|
|
32
|
+
const intent = (name, fm, eol = '\n') => `---${eol}${fm}${eol}---${eol}# INTENT — ${name}${eol}`;
|
|
33
|
+
const incident = (fm) => `---\n${fm}\n---\n# INCIDENT\n`;
|
|
34
|
+
const run = (root, { branch = 'main', staged = [] } = {}) => {
|
|
35
|
+
const listFile = path.join(root, 'staged.txt');
|
|
36
|
+
fs.writeFileSync(listFile, staged.join('\n') + '\n');
|
|
37
|
+
const r = spawnSync(process.execPath, [SCRIPT, '--root', root, '--branch', branch, '--staged-file', listFile], { encoding: 'utf8' });
|
|
38
|
+
return { code: r.status, out: `${r.stdout || ''}${r.stderr || ''}` };
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
// 通用 fixture:面清单 + 一个活跃 L1 intent(2026-09-28)+ 可选第二个入口
|
|
42
|
+
const base = (root, { level = 'L1', status = 'approved', extra = null } = {}) => {
|
|
43
|
+
w(root, '.agents/lane-surfaces.txt', '^templates/\n^\\.agents/(scripts|hooks)/\n');
|
|
44
|
+
w(root, 'workflow/intents/2026-09-28-old-l1.md', intent('x', `状态: ${status}\n级别: ${level}\n日期: 2026-09-28\n模块: pipeline`));
|
|
45
|
+
if (extra) w(root, extra[0], intent('y', extra[1]));
|
|
46
|
+
};
|
|
47
|
+
const FM_L0_NEW = '状态: draft\n级别: L0\n日期: 2026-10-01\n模块: ui';
|
|
48
|
+
|
|
49
|
+
// --- S1:命中 + 活跃 L2 存在 → 豁免放行 ---
|
|
50
|
+
{
|
|
51
|
+
const d = mkfix();
|
|
52
|
+
base(d, { level: 'L2' });
|
|
53
|
+
const r = run(d, { staged: ['templates/_agents/scripts/x.mjs'] });
|
|
54
|
+
check('S1 命中面+活跃 L2 入口 → 放行且无触达面判低输出', r.code === 0 && !r.out.includes('触达面判低'), r.out);
|
|
55
|
+
}
|
|
56
|
+
// --- S2:命中 + 活跃全为 L0/L1 → 阻断 ---
|
|
57
|
+
{
|
|
58
|
+
const d = mkfix();
|
|
59
|
+
base(d);
|
|
60
|
+
const r = run(d, { staged: ['templates/_agents/scripts/x.mjs', 'docs/readme.md'] });
|
|
61
|
+
check('S2 命中面+仅活跃 L1 → 阻断且消息含标签/命中文件/入口名/修法',
|
|
62
|
+
r.code === 1 && r.out.includes('[触达面判低]') && r.out.includes('templates/_agents/scripts/x.mjs')
|
|
63
|
+
&& r.out.includes('workflow/intents/2026-09-28-old-l1.md') && r.out.includes('修法'),
|
|
64
|
+
r.out);
|
|
65
|
+
}
|
|
66
|
+
// --- S2b:多个 L0/L1 活跃 → 点名最新(日期排序)---
|
|
67
|
+
{
|
|
68
|
+
const d = mkfix();
|
|
69
|
+
base(d, { extra: ['workflow/intents/2026-10-01-new-l0.md', FM_L0_NEW] });
|
|
70
|
+
const r = run(d, { staged: ['templates/t.md'] });
|
|
71
|
+
check('S2b 多活跃 L0/L1 → 关联入口取最新日期件', r.code === 1 && r.out.includes('2026-10-01-new-l0') && r.out.includes('共 2 件'), r.out);
|
|
72
|
+
}
|
|
73
|
+
// --- S2c:experiment 分支 → 检查 A 降级 advisory ---
|
|
74
|
+
{
|
|
75
|
+
const d = mkfix();
|
|
76
|
+
base(d);
|
|
77
|
+
const r = run(d, { branch: 'experiment/poc', staged: ['templates/t.md'] });
|
|
78
|
+
check('S2c experiment 分支检查 A → advisory 放行', r.code === 0 && r.out.includes('advisory·experiment'), r.out);
|
|
79
|
+
}
|
|
80
|
+
// --- S3:命中 + 无活跃入口 → advisory 放行(done 不算活跃)---
|
|
81
|
+
{
|
|
82
|
+
const d = mkfix();
|
|
83
|
+
w(d, '.agents/lane-surfaces.txt', '^templates/\n');
|
|
84
|
+
w(d, 'workflow/intents/2026-09-01-closed.md', intent('c', '状态: done\n级别: L1\n日期: 2026-09-01\n模块: ui'));
|
|
85
|
+
const r = run(d, { staged: ['templates/t.md'] });
|
|
86
|
+
check('S3 命中+无活跃(done 不算活跃)→ advisory 放行', r.code === 0 && r.out.includes('无任何活跃入口'), r.out);
|
|
87
|
+
}
|
|
88
|
+
// --- S4b:experiment 分支 intent 缺标记 → 阻断;补标记放行 ---
|
|
89
|
+
{
|
|
90
|
+
const d = mkfix();
|
|
91
|
+
w(d, 'workflow/intents/2026-10-01-poc.md', intent('p', '状态: draft\n级别: L1\n日期: 2026-10-01\n模块: ui'));
|
|
92
|
+
const r1 = run(d, { branch: 'experiment/poc', staged: ['workflow/intents/2026-10-01-poc.md'] });
|
|
93
|
+
check('S4b-1 experiment 分支无标记 intent → 阻断且含修法',
|
|
94
|
+
r1.code === 1 && r1.out.includes('[探索标记缺失]') && r1.out.includes('阶段: exploring'), r1.out);
|
|
95
|
+
w(d, 'workflow/intents/2026-10-01-poc.md', intent('p', '状态: draft\n级别: L1\n日期: 2026-10-01\n模块: ui\n阶段: exploring'));
|
|
96
|
+
const r2 = run(d, { branch: 'experiment/poc', staged: ['workflow/intents/2026-10-01-poc.md'] });
|
|
97
|
+
check('S4b-2 补「阶段: exploring」后放行', r2.code === 0, r2.out);
|
|
98
|
+
}
|
|
99
|
+
// --- S4b-c:口径同加固门——「状态: exploring」亦认 ---
|
|
100
|
+
{
|
|
101
|
+
const d = mkfix();
|
|
102
|
+
w(d, 'workflow/intents/2026-10-01-poc.md', intent('p', '状态: exploring\n级别: L1\n日期: 2026-10-01\n模块: ui'));
|
|
103
|
+
const r = run(d, { branch: 'experiment/poc', staged: ['workflow/intents/2026-10-01-poc.md'] });
|
|
104
|
+
check('S4b-c 「状态: exploring」亦认(口径同加固门)', r.code === 0, r.out);
|
|
105
|
+
}
|
|
106
|
+
// --- S6:非 experiment 分支 → 检查 B 不触发 ---
|
|
107
|
+
{
|
|
108
|
+
const d = mkfix();
|
|
109
|
+
w(d, 'workflow/intents/2026-10-01-poc.md', intent('p', '状态: draft\n级别: L1\n日期: 2026-10-01\n模块: ui'));
|
|
110
|
+
const r = run(d, { branch: 'main', staged: ['workflow/intents/2026-10-01-poc.md'] });
|
|
111
|
+
check('S6 非 experiment 分支无标记 intent → 放行(B 不触发)', r.code === 0 && !r.out.includes('探索标记缺失'), r.out);
|
|
112
|
+
}
|
|
113
|
+
// --- S5:配置缺失 → 检查 A 整体静默跳过(装户无感)---
|
|
114
|
+
{
|
|
115
|
+
const d = mkfix();
|
|
116
|
+
w(d, 'workflow/intents/2026-09-28-l1.md', intent('x', '状态: approved\n级别: L1\n日期: 2026-09-28\n模块: ui'));
|
|
117
|
+
const r = run(d, { staged: ['templates/t.md'] });
|
|
118
|
+
check('S5 无 lane-surfaces.txt → 静默跳过', r.code === 0 && r.out.trim() === '', JSON.stringify(r.out));
|
|
119
|
+
}
|
|
120
|
+
// --- incident 等价:open incident L2 豁免;fixed L1 算活跃低级 ---
|
|
121
|
+
{
|
|
122
|
+
const d = mkfix();
|
|
123
|
+
w(d, '.agents/lane-surfaces.txt', '^src/\n');
|
|
124
|
+
w(d, 'workflow/incidents/2026-09-30-inc.md', incident('状态: open\n级别: L2\n发现: 2026-09-30\n模块: backend'));
|
|
125
|
+
const r = run(d, { staged: ['src/a.ts'] });
|
|
126
|
+
check('incident 等价:活跃 open incident L2 → 豁免放行', r.code === 0, r.out);
|
|
127
|
+
}
|
|
128
|
+
{
|
|
129
|
+
const d = mkfix();
|
|
130
|
+
w(d, '.agents/lane-surfaces.txt', '^src/\n');
|
|
131
|
+
w(d, 'workflow/incidents/2026-09-30-inc.md', incident('状态: fixed\n级别: L1\n发现: 2026-09-30\n模块: backend'));
|
|
132
|
+
const r = run(d, { staged: ['src/a.ts'] });
|
|
133
|
+
check('incident 等价:fixed L1 算活跃低级入口 → 阻断',
|
|
134
|
+
r.code === 1 && r.out.includes('[触达面判低]') && r.out.includes('incidents/2026-09-30-inc.md'), r.out);
|
|
135
|
+
}
|
|
136
|
+
// --- 级别缺失:按低级计(不能自证 L2+ 即拦)---
|
|
137
|
+
{
|
|
138
|
+
const d = mkfix();
|
|
139
|
+
base(d, { level: '', status: 'draft' });
|
|
140
|
+
const r = run(d, { staged: ['templates/t.md'] });
|
|
141
|
+
check('级别缺失的活跃入口按低级计 → 阻断且消息含「级别缺失」', r.code === 1 && r.out.includes('级别缺失'), r.out);
|
|
142
|
+
}
|
|
143
|
+
// --- 平台边界:CRLF frontmatter ---
|
|
144
|
+
{
|
|
145
|
+
const d = mkfix();
|
|
146
|
+
w(d, '.agents/lane-surfaces.txt', '^templates/\n');
|
|
147
|
+
w(d, 'workflow/intents/2026-09-28-crlf.md', intent('x', '状态: approved\r\n级别: L1\r\n日期: 2026-09-28\r\n模块: ui', '\r\n'));
|
|
148
|
+
const r = run(d, { staged: ['templates/t.md'] });
|
|
149
|
+
check('CRLF frontmatter 正常解析(仍阻断)', r.code === 1 && r.out.includes('2026-09-28-crlf'), r.out);
|
|
150
|
+
}
|
|
151
|
+
// --- 平台边界:非 ASCII 路径透传(quotepath 教训的对账面)---
|
|
152
|
+
{
|
|
153
|
+
const d = mkfix();
|
|
154
|
+
w(d, '.agents/lane-surfaces.txt', '^模块/\n');
|
|
155
|
+
w(d, 'workflow/intents/2026-09-28-l1.md', intent('x', '状态: approved\n级别: L1\n日期: 2026-09-28\n模块: ui'));
|
|
156
|
+
const r = run(d, { staged: ['模块/接口契约.ts'] });
|
|
157
|
+
check('非 ASCII 暂存路径命中模式且消息原样透传', r.code === 1 && r.out.includes('模块/接口契约.ts'), r.out);
|
|
158
|
+
}
|
|
159
|
+
// --- 平台边界:detached HEAD(branch=HEAD)→ A strict、B 跳过 ---
|
|
160
|
+
{
|
|
161
|
+
const d = mkfix();
|
|
162
|
+
base(d);
|
|
163
|
+
w(d, 'workflow/intents/2026-10-01-poc.md', intent('p', '状态: draft\n级别: L1\n日期: 2026-10-01\n模块: ui'));
|
|
164
|
+
const r = run(d, { branch: 'HEAD', staged: ['templates/t.md', 'workflow/intents/2026-10-01-poc.md'] });
|
|
165
|
+
check('detached HEAD:A 按 strict 阻断、B 不触发', r.code === 1 && r.out.includes('[触达面判低]') && !r.out.includes('探索标记缺失'), r.out);
|
|
166
|
+
}
|
|
167
|
+
// --- 边界:_TEMPLATE.md 排除(不算活跃入口、不触发 B)---
|
|
168
|
+
{
|
|
169
|
+
const d = mkfix();
|
|
170
|
+
w(d, 'workflow/intents/_TEMPLATE.md', intent('t', '状态: draft\n级别: L1'));
|
|
171
|
+
const r = run(d, { branch: 'experiment/poc', staged: ['workflow/intents/_TEMPLATE.md'] });
|
|
172
|
+
check('_TEMPLATE.md 不触发检查 B,也不算活跃入口', r.code === 0, r.out);
|
|
173
|
+
}
|
|
174
|
+
// --- 配置边界:非法 ERE 行 → fail-closed ---
|
|
175
|
+
{
|
|
176
|
+
const d = mkfix();
|
|
177
|
+
w(d, '.agents/lane-surfaces.txt', '^ok/\n[invalid(\n');
|
|
178
|
+
const r = run(d, { staged: ['ok/a.ts'] });
|
|
179
|
+
check('配置含非法 ERE → fail-closed 阻断并指出坏行', r.code === 1 && r.out.includes('[面清单语法错误]') && r.out.includes('[invalid('), r.out);
|
|
180
|
+
}
|
|
181
|
+
// --- git 真仓端到端:走生产发现路径(quotepath=off + diff --cached)---
|
|
182
|
+
{
|
|
183
|
+
const d = mkfix();
|
|
184
|
+
const g = (a) => spawnSync('git', ['-c', 'core.quotepath=off', ...a], { cwd: d, encoding: 'utf8' });
|
|
185
|
+
g(['init', '-q']);
|
|
186
|
+
g(['config', 'user.email', 't@t']);
|
|
187
|
+
g(['config', 'user.name', 't']);
|
|
188
|
+
w(d, '.agents/lane-surfaces.txt', '^模块/\n');
|
|
189
|
+
w(d, 'workflow/intents/2026-09-28-l1.md', intent('x', '状态: approved\n级别: L1\n日期: 2026-09-28\n模块: ui'));
|
|
190
|
+
fs.mkdirSync(path.join(d, '模块'), { recursive: true });
|
|
191
|
+
w(d, '模块/接口契约.ts', 'export {};\n');
|
|
192
|
+
g(['add', '模块/接口契约.ts']);
|
|
193
|
+
const r = spawnSync(process.execPath, [SCRIPT, '--root', d], { cwd: d, encoding: 'utf8' });
|
|
194
|
+
check('git 端到端:真实暂存非 ASCII 路径 → 阻断且路径不转义',
|
|
195
|
+
r.status === 1 && `${r.stdout || ''}${r.stderr || ''}`.includes('模块/接口契约.ts'),
|
|
196
|
+
`${r.stdout || ''}${r.stderr || ''}`);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
console.log(`\n${fail ? `❌ ${fail} failed` : '✅ all passed'}(${pass} passed)`);
|
|
200
|
+
process.exit(fail ? 1 : 0);
|