agentic-flow-kit 2.0.0 → 2.1.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/LICENSE +21 -0
- package/README.md +92 -23
- package/bin/flow-kit.mjs +3 -1
- package/locales/en/installer.json +87 -2
- package/locales/en/overrides/.codebuddy/skills/tfs-pr/SKILL.md +1 -1
- package/locales/en/overrides/workflow/README.md +2 -0
- package/locales/zh-CN/installer.json +87 -2
- package/package.json +2 -2
- package/schemas/install-manifest.schema.json +3 -1
- package/schemas/project-profile.schema.json +1 -1
- package/src/cli/args.mjs +3 -1
- package/src/cli/main.mjs +9 -2
- package/src/commands/doctor.mjs +50 -4
- package/src/commands/install.mjs +160 -13
- package/src/hosts/registry.mjs +154 -10
- package/src/i18n.mjs +3 -0
- package/src/install/apply.mjs +17 -3
- package/src/install/frontmatter.mjs +84 -0
- package/src/install/plan.mjs +174 -16
- package/src/models/known.mjs +13 -0
- package/src/models/probe.mjs +163 -0
- package/src/models/resolve.mjs +312 -0
- package/src/render/tokens.mjs +56 -9
- package/src/tui/ansi.mjs +55 -0
- package/src/tui/flows.mjs +306 -0
- package/src/tui/prompts.mjs +139 -0
- package/src/tui/screen.mjs +217 -0
- package/src/tui/widgets.mjs +111 -0
- package/src/tui/wizard.mjs +68 -0
- package/templates/engine/.agents/commands/new-task.md +2 -2
- package/templates/engine/.agents/commands/pipeline-run.md +5 -1
- package/templates/engine/.agents/commands/stages.md +2 -1
- package/templates/engine/.agents/config/agent-eval.json +17 -0
- package/templates/engine/.agents/config/mechanism-registry.md +2 -1
- package/templates/engine/.agents/config/memory-tiers.json +13 -0
- package/templates/engine/.agents/config/pipeline.json +36 -0
- package/templates/engine/.agents/config/steering.json +28 -0
- package/templates/engine/.agents/evals/README.md +49 -0
- package/templates/engine/.agents/evals/tasks.tsv +6 -0
- package/templates/engine/.agents/hooks/pre-write-check.cjs +27 -0
- package/templates/engine/.agents/notes/injected-memory.md +14 -0
- package/templates/engine/.agents/notes/steering-tiers.md +28 -0
- package/templates/engine/.agents/scripts/agent-eval.mjs +268 -0
- package/templates/engine/.agents/scripts/check-pipeline-state.mjs +85 -0
- package/templates/engine/.agents/scripts/claims.mjs +166 -0
- package/templates/engine/.agents/scripts/dream-run.mjs +78 -3
- package/templates/engine/.agents/scripts/fix-task.mjs +77 -0
- package/templates/engine/.agents/scripts/memory-tier.mjs +196 -0
- package/templates/engine/.agents/scripts/memory-wiki.mjs +296 -0
- package/templates/engine/.agents/scripts/outcome-metrics.mjs +195 -0
- package/templates/engine/.agents/scripts/path-lease.mjs +177 -0
- package/templates/engine/.agents/scripts/session-log.mjs +157 -0
- package/templates/engine/.agents/scripts/steering.mjs +75 -0
- package/templates/engine/.agents/scripts/verify.mjs +16 -5
- package/templates/engine/.agents/skills/arena/SKILL.md +4 -0
- package/templates/engine/.agents/skills/dream/SKILL.md +21 -2
- package/templates/engine/.agents/skills/eval/SKILL.md +16 -0
- package/templates/engine/.agents/skills/model-upgrade/SKILL.md +41 -0
- package/templates/engine/.agents/skills/session-resume/SKILL.md +7 -0
- package/templates/engine/.agents/skills/show-me-your-work/SKILL.md +14 -0
- package/templates/engine/.agents/skills/wiki/SKILL.md +4 -1
- package/templates/git/.gitattributes +1 -0
- package/templates/git/.githooks/package.json +3 -0
- package/templates/git/.github/workflows/ci.yml +32 -0
- package/templates/git/azure-pipelines.yml +46 -0
- package/templates/host-meta/SKILLS.md +3 -0
- package/templates/host-meta/claude/skill-pointer.md +6 -0
- package/templates/host-meta/cline/skill-index.md +6 -0
- package/templates/host-meta/codebuddy/skill-pointer.md +6 -0
- package/templates/host-meta/commandcode/skill-index.md +5 -0
- package/templates/host-meta/opencode/skill-pointer.md +6 -0
- package/templates/host-meta/qoder/skill-index.md +5 -0
- package/templates/host-meta/qwen/skill-index.md +5 -0
- package/templates/host-meta/trae/skill-index.md +6 -0
- package/templates/hosts/claude/settings.hardened.json +94 -0
- package/templates/hosts/claude/settings.json +1 -3
- package/templates/hosts/commandcode/settings.json +1 -3
- package/templates/hosts/qoder/settings.json +1 -3
- package/templates/hosts/qwen/settings.json +1 -3
- package/templates/profiles/go/AGENTS.md +52 -0
- package/templates/profiles/java/AGENTS.md +52 -0
- package/templates/profiles/rust/AGENTS.md +52 -0
- package/templates/profiles/typescript/AGENTS.md +52 -0
- package/templates/scaffold/workflow/INDEX.md +16 -0
- package/templates/scaffold/workflow/README.md +2 -0
- package/templates/scaffold/workflow/artifact-sources.md +25 -0
- package/templates/scaffold/workflow/non-engineer-intent.md +51 -0
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// agent-eval.mjs — 行为评测门(AI-native QA)
|
|
3
|
+
//
|
|
4
|
+
// 背景(Anthropic《The AI-native SDLC playbook》):evals 是 AI 时代的 QA。
|
|
5
|
+
// 当技能 / 命令 / 角色 / 模型别名变化时,静态门无法回答“行为是否还一样”,
|
|
6
|
+
// 必须用固定任务集跑一次行为评测,并把结果与「被改动的配置面」绑定成凭证。
|
|
7
|
+
//
|
|
8
|
+
// 本脚本只做三件事,不调用任何模型:
|
|
9
|
+
// 1. 给「评测敏感面」算一个内容签名(skills/commands/roles/model 配置等);
|
|
10
|
+
// 2. record 落一条评测凭证(通过率 + 任务数 + 模型 + 签名);
|
|
11
|
+
// 3. check 校验当前签名是否有新鲜、达标、可追溯的凭证。
|
|
12
|
+
//
|
|
13
|
+
// 评测本身仍由既有 `.agents/skills/eval/SKILL.md` 的任务集流程执行;
|
|
14
|
+
// 本脚本负责把「跑过没有、跑的是不是当前配置」变成机器可判的门。
|
|
15
|
+
//
|
|
16
|
+
// 用法:
|
|
17
|
+
// node .agents/scripts/agent-eval.mjs check [--advisory] [--json]
|
|
18
|
+
// node .agents/scripts/agent-eval.mjs record --pass-rate 0.9 --tasks 10 --model opus [--note "..."]
|
|
19
|
+
// node .agents/scripts/agent-eval.mjs status [--json]
|
|
20
|
+
// node .agents/scripts/agent-eval.mjs signature
|
|
21
|
+
//
|
|
22
|
+
// 选项:
|
|
23
|
+
// --root <dir> 目标根(默认 cwd)
|
|
24
|
+
// --files a,b 只对指定文件算签名(测试/定向复核)
|
|
25
|
+
// --threshold <0..1> 通过率阈值(默认取配置)
|
|
26
|
+
// --max-age-days <N> 凭证有效期(默认取配置)
|
|
27
|
+
//
|
|
28
|
+
// 退出码:check 通过 0;无凭证/过期/不达标 1(--advisory 时仍为 0 并打 WARN)。
|
|
29
|
+
import fs from 'node:fs';
|
|
30
|
+
import path from 'node:path';
|
|
31
|
+
import crypto from 'node:crypto';
|
|
32
|
+
|
|
33
|
+
export const DEFAULT_CONFIG = {
|
|
34
|
+
version: 1,
|
|
35
|
+
// 评测敏感面:这些文件变化会改变智能体行为。
|
|
36
|
+
sensitive: [
|
|
37
|
+
'.agents/skills/**',
|
|
38
|
+
'.agents/commands/**',
|
|
39
|
+
'.agents/roles/**',
|
|
40
|
+
'.agents/config/model-roles.json',
|
|
41
|
+
'.agents/config/steering.json',
|
|
42
|
+
'.agents/config/pipeline.json',
|
|
43
|
+
// 门禁自身的阈值/敏感面提交也属于“行为面”变更:改配置必须重新评测。
|
|
44
|
+
'.agents/config/agent-eval.json',
|
|
45
|
+
],
|
|
46
|
+
results: '.agents/evals/results.jsonl',
|
|
47
|
+
threshold: 0.8,
|
|
48
|
+
maxAgeDays: 30,
|
|
49
|
+
// 凭证必须可追溯:少于该任务数不接受 record(eval/SKILL.md 要求 5–10 条)
|
|
50
|
+
minTasks: 5,
|
|
51
|
+
advisory: true,
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const SKIP_DIRS = new Set(['.git', 'node_modules', '.agents/cache', '.agents/tmp', 'tmp']);
|
|
55
|
+
|
|
56
|
+
function readJson(file) {
|
|
57
|
+
try {
|
|
58
|
+
return JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
59
|
+
} catch {
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function loadConfig(root) {
|
|
65
|
+
const file = path.join(root, '.agents', 'config', 'agent-eval.json');
|
|
66
|
+
const raw = readJson(file);
|
|
67
|
+
return { ...DEFAULT_CONFIG, ...(raw || {}) };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function globToRegExp(glob) {
|
|
71
|
+
const g = String(glob).replace(/\\/g, '/');
|
|
72
|
+
let out = '';
|
|
73
|
+
for (let i = 0; i < g.length; i += 1) {
|
|
74
|
+
const c = g[i];
|
|
75
|
+
if (c === '*') {
|
|
76
|
+
if (g[i + 1] === '*') {
|
|
77
|
+
out += '.*';
|
|
78
|
+
i += 1;
|
|
79
|
+
if (g[i + 1] === '/') i += 1;
|
|
80
|
+
} else out += '[^/]*';
|
|
81
|
+
} else if (c === '?') out += '[^/]';
|
|
82
|
+
else out += c.replace(/[.+^${}()|[\]\\]/g, '\\$&');
|
|
83
|
+
}
|
|
84
|
+
return new RegExp(`^${out}$`);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function isSensitive(rel, patterns) {
|
|
88
|
+
const norm = String(rel).replace(/\\/g, '/');
|
|
89
|
+
return (patterns || []).some((p) => globToRegExp(p).test(norm));
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function walk(root, rel, out) {
|
|
93
|
+
let ents;
|
|
94
|
+
try {
|
|
95
|
+
ents = fs.readdirSync(path.join(root, rel), { withFileTypes: true });
|
|
96
|
+
} catch {
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
for (const ent of ents) {
|
|
100
|
+
const childRel = rel ? `${rel}/${ent.name}` : ent.name;
|
|
101
|
+
if (ent.isDirectory()) {
|
|
102
|
+
if (SKIP_DIRS.has(childRel)) continue;
|
|
103
|
+
walk(root, childRel, out);
|
|
104
|
+
} else if (ent.isFile()) out.push(childRel);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function sensitiveFiles(root, cfg = loadConfig(root), only = null) {
|
|
109
|
+
let files = [];
|
|
110
|
+
if (only && only.length) files = only.map((f) => String(f).replace(/\\/g, '/'));
|
|
111
|
+
else walk(root, '', files);
|
|
112
|
+
return files.filter((f) => isSensitive(f, cfg.sensitive)).sort();
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// 内容签名:只覆盖评测敏感面,避免无关改动让凭证失效。
|
|
116
|
+
export function treeSignature(root, cfg = loadConfig(root), only = null) {
|
|
117
|
+
const files = sensitiveFiles(root, cfg, only);
|
|
118
|
+
const h = crypto.createHash('sha256');
|
|
119
|
+
for (const rel of files) {
|
|
120
|
+
let buf;
|
|
121
|
+
try {
|
|
122
|
+
buf = fs.readFileSync(path.join(root, rel));
|
|
123
|
+
} catch {
|
|
124
|
+
buf = Buffer.from('<missing>');
|
|
125
|
+
}
|
|
126
|
+
h.update(rel);
|
|
127
|
+
h.update('\0');
|
|
128
|
+
h.update(buf);
|
|
129
|
+
h.update('\0');
|
|
130
|
+
}
|
|
131
|
+
return { signature: h.digest('hex'), files };
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export function readResults(root, cfg = loadConfig(root)) {
|
|
135
|
+
const file = path.join(root, cfg.results);
|
|
136
|
+
let raw = '';
|
|
137
|
+
try {
|
|
138
|
+
raw = fs.readFileSync(file, 'utf8');
|
|
139
|
+
} catch {
|
|
140
|
+
return [];
|
|
141
|
+
}
|
|
142
|
+
const rows = [];
|
|
143
|
+
for (const line of raw.split(/\r?\n/)) {
|
|
144
|
+
if (!line.trim()) continue;
|
|
145
|
+
try {
|
|
146
|
+
rows.push(JSON.parse(line));
|
|
147
|
+
} catch {
|
|
148
|
+
/* 坏行忽略:凭证是 append-only 的,不能因一行损坏阻塞 */
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
return rows;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function ageDays(ts, now = Date.now()) {
|
|
155
|
+
const t = Date.parse(ts);
|
|
156
|
+
if (!Number.isFinite(t)) return Infinity;
|
|
157
|
+
return (now - t) / 86400000;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export function evaluate(root, { now = Date.now(), only = null } = {}) {
|
|
161
|
+
const cfg = loadConfig(root);
|
|
162
|
+
const { signature, files } = treeSignature(root, cfg, only);
|
|
163
|
+
if (!files.length) {
|
|
164
|
+
return { ok: true, reason: 'no-sensitive-files', signature, files: [], cfg };
|
|
165
|
+
}
|
|
166
|
+
const rows = readResults(root, cfg);
|
|
167
|
+
const hits = rows.filter((r) => r && r.signature === signature);
|
|
168
|
+
const fresh = hits.filter((r) => ageDays(r.ts, now) <= cfg.maxAgeDays);
|
|
169
|
+
const passing = fresh.filter((r) => Number(r.passRate) >= cfg.threshold);
|
|
170
|
+
if (!hits.length) return { ok: false, reason: 'no-eval-for-tree', signature, files, cfg };
|
|
171
|
+
if (!fresh.length) return { ok: false, reason: 'eval-expired', signature, files, cfg };
|
|
172
|
+
if (!passing.length) return { ok: false, reason: 'eval-below-threshold', signature, files, cfg };
|
|
173
|
+
const latest = passing.sort((a, b) => Date.parse(b.ts) - Date.parse(a.ts))[0];
|
|
174
|
+
return { ok: true, reason: 'pass', signature, files, cfg, record: latest };
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export function recordEval(root, { passRate, tasks = 0, model = '', note = '', signature = null, now = new Date() } = {}) {
|
|
178
|
+
const rate = Number(passRate);
|
|
179
|
+
if (!Number.isFinite(rate) || rate < 0 || rate > 1) throw new Error('pass-rate 必须是 0..1');
|
|
180
|
+
const cfg = loadConfig(root);
|
|
181
|
+
const sig = signature || treeSignature(root, cfg).signature;
|
|
182
|
+
const row = {
|
|
183
|
+
ts: now.toISOString(),
|
|
184
|
+
signature: sig,
|
|
185
|
+
passRate: rate,
|
|
186
|
+
tasks: Number(tasks) || 0,
|
|
187
|
+
model: String(model || ''),
|
|
188
|
+
note: String(note || ''),
|
|
189
|
+
};
|
|
190
|
+
const file = path.join(root, cfg.results);
|
|
191
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
192
|
+
fs.appendFileSync(file, `${JSON.stringify(row)}\n`);
|
|
193
|
+
return row;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// ---- CLI ----
|
|
197
|
+
function argOf(argv, name, fallback = null) {
|
|
198
|
+
const i = argv.indexOf(`--${name}`);
|
|
199
|
+
if (i < 0) return fallback;
|
|
200
|
+
const v = argv[i + 1];
|
|
201
|
+
return v == null || v.startsWith('--') ? true : v;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function main() {
|
|
205
|
+
const argv = process.argv.slice(2);
|
|
206
|
+
const cmd = argv[0] || 'check';
|
|
207
|
+
const root = path.resolve(argOf(argv, 'root', process.cwd()));
|
|
208
|
+
const only = argOf(argv, 'files');
|
|
209
|
+
const json = argv.includes('--json');
|
|
210
|
+
|
|
211
|
+
if (cmd === 'signature') {
|
|
212
|
+
const { signature, files } = treeSignature(root, loadConfig(root), only ? String(only).split(',') : null);
|
|
213
|
+
console.log(json ? JSON.stringify({ signature, files }, null, 2) : signature);
|
|
214
|
+
return 0;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
if (cmd === 'record') {
|
|
218
|
+
const cfg = loadConfig(root);
|
|
219
|
+
const rawRate = argOf(argv, 'pass-rate');
|
|
220
|
+
const rawTasks = argOf(argv, 'tasks');
|
|
221
|
+
const rawModel = argOf(argv, 'model');
|
|
222
|
+
// 不带值(或后面紧跟另一个 flag)时 argOf 返回 true;绝不能把它当成 1.0 落账。
|
|
223
|
+
if (rawRate === true || rawRate == null || !Number.isFinite(Number(rawRate))) {
|
|
224
|
+
console.error('record 需要 --pass-rate <0..1>(必须是真实评测结果)');
|
|
225
|
+
return 2;
|
|
226
|
+
}
|
|
227
|
+
const tasks = rawTasks === true ? NaN : Number(rawTasks ?? 0);
|
|
228
|
+
const minTasks = Number(cfg.minTasks ?? 5);
|
|
229
|
+
if (!Number.isFinite(tasks) || tasks < minTasks) {
|
|
230
|
+
console.error(`record 需要 --tasks >= ${minTasks}(凭证必须可追溯)`);
|
|
231
|
+
return 2;
|
|
232
|
+
}
|
|
233
|
+
if (rawModel === true || !String(rawModel || '').trim()) {
|
|
234
|
+
console.error('record 需要 --model <评测模型>(凭证必须可追溯)');
|
|
235
|
+
return 2;
|
|
236
|
+
}
|
|
237
|
+
const row = recordEval(root, {
|
|
238
|
+
passRate: Number(rawRate),
|
|
239
|
+
tasks,
|
|
240
|
+
model: String(rawModel).trim(),
|
|
241
|
+
note: argOf(argv, 'note', '') === true ? '' : String(argOf(argv, 'note', '')),
|
|
242
|
+
});
|
|
243
|
+
console.log(`recorded eval ${row.signature.slice(0, 12)} passRate=${row.passRate} tasks=${row.tasks}`);
|
|
244
|
+
return 0;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const result = evaluate(root, { only: only ? String(only).split(',') : null });
|
|
248
|
+
if (cmd === 'status' || json) {
|
|
249
|
+
console.log(json ? JSON.stringify(result, null, 2) : `signature=${result.signature.slice(0, 12)} ok=${result.ok} reason=${result.reason}`);
|
|
250
|
+
return 0;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const advisory = argv.includes('--advisory') || result.cfg.advisory;
|
|
254
|
+
if (result.ok) {
|
|
255
|
+
console.log(`✅ agent-eval: ${result.reason}`);
|
|
256
|
+
return 0;
|
|
257
|
+
}
|
|
258
|
+
const msg = `agent-eval: ${result.reason}(签名 ${result.signature.slice(0, 12)},${result.files.length} 个敏感文件)`;
|
|
259
|
+
if (advisory) {
|
|
260
|
+
console.log(`⚠ WARN ${msg};按 .agents/skills/eval/SKILL.md 跑一次并用 record 落凭证`);
|
|
261
|
+
return 0;
|
|
262
|
+
}
|
|
263
|
+
console.error(`✗ ${msg};按 .agents/skills/eval/SKILL.md 跑一次并用 record 落凭证`);
|
|
264
|
+
return 1;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const isMain = process.argv[1] && process.argv[1].endsWith('agent-eval.mjs');
|
|
268
|
+
if (isMain) process.exit(main());
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// check-pipeline-state.mjs — pipeline-run 状态机闭包检查(静态,零依赖)
|
|
3
|
+
//
|
|
4
|
+
// 背景(Anthropic《The AI-native SDLC playbook》Stage 6):闭环执行器的可信度
|
|
5
|
+
// 来自“状态转移是闭集”。pipeline-run 自述“保留待用、零 run”,在第一次真实 run 之前,
|
|
6
|
+
// 至少要有机器检查保证:脚本里出现的 stage / stopType 都在登记表内,登记表里的 stage
|
|
7
|
+
// 也都在脚本里可达。
|
|
8
|
+
//
|
|
9
|
+
// 这是静态合同检查,不替代一次端到端 run;它拦住“新增/改名 stage 忘了登记”的低级漂移。
|
|
10
|
+
//
|
|
11
|
+
// 用法:
|
|
12
|
+
// node .agents/scripts/check-pipeline-state.mjs [--json] [--file <path>]
|
|
13
|
+
import fs from 'node:fs';
|
|
14
|
+
import path from 'node:path';
|
|
15
|
+
|
|
16
|
+
const DEFAULT_FILE = '.agents/scripts/pipeline-run.mjs';
|
|
17
|
+
const DEFAULT_CFG = '.agents/config/pipeline.json';
|
|
18
|
+
|
|
19
|
+
export function analyze(source, config) {
|
|
20
|
+
const problems = [];
|
|
21
|
+
const stages = new Set(config.stages || []);
|
|
22
|
+
const stopTypes = new Set(config.stopTypes || []);
|
|
23
|
+
|
|
24
|
+
const stageLiterals = [...String(source).matchAll(/(?:run\.stage\s*=\s*|stage:\s*)'([a-z][a-z0-9-]*)'/g)].map((m) => m[1]);
|
|
25
|
+
const stopLiterals = [...String(source).matchAll(/(?:run\.stopType\s*=\s*|stopType:\s*)'([a-z][a-z0-9-]*)'/g)].map((m) => m[1]);
|
|
26
|
+
const handled = [...String(source).matchAll(/(?:\bs\s*===\s*)'([a-z][a-z0-9-]*)'/g)].map((m) => m[1]);
|
|
27
|
+
|
|
28
|
+
const seenStages = new Set([...stageLiterals, ...handled]);
|
|
29
|
+
for (const s of seenStages) if (!stages.has(s) && s !== 'cur' && s !== 'past' && s !== 'string') {
|
|
30
|
+
problems.push(`stage 未登记:'${s}'(补 .agents/config/pipeline.json 的 stages)`);
|
|
31
|
+
}
|
|
32
|
+
for (const s of stages) {
|
|
33
|
+
if (!seenStages.has(s)) problems.push(`登记的 stage 在 pipeline-run 中不可达:'${s}'`);
|
|
34
|
+
}
|
|
35
|
+
for (const s of new Set(stopLiterals)) {
|
|
36
|
+
if (!stopTypes.has(s)) problems.push(`stopType 未登记:'${s}'(补 stopTypes)`);
|
|
37
|
+
}
|
|
38
|
+
for (const s of stopTypes) {
|
|
39
|
+
if (!stopLiterals.includes(s)) problems.push(`登记的 stopType 在脚本中未出现:'${s}'`);
|
|
40
|
+
}
|
|
41
|
+
if (!seenStages.has('triage')) problems.push("初始 stage 'triage' 未出现");
|
|
42
|
+
|
|
43
|
+
return {
|
|
44
|
+
ok: problems.length === 0,
|
|
45
|
+
problems,
|
|
46
|
+
counts: { stages: stageLiterals.length, handled: handled.length, stopTypes: stopLiterals.length },
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function argOf(argv, name, fallback = null) {
|
|
51
|
+
const i = argv.indexOf(`--${name}`);
|
|
52
|
+
if (i < 0) return fallback;
|
|
53
|
+
const v = argv[i + 1];
|
|
54
|
+
return v == null || v.startsWith('--') ? true : v;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function main() {
|
|
58
|
+
const argv = process.argv.slice(2);
|
|
59
|
+
const root = path.resolve(argOf(argv, 'root', process.cwd()));
|
|
60
|
+
const file = path.resolve(root, argOf(argv, 'file', DEFAULT_FILE));
|
|
61
|
+
const cfgFile = path.resolve(root, argOf(argv, 'config', DEFAULT_CFG));
|
|
62
|
+
if (!fs.existsSync(file)) {
|
|
63
|
+
console.error(`✗ 找不到 ${path.relative(root, file)}`);
|
|
64
|
+
return 2;
|
|
65
|
+
}
|
|
66
|
+
if (!fs.existsSync(cfgFile)) {
|
|
67
|
+
console.error(`✗ 找不到 ${path.relative(root, cfgFile)}`);
|
|
68
|
+
return 2;
|
|
69
|
+
}
|
|
70
|
+
const res = analyze(fs.readFileSync(file, 'utf8'), JSON.parse(fs.readFileSync(cfgFile, 'utf8')));
|
|
71
|
+
if (argv.includes('--json')) {
|
|
72
|
+
console.log(JSON.stringify(res, null, 2));
|
|
73
|
+
return res.ok ? 0 : 1;
|
|
74
|
+
}
|
|
75
|
+
if (!res.ok) {
|
|
76
|
+
console.error('✗ pipeline 状态机闭包检查失败:');
|
|
77
|
+
for (const p of res.problems) console.error(` - ${p}`);
|
|
78
|
+
return 1;
|
|
79
|
+
}
|
|
80
|
+
console.log(`✅ pipeline 状态机闭包(stages=${res.counts.stages} handled=${res.counts.handled} stopTypes=${res.counts.stopTypes})`);
|
|
81
|
+
return 0;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const isMain = process.argv[1] && process.argv[1].endsWith('check-pipeline-state.mjs');
|
|
85
|
+
if (isMain) process.exit(main());
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// claims.mjs — 主张账(workflow/claims.d/<YYYY-MM>.jsonl)
|
|
3
|
+
// 一条主张 = 未来会话可能当真的一句话 + 仓库里已经存在的原文片段。
|
|
4
|
+
// 脚本只回答原文还在不在:held = quote 仍在文件中,broken = 文件消失或 quote 已不在。
|
|
5
|
+
// 原文还在不等于主张为真。核对只报告,退出码恒 0,不改正文、不改历史行。
|
|
6
|
+
//
|
|
7
|
+
// 用法:
|
|
8
|
+
// node .agents/scripts/claims.mjs add --claim "..." --path <相对路径> --quote "..."
|
|
9
|
+
// node .agents/scripts/claims.mjs recheck
|
|
10
|
+
// 库:addClaim({ root, claim, path, quote, now }) / recheckClaims(root)
|
|
11
|
+
import crypto from 'node:crypto';
|
|
12
|
+
import fs from 'node:fs';
|
|
13
|
+
import path from 'node:path';
|
|
14
|
+
import { pathToFileURL } from 'node:url';
|
|
15
|
+
|
|
16
|
+
const QUOTE_MIN = 8;
|
|
17
|
+
const QUOTE_MAX = 200;
|
|
18
|
+
const CLAIM_MAX = 400;
|
|
19
|
+
|
|
20
|
+
export function addClaim({ root, claim, path: rel, quote, now = new Date() }) {
|
|
21
|
+
const text = requireClaim(claim);
|
|
22
|
+
const snippet = requireQuote(quote);
|
|
23
|
+
const target = resolveRel(root, rel);
|
|
24
|
+
const body = readNorm(target.abs);
|
|
25
|
+
if (!body.includes(norm(snippet))) {
|
|
26
|
+
throw new Error('quote 不在文件中,拒绝写入');
|
|
27
|
+
}
|
|
28
|
+
const row = {
|
|
29
|
+
ts: now.toISOString(),
|
|
30
|
+
id: `c-${crypto.randomBytes(4).toString('hex')}`,
|
|
31
|
+
claim: text,
|
|
32
|
+
path: target.rel,
|
|
33
|
+
quote: snippet,
|
|
34
|
+
};
|
|
35
|
+
const dir = path.join(path.resolve(root), 'workflow', 'claims.d');
|
|
36
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
37
|
+
const file = path.join(dir, `${row.ts.slice(0, 7)}.jsonl`);
|
|
38
|
+
fs.appendFileSync(file, `${JSON.stringify(row)}\n`);
|
|
39
|
+
return row;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function recheckClaims(root) {
|
|
43
|
+
const dir = path.join(path.resolve(root), 'workflow', 'claims.d');
|
|
44
|
+
const results = [];
|
|
45
|
+
if (!fs.existsSync(dir)) return { held: 0, broken: 0, results };
|
|
46
|
+
const files = fs.readdirSync(dir).filter((n) => /^\d{4}-\d{2}\.jsonl$/.test(n)).sort();
|
|
47
|
+
for (const name of files) {
|
|
48
|
+
const raw = fs.readFileSync(path.join(dir, name), 'utf8');
|
|
49
|
+
for (const line of raw.split(/\r?\n/)) {
|
|
50
|
+
if (!line.trim()) continue;
|
|
51
|
+
results.push(checkLine(root, line));
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
const held = results.filter((r) => r.status === 'held').length;
|
|
55
|
+
return { held, broken: results.length - held, results };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function checkLine(root, line) {
|
|
59
|
+
let row;
|
|
60
|
+
try {
|
|
61
|
+
row = JSON.parse(line);
|
|
62
|
+
} catch {
|
|
63
|
+
return { id: '(malformed)', path: '', claim: '', status: 'broken', reason: 'malformed' };
|
|
64
|
+
}
|
|
65
|
+
const id = typeof row.id === 'string' ? row.id : '(malformed)';
|
|
66
|
+
const claim = typeof row.claim === 'string' ? row.claim : '';
|
|
67
|
+
const rel = typeof row.path === 'string' ? row.path : '';
|
|
68
|
+
const quote = typeof row.quote === 'string' ? row.quote : '';
|
|
69
|
+
if (!rel || !quote) {
|
|
70
|
+
return { id, path: rel, claim, status: 'broken', reason: 'malformed' };
|
|
71
|
+
}
|
|
72
|
+
let abs;
|
|
73
|
+
try {
|
|
74
|
+
abs = resolveRel(root, rel).abs;
|
|
75
|
+
} catch {
|
|
76
|
+
return { id, path: rel, claim, status: 'broken', reason: 'file-missing' };
|
|
77
|
+
}
|
|
78
|
+
if (!fs.existsSync(abs) || !fs.statSync(abs).isFile()) {
|
|
79
|
+
return { id, path: rel, claim, status: 'broken', reason: 'file-missing' };
|
|
80
|
+
}
|
|
81
|
+
if (!readNorm(abs).includes(norm(quote))) {
|
|
82
|
+
return { id, path: rel, claim, status: 'broken', reason: 'quote-missing' };
|
|
83
|
+
}
|
|
84
|
+
return { id, path: rel, claim, status: 'held', reason: 'quote-present' };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function requireClaim(claim) {
|
|
88
|
+
if (typeof claim !== 'string' || claim.trim() === '' || Array.from(claim).length > CLAIM_MAX) {
|
|
89
|
+
throw new Error('claim 须为一句非空文本(不超过 400 字)');
|
|
90
|
+
}
|
|
91
|
+
return claim;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function requireQuote(quote) {
|
|
95
|
+
if (typeof quote !== 'string') throw new Error(`quote 须为 ${QUOTE_MIN} 到 ${QUOTE_MAX} 字`);
|
|
96
|
+
const n = Array.from(quote).length;
|
|
97
|
+
if (n < QUOTE_MIN || n > QUOTE_MAX) {
|
|
98
|
+
throw new Error(`quote 须为 ${QUOTE_MIN} 到 ${QUOTE_MAX} 字`);
|
|
99
|
+
}
|
|
100
|
+
return quote;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function resolveRel(root, rel) {
|
|
104
|
+
if (typeof rel !== 'string' || rel === '' || path.isAbsolute(rel)) {
|
|
105
|
+
throw new Error('path 须为相对路径');
|
|
106
|
+
}
|
|
107
|
+
const posix = rel.replace(/\\/g, '/');
|
|
108
|
+
if (posix.split('/').includes('..')) throw new Error('path 禁止 ..');
|
|
109
|
+
const lower = posix.toLowerCase();
|
|
110
|
+
if (lower === 'tmp' || lower.startsWith('tmp/') || lower === '.agents/cache' || lower.startsWith('.agents/cache/')) {
|
|
111
|
+
throw new Error('path 禁止落在 tmp/ 或 .agents/cache/');
|
|
112
|
+
}
|
|
113
|
+
const rootAbs = path.resolve(root);
|
|
114
|
+
const abs = path.resolve(rootAbs, rel);
|
|
115
|
+
const back = path.relative(rootAbs, abs);
|
|
116
|
+
if (back.startsWith('..') || path.isAbsolute(back)) throw new Error('path 须留在仓库内');
|
|
117
|
+
if (!fs.existsSync(abs) || !fs.statSync(abs).isFile()) throw new Error('path 不是已存在的文件');
|
|
118
|
+
return { abs, rel: posix };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function norm(s) {
|
|
122
|
+
return s.replace(/\r\n/g, '\n');
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function readNorm(abs) {
|
|
126
|
+
return norm(fs.readFileSync(abs, 'utf8'));
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function arg(name) {
|
|
130
|
+
const i = process.argv.indexOf(name);
|
|
131
|
+
return i >= 0 ? process.argv[i + 1] : undefined;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function main() {
|
|
135
|
+
const root = arg('--root') || process.env.CHECK_LOOP_ROOT || process.cwd();
|
|
136
|
+
const cmd = process.argv[2];
|
|
137
|
+
try {
|
|
138
|
+
if (cmd === 'add') {
|
|
139
|
+
const row = addClaim({
|
|
140
|
+
root,
|
|
141
|
+
claim: arg('--claim'),
|
|
142
|
+
path: arg('--path'),
|
|
143
|
+
quote: arg('--quote'),
|
|
144
|
+
});
|
|
145
|
+
console.log(row.id);
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
if (cmd === 'recheck') {
|
|
149
|
+
const report = recheckClaims(root);
|
|
150
|
+
console.log(`held ${report.held}`);
|
|
151
|
+
console.log(`broken ${report.broken}`);
|
|
152
|
+
for (const row of report.results) {
|
|
153
|
+
if (row.status === 'broken') console.log(`broken ${row.id} ${row.path}: ${row.reason}`);
|
|
154
|
+
}
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
console.error('用法: claims.mjs add --claim --path --quote | claims.mjs recheck');
|
|
158
|
+
process.exit(2);
|
|
159
|
+
} catch (err) {
|
|
160
|
+
console.error(err.message);
|
|
161
|
+
process.exit(2);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const isMain = process.argv[1] && import.meta.url === pathToFileURL(path.resolve(process.argv[1])).href;
|
|
166
|
+
if (isMain) main();
|
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
// dream-run — 离线梦境最小审计(2026-11-11,对照 Anthropic《The components of agent memory》)
|
|
3
3
|
// 定位:可手动/定时触发的只读审计编排器。不发明新判定,只编排现有脚本完成「离线梦境」
|
|
4
4
|
// 职能:修剪陈旧(A) / 拓扑修复(B) / Judge 对账(C) / 错类候选(D) / 记忆负债(E) /
|
|
5
|
-
// 冷数据候选(E2)
|
|
5
|
+
// 冷数据候选(E2) / 主张依据(G,只报告 quote 还在不在) / 经验回放(H,会话摘要) /
|
|
6
|
+
// 记忆表示(I,memory-wiki) / 注入层预算与上浮候选(J) / 交付结果指标(K),
|
|
7
|
+
// 末尾追加 AI Judge 阶段(F):抽取活跃 workflow 文档 + git 历史 + 确认/验证台账证据,
|
|
6
8
|
// 生成抽样审计简报(tmp/dream-judge-brief-<date>.md),交由主智能体在下个会话委托
|
|
7
9
|
// independent-reviewer 角色核验——脚本只做采样与证据收集,判断由 AI 执行。
|
|
8
10
|
//
|
|
@@ -24,6 +26,7 @@ import { spawnSync } from 'node:child_process';
|
|
|
24
26
|
import fs from 'node:fs';
|
|
25
27
|
import path from 'node:path';
|
|
26
28
|
import { fileURLToPath } from 'node:url';
|
|
29
|
+
import { recheckClaims } from './claims.mjs';
|
|
27
30
|
|
|
28
31
|
const SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url));
|
|
29
32
|
const ROOT = process.env.CHECK_LOOP_ROOT || path.resolve(SCRIPT_DIR, '..', '..');
|
|
@@ -67,6 +70,7 @@ function git(args, opts = {}) {
|
|
|
67
70
|
|
|
68
71
|
const lines = [];
|
|
69
72
|
const add = (s = '') => lines.push(s);
|
|
73
|
+
const indent = (s, pad = ' ') => String(s).split('\n').map((l) => `${pad}${l}`).join('\n');
|
|
70
74
|
const section = (t) => { lines.push(`\n## ${t}\n`); };
|
|
71
75
|
|
|
72
76
|
// 非终态即活跃:intents/specs/plans 终态 = done/superseded/cancelled;incidents 终态 = closed
|
|
@@ -128,6 +132,65 @@ section('E2. 冷数据候选(agg-kb-usage --stale-days 90)');
|
|
|
128
132
|
const cold = run('.agents/scripts/agg-kb-usage.mjs', ['--stale-days', '90']);
|
|
129
133
|
add(cold.ok ? cold.out.trim() : `⚠ 冷数据统计失败: ${cold.err || cold.out}`);
|
|
130
134
|
|
|
135
|
+
// ---------- 阶段 G:主张依据(只报告,不写账) ----------
|
|
136
|
+
section('G. 主张依据(claims recheck,只报告)');
|
|
137
|
+
let claimRecheck = { held: 0, broken: 0, results: [] };
|
|
138
|
+
try {
|
|
139
|
+
claimRecheck = recheckClaims(ROOT);
|
|
140
|
+
add(`- held ${claimRecheck.held} / broken ${claimRecheck.broken}`);
|
|
141
|
+
const broken = claimRecheck.results.filter((r) => r.status === 'broken');
|
|
142
|
+
for (const row of broken) add(`- broken ${row.id} ${row.path}: ${row.reason}`);
|
|
143
|
+
if (broken.length === 0) add('- 无断依据');
|
|
144
|
+
} catch (err) {
|
|
145
|
+
add(`⚠ 主张核对失败: ${err.message}`);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// ---------- 阶段 H:经验回放(会话日志 → 摘要) ----------
|
|
149
|
+
section('H. 经验回放(session-log digest --days 7)');
|
|
150
|
+
const digestPath = path.join(TMP_DIR, `session-digest-${today()}.md`);
|
|
151
|
+
const sess = run('.agents/scripts/session-log.mjs', ['digest', '--days', '7', '--out', path.relative(ROOT, digestPath)]);
|
|
152
|
+
if (sess.ok) {
|
|
153
|
+
add(`- 摘要: ${path.relative(ROOT, digestPath)}`);
|
|
154
|
+
try {
|
|
155
|
+
const excerpt = fs.readFileSync(digestPath, 'utf8').split(/\r?\n/).slice(0, 12).filter(Boolean);
|
|
156
|
+
if (excerpt.length) add(indent(excerpt.join('\n'), ' '));
|
|
157
|
+
} catch { /* 摘要读不到不影响其余阶段 */ }
|
|
158
|
+
add(' (无日志时摘要会明确写“无输入”——dreaming 不能把“没有经验”当成“没有错误”)');
|
|
159
|
+
} else {
|
|
160
|
+
add(`⚠ 会话摘要失败: ${sess.err || sess.out}`);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// ---------- 阶段 I:记忆表示(memory-wiki) ----------
|
|
164
|
+
section('I. 记忆表示(memory-wiki --check)');
|
|
165
|
+
const mw = run('.agents/scripts/memory-wiki.mjs', ['--check']);
|
|
166
|
+
if (mw.ok) {
|
|
167
|
+
add('- memory-wiki: 一致');
|
|
168
|
+
} else {
|
|
169
|
+
add(`- memory-wiki: 漂移\n ${mw.out.split('\n').filter(Boolean).slice(0, 8).join('\n ')}`);
|
|
170
|
+
if (EXECUTE) {
|
|
171
|
+
const regen = run('.agents/scripts/memory-wiki.mjs', []);
|
|
172
|
+
add(regen.ok ? ' → 已重生成' : ` → 重生成失败: ${regen.err || regen.out}`);
|
|
173
|
+
} else {
|
|
174
|
+
add(' → 待生成(--execute)');
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// ---------- 阶段 J:注入层预算与上浮候选 ----------
|
|
179
|
+
section('J. 注入层预算与上浮候选(memory-tier)');
|
|
180
|
+
const tierAudit = run('.agents/scripts/memory-tier.mjs', ['audit']);
|
|
181
|
+
add(tierAudit.ok ? '- 注入层: 在预算内' : `- 注入层超限:\n ${tierAudit.out.split('\n').filter(Boolean).slice(0, 6).join('\n ')}`);
|
|
182
|
+
const tierSuggest = run('.agents/scripts/memory-tier.mjs', ['suggest', '--days', '90']);
|
|
183
|
+
add(tierSuggest.ok ? tierSuggest.out.trim() : `⚠ 上浮候选失败: ${tierSuggest.err || tierSuggest.out}`);
|
|
184
|
+
|
|
185
|
+
// ---------- 阶段 K:交付结果指标 ----------
|
|
186
|
+
section('K. 交付结果指标(outcome-metrics --days 30)');
|
|
187
|
+
const om = run('.agents/scripts/outcome-metrics.mjs', ['--days', '30']);
|
|
188
|
+
if (om.ok) {
|
|
189
|
+
add(om.out.split('\n').slice(0, 18).join('\n'));
|
|
190
|
+
} else {
|
|
191
|
+
add(`⚠ 指标聚合失败: ${om.err || om.out}`);
|
|
192
|
+
}
|
|
193
|
+
|
|
131
194
|
// ---------- 阶段 F:AI Judge 抽样简报 ----------
|
|
132
195
|
let judgeBriefPath = '';
|
|
133
196
|
if (!NO_JUDGE && JUDGE_COUNT > 0) {
|
|
@@ -137,7 +200,7 @@ if (!NO_JUDGE && JUDGE_COUNT > 0) {
|
|
|
137
200
|
if (sample.docs.length === 0) {
|
|
138
201
|
add('- 无活跃 workflow 文档,跳过抽样');
|
|
139
202
|
} else {
|
|
140
|
-
writeJudgeBrief(judgeBriefPath, sample);
|
|
203
|
+
writeJudgeBrief(judgeBriefPath, sample, claimRecheck);
|
|
141
204
|
add(`- 已抽取 ${sample.docs.length} 份活跃文档,证据简报: ${path.relative(ROOT, judgeBriefPath)}`);
|
|
142
205
|
add('- 下一步: 主智能体读取简报,按末尾模板委托 independent-reviewer 核验');
|
|
143
206
|
}
|
|
@@ -243,7 +306,7 @@ function gatherEvidence(doc) {
|
|
|
243
306
|
return ev;
|
|
244
307
|
}
|
|
245
308
|
|
|
246
|
-
function writeJudgeBrief(briefPath, sample) {
|
|
309
|
+
function writeJudgeBrief(briefPath, sample, claimRecheck = { results: [] }) {
|
|
247
310
|
const parts = [];
|
|
248
311
|
parts.push(`# AI Judge 抽样审计简报 ${today()}`);
|
|
249
312
|
parts.push('');
|
|
@@ -262,6 +325,18 @@ function writeJudgeBrief(briefPath, sample) {
|
|
|
262
325
|
const ev = gatherEvidence(doc);
|
|
263
326
|
if (ev.length) parts.push('', ev.join('\n\n'));
|
|
264
327
|
});
|
|
328
|
+
const brokenClaims = (claimRecheck.results || []).filter((r) => r.status === 'broken');
|
|
329
|
+
if (brokenClaims.length) {
|
|
330
|
+
parts.push('');
|
|
331
|
+
parts.push('## 主张依据已断');
|
|
332
|
+
parts.push('');
|
|
333
|
+
parts.push('> 脚本只核对原文还在不在。断了不等于主张为假,还在也不等于主张为真。');
|
|
334
|
+
parts.push('');
|
|
335
|
+
for (const row of brokenClaims) {
|
|
336
|
+
parts.push(`- ${row.id} \`${row.path}\`: ${row.reason}`);
|
|
337
|
+
parts.push(` 主张: ${row.claim}`);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
265
340
|
parts.push('');
|
|
266
341
|
parts.push('---');
|
|
267
342
|
parts.push('');
|