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.
- package/README.md +36 -12
- 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/{build.md → wf-build.md} +3 -4
- package/modules/hosts/opencode/commands/{design.md → wf-design.md} +2 -2
- package/modules/hosts/opencode/commands/{maintain.md → wf-maintain.md} +1 -1
- package/modules/hosts/opencode/commands/{plan.md → wf-plan.md} +3 -3
- package/modules/hosts/opencode/commands/{review.md → wf-review.md} +1 -1
- package/modules/hosts/opencode/commands/{source-sync-check.md → wf-source-sync-check.md} +1 -1
- package/modules/hosts/opencode/commands/{sync-hosts.md → wf-sync-hosts.md} +1 -1
- package/modules/hosts/opencode/commands/{test.md → 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-sync-hosts.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 +1 -1
- package/src/cli.mjs +16 -4
- package/src/doctor.mjs +58 -5
- 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.mjs +12 -6
- package/src/sync-hosts.test.mjs +40 -17
- 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/sync-hosts.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-ledger.mjs +65 -0
- package/templates/_agents/scripts/check-ledger.test.mjs +127 -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 +105 -4
- 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/solidify-task.mjs +101 -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 +49 -2
- package/templates/_githooks/pre-push +56 -12
- package/templates/workflow/README.md +22 -3
- package/templates/workflow/incidents/_TEMPLATE.md +4 -0
- package/templates/workflow/intents/_TEMPLATE.md +1 -0
- /package/modules/hosts/opencode/commands/{deploy.md → wf-deploy.md} +0 -0
- /package/modules/hosts/opencode/commands/{gate-checklist.md → wf-gate-checklist.md} +0 -0
- /package/modules/hosts/opencode/commands/{new-task.md → wf-new-task.md} +0 -0
|
@@ -61,7 +61,7 @@ const PAIRS = [
|
|
|
61
61
|
{ doctor: '3', cl: null, note: 'git 仓库与钩子——装户体检独有' },
|
|
62
62
|
{ doctor: '4', cl: null, note: 'kit.json managed 台账——装户体检独有' },
|
|
63
63
|
{ doctor: '4.5', cl: null, note: '台账覆盖率——装户体检独有(盘上有 managed 类文件未登记,2026-09-26 managed-ledger-adopt)' },
|
|
64
|
-
{ doctor: '6.5', cl: null, note: 'delegations
|
|
64
|
+
{ doctor: '6.5', cl: null, note: 'delegations 台账结构 + 快照新鲜度——装户体检独有' },
|
|
65
65
|
{ doctor: '6.6', cl: null, note: 'owned 漂移——装户体检独有' },
|
|
66
66
|
{ doctor: '6.7', cl: null, note: '跨宿主薄适配——装户体检独有' },
|
|
67
67
|
{ doctor: '6.8', cl: null, note: 'workflows 编排脚本 lint——装户体检独有' },
|
|
@@ -81,6 +81,9 @@ const PAIRS = [
|
|
|
81
81
|
{ doctor: '7', cl: '14', note: '经 §7 整体运行覆盖' },
|
|
82
82
|
{ doctor: '7', cl: '15', note: '经 §7 整体运行覆盖' },
|
|
83
83
|
{ doctor: '7', cl: '16', note: '经 §7 整体运行覆盖(量化断言指标签名对账,2026-09-28 claim-exceeds-fix)' },
|
|
84
|
+
{ doctor: '7', cl: '17', note: '经 §7 整体运行覆盖(发版提交树上仍未收口的 intent/spec/plan)' },
|
|
85
|
+
{ doctor: '7', cl: '18', note: '经 §7 整体运行覆盖(委派台账对账)' },
|
|
86
|
+
{ doctor: '7', cl: '19', note: '经 §7 整体运行覆盖(逐阶段审计——起草先于入口确认 / 台账审批顺序倒置)' },
|
|
84
87
|
];
|
|
85
88
|
|
|
86
89
|
// gateChecklist:按登记表对照两侧检查项(pure function;pairs 可注入供测试)
|
|
@@ -177,8 +177,12 @@ function previewDiff(before, after) {
|
|
|
177
177
|
if (added.length > 10) console.log(` + …(其余 ${added.length - 10} 行略)`);
|
|
178
178
|
}
|
|
179
179
|
|
|
180
|
+
// 行尾归一比较(2026-09-30:fresh clone 检出 CRLF、生成段恒 LF——直接字符串相等会在克隆环境假阳性
|
|
181
|
+
// 「漂移」(doctor §6 与 check-loop 检查 11 均经 --check 判定);仅内容差异才算漂移)
|
|
182
|
+
const normEol = (s) => String(s).replace(/\r\n/g, '\n');
|
|
183
|
+
|
|
180
184
|
if (CHECK) {
|
|
181
|
-
if (out === existing) {
|
|
185
|
+
if (normEol(out) === normEol(existing)) {
|
|
182
186
|
console.log(`✅ workflow/INDEX.md 与磁盘一致(${summary})`);
|
|
183
187
|
process.exit(0);
|
|
184
188
|
}
|
|
@@ -189,7 +193,7 @@ if (CHECK) {
|
|
|
189
193
|
|
|
190
194
|
if (DRY) {
|
|
191
195
|
console.log(`🔎 --dry-run:未写盘(${summary})`);
|
|
192
|
-
if (out === existing) console.log('• workflow/INDEX.md:无变化');
|
|
196
|
+
if (normEol(out) === normEol(existing)) console.log('• workflow/INDEX.md:无变化');
|
|
193
197
|
else previewDiff(existing, out);
|
|
194
198
|
} else {
|
|
195
199
|
fs.writeFileSync(INDEX_P, out, 'utf8');
|
|
@@ -85,6 +85,18 @@ const run = (root, args = []) => spawnSync(process.execPath, [GEN, ...args], { c
|
|
|
85
85
|
fs.rmSync(root, { recursive: true, force: true });
|
|
86
86
|
}
|
|
87
87
|
|
|
88
|
+
// ---- 场景 2b:CRLF 检出形态(fresh clone)--check 不误报(2026-09-30 行尾归一)----
|
|
89
|
+
{
|
|
90
|
+
const root = mkfix();
|
|
91
|
+
run(root);
|
|
92
|
+
const ip = path.join(root, 'workflow', 'INDEX.md');
|
|
93
|
+
fs.writeFileSync(ip, fs.readFileSync(ip, 'utf8').replace(/\r\n/g, '\n').replace(/\n/g, '\r\n'), 'utf8');
|
|
94
|
+
const crlf = run(root, ['--check']);
|
|
95
|
+
check('场景 2b:INDEX 为 CRLF 检出形态 → --check exit 0(行尾归一,不假阳性)',
|
|
96
|
+
crlf.status === 0, `exit=${crlf.status}\n${crlf.stdout}${crlf.stderr}`);
|
|
97
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
98
|
+
}
|
|
99
|
+
|
|
88
100
|
// ---- 场景 3:缺 `模块:` 容忍显示 —(不报错、不阻断)----
|
|
89
101
|
{
|
|
90
102
|
const root = mkfix();
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
// policy.mjs — check-loop 豁免锚的单源(policyVersion)
|
|
2
|
+
// 新豁免改 POLICIES 并升版本,不在 check-loop 里再写散落日期。
|
|
3
|
+
// kit.json 缺 policyVersion、或版本不在表内:回退版本 1(不启用版本 2 的 approved 锚)。
|
|
4
|
+
// audit 不在本表:缺省视为全量检查(已装仓库与无 kit 的测试夹具);init 新装显式写 false。
|
|
5
|
+
import fs from 'node:fs';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
|
|
8
|
+
export const POLICIES = {
|
|
9
|
+
1: {
|
|
10
|
+
moduleSince: '2026-09-22',
|
|
11
|
+
check14Since: '2026-09-23',
|
|
12
|
+
confirmDocsEffective: '2026-09-27',
|
|
13
|
+
confirmIncidentsEffective: '2026-09-28',
|
|
14
|
+
bindingTs: '2026-09-28',
|
|
15
|
+
},
|
|
16
|
+
// 版本 2 = 版本 1 的五个日期 + 检查 17 的 approved 锚 + 检查 19 的 stageGateSince + check14Since 后移至确认门实际上线日(2026-09-23 → 2026-09-26)。
|
|
17
|
+
// 漏抄日期键会改动其他检查的生效日。
|
|
18
|
+
2: {
|
|
19
|
+
moduleSince: '2026-09-22',
|
|
20
|
+
check14Since: '2026-09-26',
|
|
21
|
+
confirmDocsEffective: '2026-09-27',
|
|
22
|
+
confirmIncidentsEffective: '2026-09-28',
|
|
23
|
+
bindingTs: '2026-09-28',
|
|
24
|
+
check17UnclosedAfter: '0.8.0',
|
|
25
|
+
stageGateSince: '2026-09-29', // 检查 19 逐阶段审计的生效日(2026-09-30 stage-gate-machine);锚 = 台账 ts 的 UTC 日期(复核 P2-3:本批 ts 2026-09-29T17:33Z 落在受审范围,顺序合规故静默);v1 无此键 → 该检查整体跳过
|
|
26
|
+
},
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export function loadKitPolicy(root) {
|
|
30
|
+
let raw = null;
|
|
31
|
+
try {
|
|
32
|
+
raw = JSON.parse(fs.readFileSync(path.join(root, '.agents', 'kit.json'), 'utf8'));
|
|
33
|
+
} catch { /* 无 kit 或不可解析 */ }
|
|
34
|
+
const requested = raw && Number.isInteger(raw.policyVersion) ? raw.policyVersion : 1;
|
|
35
|
+
const policyVersion = Object.prototype.hasOwnProperty.call(POLICIES, requested) ? requested : 1;
|
|
36
|
+
return {
|
|
37
|
+
...POLICIES[policyVersion],
|
|
38
|
+
policyVersion,
|
|
39
|
+
requestedVersion: requested,
|
|
40
|
+
audit: raw && typeof raw.audit === 'boolean' ? raw.audit : undefined,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function auditEnabled(policy) {
|
|
45
|
+
return policy.audit !== false;
|
|
46
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// solidify-task: 将 L1 液态草稿一次性固化为正式 workflow 文档(迁移 + 批量确认 + 索引更新)
|
|
3
|
+
// 用法:node .agents/scripts/solidify-task.mjs --topic "主题" [--root <仓库根>]
|
|
4
|
+
import fs from 'node:fs';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import { spawnSync } from 'node:child_process';
|
|
7
|
+
import { fileURLToPath } from 'node:url';
|
|
8
|
+
|
|
9
|
+
const SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url));
|
|
10
|
+
const ROOT = process.argv.includes('--root') ?
|
|
11
|
+
process.argv[process.argv.indexOf('--root') + 1] :
|
|
12
|
+
process.cwd();
|
|
13
|
+
const DRAFTS_DIR = path.join(ROOT, '.zcode', 'drafts');
|
|
14
|
+
const WF_ROOT = path.join(ROOT, 'workflow');
|
|
15
|
+
|
|
16
|
+
function fail(msg) { console.error('❌ ' + msg); process.exit(1); }
|
|
17
|
+
|
|
18
|
+
function parseArgs() {
|
|
19
|
+
const topicIdx = process.argv.indexOf('--topic');
|
|
20
|
+
if (topicIdx === -1 || !process.argv[topicIdx + 1]) {
|
|
21
|
+
console.error('用法: node .agents/scripts/solidify-task.mjs --topic "主题"');
|
|
22
|
+
process.exit(1);
|
|
23
|
+
}
|
|
24
|
+
return process.argv[topicIdx + 1];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async function run() {
|
|
28
|
+
const topic = parseArgs();
|
|
29
|
+
console.log(`🚀 开始固化 L1 任务: ${topic}...`);
|
|
30
|
+
|
|
31
|
+
if (!fs.existsSync(DRAFTS_DIR)) fail('草稿目录 .zcode/drafts/ 不存在');
|
|
32
|
+
|
|
33
|
+
// 1. 搜寻草稿
|
|
34
|
+
const files = fs.readdirSync(DRAFTS_DIR).filter(f => f.includes(topic) && f.endsWith('.md'));
|
|
35
|
+
if (!files.length) fail(`在 ${DRAFTS_DIR} 中未找到主题为 ${topic} 的草稿文件`);
|
|
36
|
+
|
|
37
|
+
const today = new Date().toISOString().slice(0, 10);
|
|
38
|
+
const solidified = [];
|
|
39
|
+
|
|
40
|
+
// 2. 迁移并规范化命名
|
|
41
|
+
for (const file of files) {
|
|
42
|
+
const content = fs.readFileSync(path.join(DRAFTS_DIR, file), 'utf8');
|
|
43
|
+
let targetDir = '';
|
|
44
|
+
|
|
45
|
+
// 简单启发式判定类型
|
|
46
|
+
if (content.includes('# INTENT')) targetDir = 'intents';
|
|
47
|
+
else if (content.includes('# PLAN')) targetDir = 'plans';
|
|
48
|
+
else if (content.includes('# SPEC')) targetDir = 'specs';
|
|
49
|
+
else if (content.includes('# INCIDENT')) targetDir = 'incidents';
|
|
50
|
+
else continue;
|
|
51
|
+
|
|
52
|
+
const targetName = `${today}-${file.replace(/.*-/, '').replace('.md', '.md')}`;
|
|
53
|
+
if (!targetName.startsWith('20')) {
|
|
54
|
+
// 如果文件名没日期前缀,强制加上
|
|
55
|
+
// 假设文件名为 <topic>.md
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// 统一处理:强制 YYYY-MM-DD- 前缀
|
|
59
|
+
const finalName = /^\d{4}-\d{2}-\d{2}-/.test(file) ? file : `${today}-${file}`;
|
|
60
|
+
const targetPath = path.join(WF_ROOT, targetDir, finalName);
|
|
61
|
+
|
|
62
|
+
fs.mkdirSync(path.dirname(targetPath), { recursive: true });
|
|
63
|
+
fs.copyFileSync(path.join(DRAFTS_DIR, file), targetPath);
|
|
64
|
+
|
|
65
|
+
solidified.push({
|
|
66
|
+
rel: `workflow/${targetDir}/${finalName}`,
|
|
67
|
+
abs: targetPath,
|
|
68
|
+
type: targetDir
|
|
69
|
+
});
|
|
70
|
+
console.log(` 📦 迁移: ${file} -> ${targetKebab(targetDir)}/${finalName}`);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function targetKebab(dir) { return dir; }
|
|
74
|
+
|
|
75
|
+
// 3. 批量确认 (Delegated)
|
|
76
|
+
console.log('🛡️ 执行批量确认...');
|
|
77
|
+
for (const item of solidified) {
|
|
78
|
+
const res = spawnSync('node', [
|
|
79
|
+
path.join(SCRIPT_DIR, 'confirm-doc.mjs'),
|
|
80
|
+
item.rel,
|
|
81
|
+
'--delegated',
|
|
82
|
+
`固化 L1 任务 ${topic}:用户确认草稿无误,执行固化归档`
|
|
83
|
+
], { encoding: 'utf8' });
|
|
84
|
+
|
|
85
|
+
if (res.status !== 0) {
|
|
86
|
+
console.error(` ❌ 确认失败 [${item.rel}]: ${res.stderr}`);
|
|
87
|
+
// 此时不 exit,尝试继续其他文件的确认
|
|
88
|
+
} else {
|
|
89
|
+
console.log(` ✅ 已确认: ${item.rel}`);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// 4. 更新索引
|
|
94
|
+
console.log('📖 更新工作流索引...');
|
|
95
|
+
spawnSync('node', [path.join(SCRIPT_DIR, 'gen-workflow-index.mjs')], { stdio: 'inherit' });
|
|
96
|
+
|
|
97
|
+
console.log(`\n✨ 任务 ${topic} 已成功固化!`);
|
|
98
|
+
console.log(`👉 建议执行: git add workflow/ .agents/confirmations.jsonl && git commit -m "docs(workflow): solidify L1 task ${topic}"`);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
run();
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
// stage-gates.mjs — 逐阶段前置判定的单源(fill-spec / fill-plan / confirm-doc 共享;2026-09-30 stage-gate-machine)
|
|
2
|
+
// 口径(与 check-loop 检查 19 内联实现互引——改一处须跟另一处):
|
|
3
|
+
// 入口「已确认」= 文件存在且 frontmatter 状态 ∈ {approved, done},且【机器事实源】满足其一:
|
|
4
|
+
// ① confirmations.jsonl 台账存在该 doc 的 stage ∈ {approved, done} 行(经 confirm-doc 才产生,防手改状态冒充);
|
|
5
|
+
// ② 存量口径:文档日期早于 policy.confirmDocsEffective,或行首含「流程: legacy」。
|
|
6
|
+
// incident 入口 = 文件存在 + 「时间线」小节内、行首列表条目含「用户确认」(原 maintain.md §3 约定;
|
|
7
|
+
// 2026-09-30 复核 P2-1 收窄为小节内行首条目;第三轮复核 P1-1 起接受日期前缀形态,与模板样例一致);
|
|
8
|
+
// 状态 ∈ {open, fixed, closed}(spec/plan 须先于 fixed 起草,故 open 即为有效态)。
|
|
9
|
+
// 入口「级别」须 ∈ L0-L3;缺失即拒(fail-closed)——**但库存量形态短路**(`流程: legacy` 或 日期早于
|
|
10
|
+
// 确认门生效日):跳过级别校验与 spec 档(2026-09-30 增量复核 N1——存量最可能缺级别,硬拒会锁死
|
|
11
|
+
// spec 起草与确认)。
|
|
12
|
+
// 注意:**库存量信号本身即等于「入口已确认」**(judgeDoc:`台账行 || 存量口径`,与检查 15 / 检查 1 /
|
|
13
|
+
// 配对门的既有口径一致——早于确认门生效日的自报日期即存量豁免,不要求另有台账行);此处「短路」
|
|
14
|
+
// 只额外豁免**级别前置与 spec 档**,不是对台账要求的二次豁免(P2-1:原注释把二者写反,已订正)。
|
|
15
|
+
// plan 门追加:入口级别(取入口 frontmatter 级别,不取工具 --level)为 L2/L3 时,同名 spec 须已确认(同口径)。
|
|
16
|
+
// 探索泳道豁免(2026-09-30 hybrid-governance-explore-hardening):同名 intent 仍为 draft 且风险泳道为
|
|
17
|
+
// L0/L1(级别优先;级别缺失或非法时回落 risk_level——fill-intent 双写同值)→「先起草后确认」是协作道
|
|
18
|
+
// 语义(check-loop 检查 19 同款豁免口径),放行 spec/plan 起草与确认;L2/L3 防御道维持入口确认前置。
|
|
19
|
+
// incident 入口不豁免(过目留痕本就轻量)。收口由 check-loop --hardening 加固门兜底(入 main 前须
|
|
20
|
+
// intent approved/done + spec/plan 在场)。
|
|
21
|
+
// 信任边界声明(沿 2026-09-28 audit-gate-hardening 口径):本地可写台账 / 手改状态仍可伪造——本模块只把
|
|
22
|
+
// 「顺手绕过」抬到「主动伪造」,不宣称通道关闭;事后对质靠 git 历史与台账。
|
|
23
|
+
// 设计先例:fill-* 已 import 同目录 workflow-enums.mjs;check-loop 检查 19 内联同口径(门禁脚本自包含、防兄弟依赖)。
|
|
24
|
+
import fs from 'node:fs';
|
|
25
|
+
import path from 'node:path';
|
|
26
|
+
import { spawnSync } from 'node:child_process';
|
|
27
|
+
import { loadKitPolicy } from './policy.mjs';
|
|
28
|
+
|
|
29
|
+
export const ENTRY_STATUSES = ['approved', 'done'];
|
|
30
|
+
export const INCIDENT_STATUSES = ['open', 'fixed', 'closed'];
|
|
31
|
+
|
|
32
|
+
// readFm:frontmatter 区(首 --- 至闭合 ---)取单字段值;无 frontmatter / 无该键 → ''
|
|
33
|
+
export function readFm(abs, key) {
|
|
34
|
+
let text;
|
|
35
|
+
try { text = fs.readFileSync(abs, 'utf8'); } catch { return ''; }
|
|
36
|
+
const lines = text.replace(/\r\n/g, '\n').split('\n');
|
|
37
|
+
if (!/^---\s*$/.test(lines[0] || '')) return '';
|
|
38
|
+
for (let i = 1; i < lines.length; i++) {
|
|
39
|
+
if (/^---\s*$/.test(lines[i])) break;
|
|
40
|
+
const m = lines[i].match(new RegExp(`^${key}:\\s*(.*)$`));
|
|
41
|
+
if (m) return m[1].trim();
|
|
42
|
+
}
|
|
43
|
+
return '';
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function readBody(abs) {
|
|
47
|
+
try { return fs.readFileSync(abs, 'utf8'); } catch { return ''; }
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// readLedger:confirmations.jsonl 追加式台账(坏行容忍跳过;与 check-loop 检查 15 同容错口径)
|
|
51
|
+
export function readLedger(root) {
|
|
52
|
+
const p = path.join(root, '.agents', 'confirmations.jsonl');
|
|
53
|
+
let text = '';
|
|
54
|
+
try { text = fs.readFileSync(p, 'utf8'); } catch { return []; }
|
|
55
|
+
const out = [];
|
|
56
|
+
for (const line of text.split('\n')) {
|
|
57
|
+
if (!line.trim()) continue;
|
|
58
|
+
try { out.push(JSON.parse(line)); } catch { /* 坏行跳过 */ }
|
|
59
|
+
}
|
|
60
|
+
return out;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// judgeDoc:状态枚举 + 机器事实源判定(供入口 / spec 复用)
|
|
64
|
+
// legacy:库存量信号(`流程: legacy` 标记 或 日期 < confirmDocsEffective)——所有调用方与门禁其余部分
|
|
65
|
+
// 同口径(配对门 / 检查 1 / 检查 15 / 检查 19);2026-09-30 复核 N1:存量入口须先短路,不得被
|
|
66
|
+
// 「缺级别」fail-closed 硬拒(存量最可能缺级别,硬拒会锁死 spec 起草/确认,违反「存量零新增」契约)。
|
|
67
|
+
function judgeDoc(root, abs, rel, statuses) {
|
|
68
|
+
const st = readFm(abs, '状态');
|
|
69
|
+
if (!statuses.includes(st)) {
|
|
70
|
+
return { ok: false, status: st, legacy: false, reason: `状态「${st || '缺失'}」不在 ${statuses.join(' / ')}` };
|
|
71
|
+
}
|
|
72
|
+
const policy = loadKitPolicy(root);
|
|
73
|
+
const body = readBody(abs);
|
|
74
|
+
const legacyByMarker = body.split(/\r?\n/).some((l) => /^流程: legacy/.test(l));
|
|
75
|
+
const d = readFm(abs, '日期') || readFm(abs, '发现');
|
|
76
|
+
const legacyByDate = /^\d{4}-\d{2}-\d{2}$/.test(d) && d < policy.confirmDocsEffective;
|
|
77
|
+
const legacy = legacyByMarker || legacyByDate;
|
|
78
|
+
const hasRow = readLedger(root).some((e) => e && e.doc === rel && (e.stage === 'approved' || e.stage === 'done'));
|
|
79
|
+
if (hasRow || legacy) return { ok: true, status: st, legacy };
|
|
80
|
+
return { ok: false, status: st, legacy: false, reason: `无确认台账行(门禁后的入口须经 confirm-doc,且台账无 ${rel} 的 approved / done 行)` };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// entryStorageLegacy:入口是否属「库存量」形态(`流程: legacy` 标记 或 日期早于确认门生效日)——
|
|
84
|
+
// 与 judgeDoc 的存量信号同口径,供门在**级别校验**处短路(2026-09-30 增量复核 N1)。注意:本短路
|
|
85
|
+
// **只豁免 level 前置**(存量多为门禁前建档、无级别字段),**不豁免台账要求**——`judgeDoc` 的
|
|
86
|
+
// hasRow||legacy 仍照常判定(门禁后无台账行即拒,不因日期早于生效日而放行)。
|
|
87
|
+
export function entryStorageLegacy(root, abs) {
|
|
88
|
+
const body = readBody(abs);
|
|
89
|
+
if (body.split(/\r?\n/).some((l) => /^流程: legacy/.test(l))) return true;
|
|
90
|
+
const d = readFm(abs, '日期') || readFm(abs, '发现');
|
|
91
|
+
return /^\d{4}-\d{2}-\d{2}$/.test(d) && d < loadKitPolicy(root).confirmDocsEffective;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// incidentMarked:「## 时间线」小节内、行首列表条目形态的「用户确认」(维持口径;正文叙述句不算——
|
|
95
|
+
// 2026-09-30 复核 P2-1 实证:全仓唯一历史命中为叙述句,非过目留痕)。
|
|
96
|
+
// 行首可选日期前缀(`- 2026-09-30 用户确认:…`)与可选加粗(`- **用户确认**:…`)均接受——
|
|
97
|
+
// 与 _TEMPLATE.md 时间线样例形态保持一致(2026-09-30 第三轮复核 P1-1:样例若不可解析即误导)。
|
|
98
|
+
export function incidentMarked(body) {
|
|
99
|
+
let inTimeline = false;
|
|
100
|
+
for (const l of String(body).replace(/\r\n/g, '\n').split('\n')) {
|
|
101
|
+
if (/^##\s/.test(l)) { inTimeline = /^##\s*时间线/.test(l); continue; }
|
|
102
|
+
if (inTimeline && MARK_RE.test(l)) return true;
|
|
103
|
+
}
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// MARK_RE:行首列表条目 + 可选日期前缀(YYYY-MM-DD 或 <日期> 占位)+ 可选加粗围栏(词前 / 词后均可)
|
|
108
|
+
// + 「用户确认」+ **须紧跟冒号或行尾**(词边界)。
|
|
109
|
+
// 形态矩阵(2026-09-30 四/五轮复核收敛,8 正例 / 5 负例由 fill-spec.test S19/S20 钉死):
|
|
110
|
+
// 命中:`- 用户确认:…` / `- **用户确认**:…` / `- 2026-09-30 用户确认:…` / `- <日期> **用户确认**:…` / 行尾无冒号
|
|
111
|
+
// 拒绝:`- 2026-09-30 用户确认了方案` / `- 用户确认书已归档` / `- 用户确认。` / `- 待用户确认:…`
|
|
112
|
+
// 注:词前的 `\**` 与词后的 `\**` 必须都允许——只开一侧即产生结构性假阴性(第五轮复核 P1 实证:
|
|
113
|
+
// 加粗围栏形态被拒,与 _TEMPLATE.md 自述契约冲突)。
|
|
114
|
+
export const MARK_RE = /^\s*[-*]\s*(?:\d{4}-\d{2}-\d{2}|<日期>)?\s*\**用户确认\**(?=\s*[::]|\s*$)/;
|
|
115
|
+
|
|
116
|
+
// entryConfirmed:同主题入口判定(intent 优先;否则 incidents)——返回 {ok, kind, rel, level, status, reason?}
|
|
117
|
+
export function entryConfirmed(root, base) {
|
|
118
|
+
const intentRel = `workflow/intents/${base}.md`;
|
|
119
|
+
const intentAbs = path.join(root, intentRel);
|
|
120
|
+
if (fs.existsSync(intentAbs)) {
|
|
121
|
+
const r = judgeDoc(root, intentAbs, intentRel, ENTRY_STATUSES);
|
|
122
|
+
return { ...r, kind: 'intent', rel: intentRel, level: readFm(intentAbs, '级别'), risk: readFm(intentAbs, 'risk_level'), storageLegacy: entryStorageLegacy(root, intentAbs) };
|
|
123
|
+
}
|
|
124
|
+
const incRel = `workflow/incidents/${base}.md`;
|
|
125
|
+
const incAbs = path.join(root, incRel);
|
|
126
|
+
if (fs.existsSync(incAbs)) {
|
|
127
|
+
const st = readFm(incAbs, '状态');
|
|
128
|
+
const level = readFm(incAbs, '级别');
|
|
129
|
+
if (INCIDENT_STATUSES.includes(st) && incidentMarked(readBody(incAbs))) {
|
|
130
|
+
return { ok: true, kind: 'incident', rel: incRel, level, status: st, legacy: false };
|
|
131
|
+
}
|
|
132
|
+
const why = !INCIDENT_STATUSES.includes(st) ? `状态「${st || '缺失'}」非法` : '「时间线」小节缺「用户确认」条目(maintain.md §3)';
|
|
133
|
+
return { ok: false, kind: 'incident', rel: incRel, level, status: st, legacy: false, reason: `同名 incident 未过目确认——${why}` };
|
|
134
|
+
}
|
|
135
|
+
return { ok: false, kind: null, rel: null, level: '', status: '', reason: `未找到同名入口(workflow/intents 与 workflow/incidents 均无 ${base}.md)` };
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// specConfirmed:plan 门的 L2/L3 追加要求
|
|
139
|
+
export function specConfirmed(root, base) {
|
|
140
|
+
const rel = `workflow/specs/${base}.md`;
|
|
141
|
+
const abs = path.join(root, rel);
|
|
142
|
+
if (!fs.existsSync(abs)) return { ok: false, reason: `缺同名 spec(${rel})` };
|
|
143
|
+
return judgeDoc(root, abs, rel, ENTRY_STATUSES);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// draftGateFor:起草门总判(kind = 'spec' | 'plan',fill 工具与 confirm-doc 逐阶段门共用)
|
|
147
|
+
export function draftGateFor(kind, root, base) {
|
|
148
|
+
const entry = entryConfirmed(root, base);
|
|
149
|
+
if (!entry.ok) {
|
|
150
|
+
// 探索泳道豁免(2026-09-30 hybrid-governance-explore-hardening,口径见头部注释):L0/L1 draft intent
|
|
151
|
+
// 放行起草——「先动手后确认」;泳道取级别优先、缺失/非法回落 risk_level(与 fill-intent 双写兼容,
|
|
152
|
+
// 级别合法但为 L2/L3 时不回落——fail-closed 就严)。
|
|
153
|
+
const lane = /^L[0-3]$/.test(entry.level || '') ? entry.level : entry.risk;
|
|
154
|
+
if (entry.kind === 'intent' && entry.status === 'draft' && (lane === 'L0' || lane === 'L1')) {
|
|
155
|
+
return { ok: true, entry, laneBypass: true };
|
|
156
|
+
}
|
|
157
|
+
return {
|
|
158
|
+
ok: false,
|
|
159
|
+
reason: `同主题入口未确认——${entry.reason}`,
|
|
160
|
+
hint: '先走 plan.md 完成入口确认(修复类走 maintain.md,incident 草稿过目后在时间线留「用户确认」条目)',
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
// 库存量形态短路(2026-09-30 增量复核 N1):**只额外豁免「级别前置」与「spec 档」**——
|
|
164
|
+
// 库存量入口(`流程: legacy` / 日期早于确认门生效日)多为门禁前建档、常缺「级别」字段,硬拒会锁死
|
|
165
|
+
// spec 起草与确认。注意:库存量信号**本身即等于入口已确认**(judgeDoc 的 `台账行 || 存量口径`,
|
|
166
|
+
// 与检查 1 / 15 / 配对门同口径)——台账豁免发生在 judgeDoc,不在本处;本处不做二次豁免
|
|
167
|
+
// (第三轮复核 P2-1:原句「台账要求不豁免」把两件事写反,已订正)。
|
|
168
|
+
if (!entry.storageLegacy && !/^L[0-3]$/.test(entry.level)) {
|
|
169
|
+
return {
|
|
170
|
+
ok: false,
|
|
171
|
+
reason: `入口缺合法「级别」字段(现「${entry.level || '缺失'}」,无法判定前置深度)`,
|
|
172
|
+
hint: '先给入口补「级别: L0-L3」字段,再继续',
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
if (kind === 'plan' && !entry.storageLegacy && (entry.level === 'L2' || entry.level === 'L3')) {
|
|
176
|
+
const sp = specConfirmed(root, base);
|
|
177
|
+
if (!sp.ok) {
|
|
178
|
+
return { ok: false, reason: `入口级别 ${entry.level},前置 spec 未确认——${sp.reason}`, hint: '先走 design.md 完成 spec 确认' };
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
return { ok: true, entry };
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// confirmGateFor:确认门总判(confirm-doc 用;docRel = workflow/specs/... 或 workflow/plans/...)
|
|
185
|
+
export function confirmGateFor(root, docRel) {
|
|
186
|
+
const base = path.basename(docRel).replace(/\.md$/, '');
|
|
187
|
+
const kind = docRel.startsWith('workflow/specs/') ? 'spec' : 'plan';
|
|
188
|
+
return draftGateFor(kind, root, base);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// ---- done 前置门族(2026-09-30 confirm-gate-approved-history)----
|
|
192
|
+
// 背景:stage-gate-machine 关单后检查 14 报「确认态缺失」×2——approved→done 期间从未提交、首次提交即
|
|
193
|
+
// done 态(先 done 后提交),git 历史无 approved 中间态;检查 14 是事后审计(发现即定局),本族把
|
|
194
|
+
// 校验收口到 confirm-doc 的「→done」跳转——「approved 态还在工作区」的最后一刻,即唯一可拦截窗口。
|
|
195
|
+
// 命中判据单源:approvedTraceHit 命令行与 check-loop 检查 14 逐字一致(-G '^状态:[[:space:]]*approved'),
|
|
196
|
+
// 本模块为唯一实现、两调用方共享(2026-09-30 复核 N3 教训:双份字面量靠注释同步必漂移)。
|
|
197
|
+
// 注(2026-09-30 复核 P2-2 登记):gitRun 的 GIT 解析 / maxBuffer / 失败语义与 check-loop.mjs 的
|
|
198
|
+
// gitOut、gitReady 与检查 14 段级两条 rev-parse 为**同口径的两份实现**(未合并——检查 14 段结构不动
|
|
199
|
+
// 属本单非目标)——任一侧调整(如加 --no-pager)须同步另一侧,防段级门与文档级判据分叉。
|
|
200
|
+
const GIT = process.platform === 'win32' ? 'git.exe' : 'git';
|
|
201
|
+
|
|
202
|
+
function gitRun(root, args) {
|
|
203
|
+
const r = spawnSync(GIT, args, { cwd: root, encoding: 'utf8', maxBuffer: 32 * 1024 * 1024 });
|
|
204
|
+
if (r.error || r.status !== 0) return null;
|
|
205
|
+
return r.stdout || '';
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// gitReady:git 环境可用(在仓库内且已有 HEAD)——与检查 14 段级两条 rev-parse 同口径
|
|
209
|
+
export function gitReady(root) {
|
|
210
|
+
return gitRun(root, ['rev-parse', '--git-dir']) !== null
|
|
211
|
+
&& gitRun(root, ['rev-parse', '-q', '--verify', 'HEAD']) !== null;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// approvedTraceHit:命中判定单源——git 历史是否出现过行首「状态: approved」(经提交进入历史才算,
|
|
215
|
+
// 当前工作区未提交的 approved 不算——这正是「留痕」语义)。rel = 仓库根相对路径。
|
|
216
|
+
// 返回:'' = 从未出现;<sha> = 命中;null = git 调用失败(调用方按各自容错口径处理——检查 14 不报、
|
|
217
|
+
// done 门跳过,均沿 fail-open 边界;诚实边界声明见 spec)。
|
|
218
|
+
export function approvedTraceHit(root, rel) {
|
|
219
|
+
const r = gitRun(root, ['log', '-1', '--format=%H', '-G', '^状态:[[:space:]]*approved', '--', rel]);
|
|
220
|
+
return r === null ? null : r.trim();
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// doneGateFor:confirm-doc「→done」前置门(三件套 intent/spec/plan 均适用;incidents 的 fixed/closed 不在本门)。
|
|
224
|
+
// 豁免与顺序:① 行首 `流程: legacy` ② 行含「存量确认态豁免(」声明(与检查 14 同标记,显式逃生口)
|
|
225
|
+
// ③ 非 git 环境 ④ git 调用失败 → 均跳过(不误拦)。未豁免且未命中 → 拒绝(调用方负责非零退出 +
|
|
226
|
+
// 零落账 + 零写盘 + 回退指引)。
|
|
227
|
+
export function doneGateFor(root, abs, rel) {
|
|
228
|
+
const body = readBody(abs);
|
|
229
|
+
if (body.split(/\r?\n/).some((l) => /^流程: legacy/.test(l))) return { ok: true, skipped: 'legacy' };
|
|
230
|
+
if (body.split(/\r?\n/).some((l) => l.includes('存量确认态豁免('))) return { ok: true, skipped: 'declared' };
|
|
231
|
+
if (!gitReady(root)) return { ok: true, skipped: 'no-git' };
|
|
232
|
+
const hit = approvedTraceHit(root, rel);
|
|
233
|
+
if (hit === null) return { ok: true, skipped: 'git-error' };
|
|
234
|
+
if (hit === '') {
|
|
235
|
+
return {
|
|
236
|
+
ok: false,
|
|
237
|
+
reason: 'git 历史中从未出现行首「状态: approved」——确认态未留痕(先 done 后提交 / 两跳同批提交)',
|
|
238
|
+
hint: '先把该文档的 approved 态提交(git add 该文件并 commit)后再重试关单',
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
return { ok: true, hit };
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// resolveWorkspace:从输出路径推导工作区根与主题名;output 路径不含 workflow 段 → null(仓外草稿,不校验)
|
|
245
|
+
export function resolveWorkspace(outputPath, rootOverride) {
|
|
246
|
+
const abs = path.resolve(outputPath);
|
|
247
|
+
const base = path.basename(abs).replace(/\.md$/, '');
|
|
248
|
+
if (rootOverride) return { root: path.resolve(rootOverride), base };
|
|
249
|
+
const parts = abs.split(path.sep);
|
|
250
|
+
for (let i = parts.length - 2; i >= 0; i--) {
|
|
251
|
+
if (parts[i].toLowerCase() === 'workflow') return { root: parts.slice(0, i).join(path.sep) || path.sep, base };
|
|
252
|
+
}
|
|
253
|
+
return null;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
export default { entryConfirmed, specConfirmed, draftGateFor, confirmGateFor, doneGateFor, gitReady, approvedTraceHit, resolveWorkspace, readFm, readLedger };
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// trust-mode: AI 自治信任等级管理(2026-09-30 ai-autonomy-trust;同日 hybrid-governance-risk-lanes 加名称双轨)
|
|
3
|
+
// 管理 .agents/trust-mode.json,授权 AI 对 L0/L1 文档进行自治放行(--auto)。
|
|
4
|
+
// 等级语义(名称 ↔ 数值双轨:confirm-doc.mjs 读数值 enabled/level,人机接口名称与数值皆收):
|
|
5
|
+
// Strict (level 0, enabled=false): AI 自治关闭(默认)——confirm-doc --auto 一律拒绝
|
|
6
|
+
// Standard (level 1): AI 可自批 L0/L1 draft -> approved
|
|
7
|
+
// Trusted (level 2): AI 可自批 L0/L1 draft -> approved 及 approved -> done(全闭环)
|
|
8
|
+
// 安全默认值:未开启(enabled: false)时,confirm-doc.mjs --auto 一律拒绝。
|
|
9
|
+
import fs from 'node:fs';
|
|
10
|
+
import path from 'node:path';
|
|
11
|
+
|
|
12
|
+
const CONFIG_PATH = path.join(process.cwd(), '.agents', 'trust-mode.json');
|
|
13
|
+
const NAME_BY_LEVEL = { 0: 'Strict', 1: 'Standard', 2: 'Trusted' };
|
|
14
|
+
const LEVEL_BY_NAME = { Strict: 0, Standard: 1, Trusted: 2 };
|
|
15
|
+
|
|
16
|
+
function readConfig() {
|
|
17
|
+
try {
|
|
18
|
+
return JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf8'));
|
|
19
|
+
} catch {
|
|
20
|
+
return { enabled: false, level: 0, name: 'Strict', enabledAt: null };
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function writeConfig(config) {
|
|
25
|
+
fs.mkdirSync(path.dirname(CONFIG_PATH), { recursive: true });
|
|
26
|
+
fs.writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2));
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function main() {
|
|
30
|
+
const args = process.argv.slice(2);
|
|
31
|
+
const config = readConfig();
|
|
32
|
+
|
|
33
|
+
if (args.includes('--enable')) {
|
|
34
|
+
const levelIdx = args.indexOf('--level');
|
|
35
|
+
const raw = levelIdx !== -1 ? String(args[levelIdx + 1] ?? '') : '1';
|
|
36
|
+
const level = LEVEL_BY_NAME[raw] ?? (['0', '1', '2'].includes(raw) ? Number(raw) : NaN);
|
|
37
|
+
if (![0, 1, 2].includes(level)) {
|
|
38
|
+
console.error('❌ --level 必须为 Strict|Standard|Trusted(或 0|1|2)');
|
|
39
|
+
process.exit(1);
|
|
40
|
+
}
|
|
41
|
+
if (level === 0) {
|
|
42
|
+
writeConfig({ enabled: false, level: 0, name: 'Strict', enabledAt: null });
|
|
43
|
+
console.log('🔒 AI 自治模式已关闭(Strict)');
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
writeConfig({ enabled: true, level, name: NAME_BY_LEVEL[level], enabledAt: new Date().toISOString() });
|
|
47
|
+
console.log(`✅ AI 自治模式已开启 (${NAME_BY_LEVEL[level]} / Level ${level})`);
|
|
48
|
+
console.log(level === 1 ? ' 授权范围:L0/L1 文档 draft -> approved' : ' 授权范围:L0/L1 文档全闭环 (approved + done)');
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (args.includes('--disable')) {
|
|
53
|
+
writeConfig({ enabled: false, level: 0, name: 'Strict', enabledAt: null });
|
|
54
|
+
console.log('🔒 AI 自治模式已关闭 (Strict)');
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (args.includes('--status')) {
|
|
59
|
+
console.log(JSON.stringify({ name: NAME_BY_LEVEL[config.level] || 'Strict', ...config }, null, 2));
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
console.log('用法: node .agents/scripts/trust-mode.mjs [--enable --level <Strict|Standard|Trusted|1|2> | --disable | --status]');
|
|
64
|
+
process.exit(config.enabled ? 0 : 1);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
main();
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
// node .agents/scripts/wf-journal.mjs add --wf <编排名> --stage <id> --status <pass|fail|blocked>
|
|
11
11
|
// [--form role|step|gate|human] [--attempt N] [--note <文本>] [--run <runId>] [--new-run]
|
|
12
12
|
// [--journal <文件>](默认 .agents/cache/orchestration-runs.jsonl,按 cwd)
|
|
13
|
-
// node .agents/scripts/wf-journal.mjs status --wf <编排名> [--run <runId>] [--journal <文件>] [--wf-root <目录>]
|
|
13
|
+
// node .agents/scripts/wf-journal.mjs status --wf <编排名> [--run <runId>] [--journal <文件>] [--wf-root <目录>] [--delegations <文件>]
|
|
14
14
|
// 测试:node templates/_agents/scripts/wf-journal.test.mjs(fixture + 真实仓库场景)
|
|
15
15
|
import fs from 'node:fs';
|
|
16
16
|
import path from 'node:path';
|
|
@@ -151,8 +151,77 @@ if (isMain) {
|
|
|
151
151
|
console.log(` ▶ 就绪(${plan.ready.length}):${plan.ready.map((s) => (s.retry ? `${s.id}(重试就绪×${s.retry})` : s.id)).join('、') || '无'}`);
|
|
152
152
|
console.log(` ⏸ 待定(${plan.pending.length}):${plan.pending.map((s) => s.id + (s.depBlocked ? '(依赖中止)' : '')).join('、') || '无'}`);
|
|
153
153
|
console.log(` ⛔ 中止(${plan.blocked.length}):${plan.blocked.map((s) => `${s.id}${s.note ? `(${s.note})` : ''}`).join('、') || '无'}`);
|
|
154
|
+
const wfLines = fs.readFileSync(wfFile, 'utf8').split(/\r?\n/);
|
|
155
|
+
const gaps = unmatchedPasses({
|
|
156
|
+
stages, state, wfLines,
|
|
157
|
+
delegationsText: readDelegationsText(typeof a.delegations === 'string' ? a.delegations : null),
|
|
158
|
+
});
|
|
159
|
+
if (gaps.length) console.log(` 委派台账未记录(${gaps.length}):${gaps.join('、')}`);
|
|
154
160
|
process.exit(0);
|
|
155
161
|
}
|
|
156
162
|
fail(`未知命令:${a.cmd ?? ''}(add | status,用法见文件头注释)`);
|
|
157
163
|
}
|
|
158
|
-
|
|
164
|
+
function readDelegationsText(file) {
|
|
165
|
+
const p = file || path.join(process.cwd(), 'workflow', 'delegations.md');
|
|
166
|
+
try { return fs.readFileSync(p, 'utf8'); } catch { return ''; }
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function delegationSectionRows(text) {
|
|
170
|
+
const lines = String(text || '').split(/\r?\n/);
|
|
171
|
+
let inSec = false;
|
|
172
|
+
const rows = [];
|
|
173
|
+
for (const line of lines) {
|
|
174
|
+
if (/^##\s+/.test(line)) {
|
|
175
|
+
if (inSec) break;
|
|
176
|
+
if (/^##\s+委派结果\s*$/.test(line)) inSec = true;
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
if (!inSec || !/^\s*\|/.test(line)) continue;
|
|
180
|
+
const cells = line.trim().replace(/^\|/, '').replace(/\|$/, '').split('|').map((c) => c.trim());
|
|
181
|
+
if (!cells.length || cells.every((c) => /^[-: ]*$/.test(c))) continue;
|
|
182
|
+
if (!/^\d{4}-\d{2}-\d{2}$/.test(cells[0])) continue;
|
|
183
|
+
rows.push({ date: cells[0], line });
|
|
184
|
+
}
|
|
185
|
+
return rows;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function mdBasenames(text) {
|
|
189
|
+
const out = [];
|
|
190
|
+
const re = /[^\s|`]+?\.md/g;
|
|
191
|
+
let m;
|
|
192
|
+
while ((m = re.exec(String(text || '')))) {
|
|
193
|
+
const base = m[0].replace(/\\/g, '/').split('/').pop();
|
|
194
|
+
if (base && !out.includes(base)) out.push(base);
|
|
195
|
+
}
|
|
196
|
+
return out;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function idInRow(id, line) {
|
|
200
|
+
const escaped = String(id).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
201
|
+
return new RegExp(`(^|[^A-Za-z0-9_-])${escaped}([^A-Za-z0-9_-]|$)`).test(line);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// unmatchedPasses:末次 pass 且 role 为 implementer 或 independent-reviewer、
|
|
205
|
+
// 委派结果表没有与 pass 的 UTC 日相同、且含阶段 id 或阶段上 .md 文件名的一行。
|
|
206
|
+
export function unmatchedPasses({ stages, state, wfLines, delegationsText }) {
|
|
207
|
+
const rows = delegationSectionRows(delegationsText);
|
|
208
|
+
const gaps = [];
|
|
209
|
+
for (const s of stages) {
|
|
210
|
+
if (s.role !== 'implementer' && s.role !== 'independent-reviewer') continue;
|
|
211
|
+
const st = state[s.id];
|
|
212
|
+
if (st?.status !== 'pass') continue;
|
|
213
|
+
const day = String(st.ts || '').slice(0, 10);
|
|
214
|
+
if (!/^\d{4}-\d{2}-\d{2}$/.test(day)) continue;
|
|
215
|
+
const raw = wfLines[(s._line || 0) - 1] || '';
|
|
216
|
+
const names = [...mdBasenames(s.files), ...mdBasenames(st.note), ...mdBasenames(raw)];
|
|
217
|
+
const hit = rows.some((r) => {
|
|
218
|
+
if (r.date !== day) return false;
|
|
219
|
+
if (idInRow(s.id, r.line)) return true;
|
|
220
|
+
return names.some((name) => r.line.includes(name));
|
|
221
|
+
});
|
|
222
|
+
if (!hit) gaps.push(s.id);
|
|
223
|
+
}
|
|
224
|
+
return gaps;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
export default { replayRun, planStages, latestRun, readJournalLines, unmatchedPasses };
|