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,195 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// outcome-metrics.mjs — 交付结果指标(而不是机制成本)
|
|
3
|
+
//
|
|
4
|
+
// 背景(Anthropic《The AI-native SDLC playbook》):每个 play 都应回答
|
|
5
|
+
// “是否真的变好”,指标分 leading / lagging。既有 mechanism-registry 记的是
|
|
6
|
+
// 门禁触发量与退役阈值(成本面),本脚本补结果面:
|
|
7
|
+
//
|
|
8
|
+
// - 首次过门率 / 验证通过率(verifications.jsonl)
|
|
9
|
+
// - 逃逸缺陷与复盘量(workflow/incidents)
|
|
10
|
+
// - 复核意见采纳率(workflow/reviews.d)——事件级近似
|
|
11
|
+
// - 行为评测通过率与陈旧面(.agents/evals/results.jsonl)
|
|
12
|
+
// - 返工提交占比(git log,可选)
|
|
13
|
+
//
|
|
14
|
+
// 输出:workflow/metrics/outcome.md;--json 输出机器可读。
|
|
15
|
+
// 诚实边界:只聚合已有台账;缺源在报告末尾单列,不造数、不把 0 当“很好”。
|
|
16
|
+
//
|
|
17
|
+
// 用法:
|
|
18
|
+
// node .agents/scripts/outcome-metrics.mjs [--days 30] [--json] [--out <path>]
|
|
19
|
+
import fs from 'node:fs';
|
|
20
|
+
import path from 'node:path';
|
|
21
|
+
import { spawnSync } from 'node:child_process';
|
|
22
|
+
|
|
23
|
+
function readText(file) {
|
|
24
|
+
try {
|
|
25
|
+
return fs.readFileSync(file, 'utf8');
|
|
26
|
+
} catch {
|
|
27
|
+
return '';
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function readJsonl(file) {
|
|
32
|
+
const rows = [];
|
|
33
|
+
for (const line of readText(file).split(/\r?\n/)) {
|
|
34
|
+
if (!line.trim()) continue;
|
|
35
|
+
try {
|
|
36
|
+
rows.push(JSON.parse(line));
|
|
37
|
+
} catch {
|
|
38
|
+
/* ignore */
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return rows;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function within(ts, since) {
|
|
45
|
+
const t = Date.parse(ts);
|
|
46
|
+
return Number.isFinite(t) && t >= since;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function pct(n, d) {
|
|
50
|
+
if (!d) return null;
|
|
51
|
+
return Math.round((n / d) * 1000) / 10;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function listFiles(dir, re) {
|
|
55
|
+
try {
|
|
56
|
+
return fs.readdirSync(dir).filter((n) => re.test(n)).sort();
|
|
57
|
+
} catch {
|
|
58
|
+
return [];
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function frontmatterStatus(text) {
|
|
63
|
+
const m = String(text).match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
64
|
+
if (!m) return '';
|
|
65
|
+
const s = m[1].match(/^\s*状态:\s*(\S+)/m);
|
|
66
|
+
return s ? s[1] : '';
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function collect(root, { days = 30, now = Date.now()} = {}) {
|
|
70
|
+
const since = now - days * 86400000;
|
|
71
|
+
const missing = [];
|
|
72
|
+
const out = { days, since: new Date(since).toISOString() };
|
|
73
|
+
|
|
74
|
+
// 1) 验证通过率
|
|
75
|
+
const verifications = readJsonl(path.join(root, '.agents', 'verifications.jsonl'));
|
|
76
|
+
if (!verifications.length) missing.push('.agents/verifications.jsonl');
|
|
77
|
+
const verRecent = verifications.filter((r) => within(r.ts, since));
|
|
78
|
+
out.verification = {
|
|
79
|
+
total: verifications.length,
|
|
80
|
+
recent: verRecent.length,
|
|
81
|
+
pass: verRecent.filter((r) => Number(r.exitCode) === 0).length,
|
|
82
|
+
passRate: pct(verRecent.filter((r) => Number(r.exitCode) === 0).length, verRecent.length),
|
|
83
|
+
gateAvg: (() => {
|
|
84
|
+
const gs = verRecent.filter((r) => Number.isFinite(Number(r.gates))).map((r) => Number(r.gates));
|
|
85
|
+
return gs.length ? Math.round((gs.reduce((a, b) => a + b, 0) / gs.length) * 10) / 10 : null;
|
|
86
|
+
})(),
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
// 2) 事故 / 逃逸缺陷
|
|
90
|
+
const incidentDir = path.join(root, 'workflow', 'incidents');
|
|
91
|
+
const incidents = listFiles(incidentDir, /\.md$/).map((n) => readText(path.join(incidentDir, n)));
|
|
92
|
+
if (!fs.existsSync(incidentDir)) missing.push('workflow/incidents/');
|
|
93
|
+
const open = incidents.filter((t) => !/closed|done|已关闭/i.test(frontmatterStatus(t))).length;
|
|
94
|
+
out.incidents = { total: incidents.length, open, closed: incidents.length - open };
|
|
95
|
+
|
|
96
|
+
// 3) 复核意见采纳率(事件级近似:数 reviews.d 表格里的采纳/拒绝/误报单元格)
|
|
97
|
+
const reviewDir = path.join(root, 'workflow', 'reviews.d');
|
|
98
|
+
let adopted = 0; let rejected = 0; let misreport = 0;
|
|
99
|
+
for (const name of listFiles(reviewDir, /\.md$/)) {
|
|
100
|
+
const text = readText(path.join(reviewDir, name));
|
|
101
|
+
for (const line of text.split(/\r?\n/)) {
|
|
102
|
+
if (!/^\|/.test(line)) continue;
|
|
103
|
+
if (/采纳/.test(line) && !/拒绝/.test(line)) adopted += 1;
|
|
104
|
+
if (/拒绝/.test(line)) rejected += 1;
|
|
105
|
+
if (/误报/.test(line)) misreport += 1;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
if (!fs.existsSync(reviewDir)) missing.push('workflow/reviews.d/');
|
|
109
|
+
out.review = { adopted, rejected, misreport, adoptionRate: pct(adopted, adopted + rejected) };
|
|
110
|
+
|
|
111
|
+
// 4) 行为评测
|
|
112
|
+
const evalRows = readJsonl(path.join(root, '.agents', 'evals', 'results.jsonl'));
|
|
113
|
+
if (!evalRows.length) missing.push('.agents/evals/results.jsonl');
|
|
114
|
+
const bySig = new Map();
|
|
115
|
+
for (const r of evalRows) {
|
|
116
|
+
if (!r || !r.signature) continue;
|
|
117
|
+
const prev = bySig.get(r.signature);
|
|
118
|
+
if (!prev || Date.parse(r.ts) > Date.parse(prev.ts)) bySig.set(r.signature, r);
|
|
119
|
+
}
|
|
120
|
+
const latest = [...bySig.values()];
|
|
121
|
+
const fresh = latest.filter((r) => within(r.ts, since));
|
|
122
|
+
out.evals = {
|
|
123
|
+
signatures: latest.length,
|
|
124
|
+
fresh: fresh.length,
|
|
125
|
+
lastPassRate: latest.sort((a, b) => Date.parse(b.ts) - Date.parse(a.ts))[0]?.passRate ?? null,
|
|
126
|
+
stale: latest.length - fresh.length,
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
// 5) 返工提交占比(可选:无 git 时省略)
|
|
130
|
+
const git = spawnSync('git', ['log', `--since=${new Date(since).toISOString()}`, '--pretty=%s'], { encoding: 'utf8', cwd: root });
|
|
131
|
+
if (git.status === 0) {
|
|
132
|
+
const subjects = git.stdout.split(/\r?\n/).filter(Boolean);
|
|
133
|
+
const rework = subjects.filter((s) => /^(fix|revert)(\(|:|!)/i.test(s)).length;
|
|
134
|
+
out.rework = { commits: subjects.length, rework, rate: pct(rework, subjects.length) };
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
out.missing = missing;
|
|
138
|
+
return out;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export function renderMarkdown(m) {
|
|
142
|
+
const fmt = (v) => (v == null ? '—' : `${v}%`);
|
|
143
|
+
const lines = [
|
|
144
|
+
`# 交付结果指标(近 ${m.days} 天)`,
|
|
145
|
+
'',
|
|
146
|
+
`> 生成时间 ${new Date().toISOString()};来源为本地台账,缺源单列,不把 0 当“很好”。`,
|
|
147
|
+
'',
|
|
148
|
+
'| 面 | Leading / Lagging | 值 |',
|
|
149
|
+
'|---|---|---|',
|
|
150
|
+
`| 验证通过率 | Leading | ${fmt(m.verification.passRate)}(${m.verification.pass}/${m.verification.recent} 次,均门数 ${m.verification.gateAvg ?? '—'}) |`,
|
|
151
|
+
`| 行为评测陈旧面 | Leading | ${m.evals.stale}/${m.evals.signatures} 个签名过期;最新通过率 ${fmt(m.evals.lastPassRate)} |`,
|
|
152
|
+
`| 复核采纳率 | Leading | ${fmt(m.review.adoptionRate)}(采纳 ${m.review.adopted} / 拒绝 ${m.review.rejected} / 误报 ${m.review.misreport}) |`,
|
|
153
|
+
`| 返工提交占比 | Lagging | ${m.rework ? `${fmt(m.rework.rate)}(${m.rework.rework}/${m.rework.commits})` : '—(无 git)'} |`,
|
|
154
|
+
`| 未关闭事故 | Lagging | ${m.incidents.open}/${m.incidents.total} |`,
|
|
155
|
+
'',
|
|
156
|
+
];
|
|
157
|
+
if (m.missing.length) {
|
|
158
|
+
lines.push('## 数据缺口', '');
|
|
159
|
+
for (const x of m.missing) lines.push(`- 缺 \`${x}\`:该行指标为空/近似,不据此下结论。`);
|
|
160
|
+
lines.push('');
|
|
161
|
+
}
|
|
162
|
+
return `${lines.join('\n')}\n`;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function argOf(argv, name, fallback = null) {
|
|
166
|
+
const i = argv.indexOf(`--${name}`);
|
|
167
|
+
if (i < 0) return fallback;
|
|
168
|
+
const v = argv[i + 1];
|
|
169
|
+
return v == null || v.startsWith('--') ? true : v;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function main() {
|
|
173
|
+
const argv = process.argv.slice(2);
|
|
174
|
+
const root = path.resolve(argOf(argv, 'root', process.cwd()));
|
|
175
|
+
const days = Number(argOf(argv, 'days', 30)) || 30;
|
|
176
|
+
const m = collect(root, { days });
|
|
177
|
+
if (argv.includes('--json')) {
|
|
178
|
+
console.log(JSON.stringify(m, null, 2));
|
|
179
|
+
return 0;
|
|
180
|
+
}
|
|
181
|
+
const md = renderMarkdown(m);
|
|
182
|
+
const outArg = argOf(argv, 'out');
|
|
183
|
+
if (outArg && outArg !== true) {
|
|
184
|
+
const file = path.resolve(root, outArg);
|
|
185
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
186
|
+
fs.writeFileSync(file, md, 'utf8');
|
|
187
|
+
console.log(file);
|
|
188
|
+
} else {
|
|
189
|
+
process.stdout.write(md);
|
|
190
|
+
}
|
|
191
|
+
return 0;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const isMain = process.argv[1] && process.argv[1].endsWith('outcome-metrics.mjs');
|
|
195
|
+
if (isMain) process.exit(main());
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// path-lease.mjs — 共享工件的单写者租约
|
|
3
|
+
//
|
|
4
|
+
// 背景(Anthropic《The components of agent memory》的并发实证):
|
|
5
|
+
// 多个 agent 同时写一张共享表,57 条合同事实只剩 41 条;一页一 agent、逐字引用则 57/57。
|
|
6
|
+
//
|
|
7
|
+
// 本脚本提供最小的本地租约:同一路径同一时间只允许一个 owner 写。
|
|
8
|
+
// 租约是 advisory(协作纪律),不是安全边界;文件放 `.agents/cache/leases/`(gitignore)。
|
|
9
|
+
//
|
|
10
|
+
// 用法:
|
|
11
|
+
// node .agents/scripts/path-lease.mjs acquire --path wiki/foo.md [--owner <id>] [--ttl 30]
|
|
12
|
+
// node .agents/scripts/path-lease.mjs release --path wiki/foo.md [--owner <id>] [--force]
|
|
13
|
+
// node .agents/scripts/path-lease.mjs check --path wiki/foo.md [--owner <id>]
|
|
14
|
+
// node .agents/scripts/path-lease.mjs list [--json]
|
|
15
|
+
// node .agents/scripts/path-lease.mjs sweep
|
|
16
|
+
//
|
|
17
|
+
// 退出码:acquire/check 成功 0;被他人持有 1;参数错误 2。
|
|
18
|
+
import fs from 'node:fs';
|
|
19
|
+
import path from 'node:path';
|
|
20
|
+
import crypto from 'node:crypto';
|
|
21
|
+
import os from 'node:os';
|
|
22
|
+
|
|
23
|
+
export function leaseDir(root) {
|
|
24
|
+
return path.join(root, '.agents', 'cache', 'leases');
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function normalizeRel(rel) {
|
|
28
|
+
return String(rel).replace(/\\/g, '/').replace(/^\.?\//, '').trim();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function leaseFile(root, rel) {
|
|
32
|
+
const key = crypto.createHash('sha1').update(normalizeRel(rel)).digest('hex');
|
|
33
|
+
return path.join(leaseDir(root), `${key}.json`);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function defaultOwner() {
|
|
37
|
+
return `${os.hostname()}/${process.pid}`;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function readLease(file) {
|
|
41
|
+
try {
|
|
42
|
+
return JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
43
|
+
} catch {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function expired(lease, now = Date.now()) {
|
|
49
|
+
return !lease || !Number.isFinite(Date.parse(lease.expiresAt)) || Date.parse(lease.expiresAt) <= now;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function acquire(root, { rel, owner = defaultOwner(), ttlMinutes = 30, now = new Date() } = {}) {
|
|
53
|
+
const target = normalizeRel(rel);
|
|
54
|
+
if (!target) throw new Error('--path 必填');
|
|
55
|
+
const ttl = Math.max(1, Number(ttlMinutes) || 30);
|
|
56
|
+
const file = leaseFile(root, target);
|
|
57
|
+
const row = {
|
|
58
|
+
path: target,
|
|
59
|
+
owner: String(owner),
|
|
60
|
+
acquiredAt: now.toISOString(),
|
|
61
|
+
expiresAt: new Date(now.getTime() + ttl * 60000).toISOString(),
|
|
62
|
+
ttlMinutes: ttl,
|
|
63
|
+
};
|
|
64
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
65
|
+
try {
|
|
66
|
+
fs.writeFileSync(file, JSON.stringify(row, null, 2), { flag: 'wx' });
|
|
67
|
+
return { ok: true, lease: row };
|
|
68
|
+
} catch (err) {
|
|
69
|
+
if (err.code !== 'EEXIST') throw err;
|
|
70
|
+
}
|
|
71
|
+
const held = readLease(file);
|
|
72
|
+
if (expired(held, now.getTime())) {
|
|
73
|
+
fs.writeFileSync(file, JSON.stringify(row, null, 2));
|
|
74
|
+
return { ok: true, lease: row, takeover: held };
|
|
75
|
+
}
|
|
76
|
+
if (held && held.owner === String(owner)) return { ok: true, lease: held, renewed: false };
|
|
77
|
+
return { ok: false, holder: held, lease: row };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function release(root, { rel, owner = defaultOwner(), force = false } = {}) {
|
|
81
|
+
const target = normalizeRel(rel);
|
|
82
|
+
const file = leaseFile(root, target);
|
|
83
|
+
const held = readLease(file);
|
|
84
|
+
if (!held) return { ok: true, missing: true };
|
|
85
|
+
if (!force && held.owner !== String(owner)) return { ok: false, holder: held };
|
|
86
|
+
fs.rmSync(file, { force: true });
|
|
87
|
+
return { ok: true, released: held };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function check(root, { rel, owner = defaultOwner(), now = Date.now() } = {}) {
|
|
91
|
+
const target = normalizeRel(rel);
|
|
92
|
+
const held = readLease(leaseFile(root, target));
|
|
93
|
+
if (!held || expired(held, now)) return { ok: true, free: true, holder: held || null };
|
|
94
|
+
if (held.owner === String(owner)) return { ok: true, owned: true, holder: held };
|
|
95
|
+
return { ok: false, holder: held };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function listLeases(root) {
|
|
99
|
+
const dir = leaseDir(root);
|
|
100
|
+
let names = [];
|
|
101
|
+
try {
|
|
102
|
+
names = fs.readdirSync(dir).filter((n) => n.endsWith('.json')).sort();
|
|
103
|
+
} catch {
|
|
104
|
+
return [];
|
|
105
|
+
}
|
|
106
|
+
return names.map((n) => readLease(path.join(dir, n))).filter(Boolean);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function sweep(root, { now = Date.now() } = {}) {
|
|
110
|
+
const removed = [];
|
|
111
|
+
for (const lease of listLeases(root)) {
|
|
112
|
+
if (expired(lease, now)) {
|
|
113
|
+
fs.rmSync(leaseFile(root, lease.path), { force: true });
|
|
114
|
+
removed.push(lease.path);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return removed;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function argOf(argv, name, fallback = null) {
|
|
121
|
+
const i = argv.indexOf(`--${name}`);
|
|
122
|
+
if (i < 0) return fallback;
|
|
123
|
+
const v = argv[i + 1];
|
|
124
|
+
return v == null || v.startsWith('--') ? true : v;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function main() {
|
|
128
|
+
const argv = process.argv.slice(2);
|
|
129
|
+
const cmd = argv[0] || 'list';
|
|
130
|
+
const root = path.resolve(argOf(argv, 'root', process.cwd()));
|
|
131
|
+
const rel = argOf(argv, 'path');
|
|
132
|
+
const owner = argOf(argv, 'owner', defaultOwner());
|
|
133
|
+
|
|
134
|
+
if (cmd === 'acquire') {
|
|
135
|
+
const res = acquire(root, { rel, owner, ttlMinutes: argOf(argv, 'ttl', 30) });
|
|
136
|
+
if (!res.ok) {
|
|
137
|
+
console.error(`✗ ${rel} 被 ${res.holder?.owner || '?'} 持有至 ${res.holder?.expiresAt || '?'}`);
|
|
138
|
+
return 1;
|
|
139
|
+
}
|
|
140
|
+
console.log(`✓ lease ${rel} owner=${res.lease.owner} until=${res.lease.expiresAt}${res.takeover ? ' (takeover)' : ''}`);
|
|
141
|
+
return 0;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (cmd === 'release') {
|
|
145
|
+
const res = release(root, { rel, owner, force: argv.includes('--force') });
|
|
146
|
+
if (!res.ok) {
|
|
147
|
+
console.error(`✗ ${rel} 由 ${res.holder?.owner || '?'} 持有;--force 可强拆`);
|
|
148
|
+
return 1;
|
|
149
|
+
}
|
|
150
|
+
console.log(res.missing ? `- ${rel} 无租约` : `✓ released ${rel}`);
|
|
151
|
+
return 0;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (cmd === 'check') {
|
|
155
|
+
const res = check(root, { rel, owner });
|
|
156
|
+
if (!res.ok) {
|
|
157
|
+
console.error(`✗ ${rel} 被 ${res.holder?.owner || '?'} 持有至 ${res.holder?.expiresAt || '?'}`);
|
|
158
|
+
return 1;
|
|
159
|
+
}
|
|
160
|
+
console.log(res.free ? `✓ ${rel} 空闲` : `✓ ${rel} 由本 owner 持有`);
|
|
161
|
+
return 0;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (cmd === 'sweep') {
|
|
165
|
+
const removed = sweep(root);
|
|
166
|
+
console.log(`swept ${removed.length}${removed.length ? `: ${removed.join(', ')}` : ''}`);
|
|
167
|
+
return 0;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const leases = listLeases(root);
|
|
171
|
+
if (argv.includes('--json')) console.log(JSON.stringify(leases, null, 2));
|
|
172
|
+
else for (const l of leases) console.log(`${l.path}\t${l.owner}\t${l.expiresAt}`);
|
|
173
|
+
return 0;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const isMain = process.argv[1] && process.argv[1].endsWith('path-lease.mjs');
|
|
177
|
+
if (isMain) process.exit(main());
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// session-log.mjs — 会话经验日志(给 dreaming 的输入)
|
|
3
|
+
//
|
|
4
|
+
// 背景(Anthropic《The components of agent memory》):dreaming 之所以能纠错,
|
|
5
|
+
// 是因为它能把记忆笔记与**原始会话记录**对照。git 历史只是工件历史,不是经验。
|
|
6
|
+
//
|
|
7
|
+
// 本脚本只做最小、可丢弃、可重建的事:
|
|
8
|
+
// - append:把显著事件(决策 / 失败 / 纠正 / 拒绝 / 备注)追加到
|
|
9
|
+
// `.agents/cache/session-logs/<YYYY-MM-DD>.jsonl`(gitignore,运行态);
|
|
10
|
+
// - digest:按天聚合成一份紧凑摘要,交给 dream-run 的“经验回放”阶段;
|
|
11
|
+
// - 不自动 commit / push,不读宿主私有 transcript。
|
|
12
|
+
//
|
|
13
|
+
// 用法:
|
|
14
|
+
// node .agents/scripts/session-log.mjs append --type failure --text "..." [--ref path] [--tag x]
|
|
15
|
+
// node .agents/scripts/session-log.mjs digest [--days 7] [--out tmp/session-digest-<date>.md]
|
|
16
|
+
// node .agents/scripts/session-log.mjs list [--days 7] [--json]
|
|
17
|
+
//
|
|
18
|
+
// 事件类型闭集:decision / failure / correction / refusal / note
|
|
19
|
+
import fs from 'node:fs';
|
|
20
|
+
import path from 'node:path';
|
|
21
|
+
|
|
22
|
+
export const TYPES = ['decision', 'failure', 'correction', 'refusal', 'note'];
|
|
23
|
+
|
|
24
|
+
export function logsDir(root) {
|
|
25
|
+
return path.join(root, '.agents', 'cache', 'session-logs');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function dayOf(iso) {
|
|
29
|
+
return String(iso).slice(0, 10);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function appendEvent(root, { type, text, ref = '', tag = '', now = new Date() } = {}) {
|
|
33
|
+
const kind = String(type || '').trim();
|
|
34
|
+
if (!TYPES.includes(kind)) throw new Error(`未知事件类型:${kind}(允许 ${TYPES.join('/')})`);
|
|
35
|
+
const body = String(text || '').trim();
|
|
36
|
+
if (!body) throw new Error('--text 不能为空');
|
|
37
|
+
const row = {
|
|
38
|
+
ts: now.toISOString(),
|
|
39
|
+
type: kind,
|
|
40
|
+
text: body,
|
|
41
|
+
ref: String(ref || ''),
|
|
42
|
+
tag: String(tag || ''),
|
|
43
|
+
};
|
|
44
|
+
const dir = logsDir(root);
|
|
45
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
46
|
+
fs.appendFileSync(path.join(dir, `${dayOf(row.ts)}.jsonl`), `${JSON.stringify(row)}\n`);
|
|
47
|
+
return row;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function readEvents(root, { days = 7, now = new Date() } = {}) {
|
|
51
|
+
const dir = logsDir(root);
|
|
52
|
+
let names = [];
|
|
53
|
+
try {
|
|
54
|
+
names = fs.readdirSync(dir).filter((n) => /^\d{4}-\d{2}-\d{2}\.jsonl$/.test(n)).sort();
|
|
55
|
+
} catch {
|
|
56
|
+
return [];
|
|
57
|
+
}
|
|
58
|
+
const cutoff = now.getTime() - days * 86400000;
|
|
59
|
+
const out = [];
|
|
60
|
+
for (const name of names) {
|
|
61
|
+
let raw;
|
|
62
|
+
try {
|
|
63
|
+
raw = fs.readFileSync(path.join(dir, name), 'utf8');
|
|
64
|
+
} catch {
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
for (const line of raw.split(/\r?\n/)) {
|
|
68
|
+
if (!line.trim()) continue;
|
|
69
|
+
let row;
|
|
70
|
+
try {
|
|
71
|
+
row = JSON.parse(line);
|
|
72
|
+
} catch {
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
const t = Date.parse(row.ts);
|
|
76
|
+
if (Number.isFinite(t) && t >= cutoff) out.push(row);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return out.sort((a, b) => Date.parse(a.ts) - Date.parse(b.ts));
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function digestEvents(events, { days = 7, maxPerType = 8 } = {}) {
|
|
83
|
+
const lines = [];
|
|
84
|
+
lines.push(`# 会话经验摘要(近 ${days} 天)`);
|
|
85
|
+
lines.push('');
|
|
86
|
+
if (!events.length) {
|
|
87
|
+
lines.push('_无会话日志输入。dreaming 只能审计工件,无法用经验纠错。_');
|
|
88
|
+
lines.push('');
|
|
89
|
+
lines.push('在关键节点执行:`node .agents/scripts/session-log.mjs append --type failure --text "…"`');
|
|
90
|
+
return `${lines.join('\n')}\n`;
|
|
91
|
+
}
|
|
92
|
+
const counts = {};
|
|
93
|
+
for (const e of events) counts[e.type] = (counts[e.type] || 0) + 1;
|
|
94
|
+
lines.push(`共 ${events.length} 条:${TYPES.map((t) => `${t}=${counts[t] || 0}`).join(' / ')}`);
|
|
95
|
+
lines.push('');
|
|
96
|
+
for (const type of TYPES) {
|
|
97
|
+
const rows = events.filter((e) => e.type === type);
|
|
98
|
+
if (!rows.length) continue;
|
|
99
|
+
lines.push(`## ${type}`);
|
|
100
|
+
for (const e of rows.slice(-maxPerType)) {
|
|
101
|
+
const ref = e.ref ? ` · \`${e.ref}\`` : '';
|
|
102
|
+
const tag = e.tag ? ` · [${e.tag}]` : '';
|
|
103
|
+
lines.push(`- ${dayOf(e.ts)} ${e.text}${ref}${tag}`);
|
|
104
|
+
}
|
|
105
|
+
if (rows.length > maxPerType) lines.push(`- …另有 ${rows.length - maxPerType} 条(见原始日志)`);
|
|
106
|
+
lines.push('');
|
|
107
|
+
}
|
|
108
|
+
return `${lines.join('\n')}\n`;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// ---- CLI ----
|
|
112
|
+
function argOf(argv, name, fallback = null) {
|
|
113
|
+
const i = argv.indexOf(`--${name}`);
|
|
114
|
+
if (i < 0) return fallback;
|
|
115
|
+
const v = argv[i + 1];
|
|
116
|
+
return v == null || v.startsWith('--') ? true : v;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function main() {
|
|
120
|
+
const argv = process.argv.slice(2);
|
|
121
|
+
const cmd = argv[0] || 'digest';
|
|
122
|
+
const root = path.resolve(argOf(argv, 'root', process.cwd()));
|
|
123
|
+
|
|
124
|
+
if (cmd === 'append') {
|
|
125
|
+
const row = appendEvent(root, {
|
|
126
|
+
type: argOf(argv, 'type', 'note'),
|
|
127
|
+
text: argOf(argv, 'text', ''),
|
|
128
|
+
ref: argOf(argv, 'ref', ''),
|
|
129
|
+
tag: argOf(argv, 'tag', ''),
|
|
130
|
+
});
|
|
131
|
+
console.log(`logged ${row.type} @ ${row.ts}`);
|
|
132
|
+
return 0;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const days = Number(argOf(argv, 'days', 7)) || 7;
|
|
136
|
+
const events = readEvents(root, { days });
|
|
137
|
+
if (cmd === 'list') {
|
|
138
|
+
if (argv.includes('--json')) console.log(JSON.stringify(events, null, 2));
|
|
139
|
+
else for (const e of events) console.log(`${e.ts}\t${e.type}\t${e.text}${e.ref ? `\t${e.ref}` : ''}`);
|
|
140
|
+
return 0;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const md = digestEvents(events, { days });
|
|
144
|
+
const outArg = argOf(argv, 'out');
|
|
145
|
+
if (outArg && outArg !== true) {
|
|
146
|
+
const out = path.resolve(root, outArg);
|
|
147
|
+
fs.mkdirSync(path.dirname(out), { recursive: true });
|
|
148
|
+
fs.writeFileSync(out, md, 'utf8');
|
|
149
|
+
console.log(out);
|
|
150
|
+
} else {
|
|
151
|
+
process.stdout.write(md);
|
|
152
|
+
}
|
|
153
|
+
return 0;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const isMain = process.argv[1] && process.argv[1].endsWith('session-log.mjs');
|
|
157
|
+
if (isMain) process.exit(main());
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// steering.mjs — 按模型档解析“严格度”(不变量不变,只调表达方式)
|
|
3
|
+
//
|
|
4
|
+
// 用法:
|
|
5
|
+
// node .agents/scripts/steering.mjs resolve --model opus [--json]
|
|
6
|
+
// node .agents/scripts/steering.mjs show
|
|
7
|
+
import fs from 'node:fs';
|
|
8
|
+
import path from 'node:path';
|
|
9
|
+
|
|
10
|
+
export const DEFAULT = {
|
|
11
|
+
version: 1,
|
|
12
|
+
defaultTier: 'standard',
|
|
13
|
+
tiers: {
|
|
14
|
+
frontier: { scaffolding: 'goal', judgmentLatitude: 'high', guidance: '' },
|
|
15
|
+
standard: { scaffolding: 'mixed', judgmentLatitude: 'medium', guidance: '' },
|
|
16
|
+
lean: { scaffolding: 'step', judgmentLatitude: 'low', guidance: '' },
|
|
17
|
+
},
|
|
18
|
+
invariants: [],
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
// 只做“已知别名 → 档位”的保守映射;未知模型落到 default,不猜能力。
|
|
22
|
+
const FRONTIER = /(^|[-/])(opus|fable|o3|o4|deepseek-r|deepseek-v4)([-/.]|$)/i;
|
|
23
|
+
const LEAN = /(^|[-/])(haiku|mini|flash|small|nano)([-/.]|$)/i;
|
|
24
|
+
|
|
25
|
+
export function loadConfig(root) {
|
|
26
|
+
try {
|
|
27
|
+
return { ...DEFAULT, ...JSON.parse(fs.readFileSync(path.join(root, '.agents', 'config', 'steering.json'), 'utf8')) };
|
|
28
|
+
} catch {
|
|
29
|
+
return DEFAULT;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function tierFor(model, cfg = DEFAULT) {
|
|
34
|
+
const id = String(model || '').toLowerCase();
|
|
35
|
+
if (!id) return cfg.defaultTier;
|
|
36
|
+
if (FRONTIER.test(id)) return 'frontier';
|
|
37
|
+
if (LEAN.test(id)) return 'lean';
|
|
38
|
+
return cfg.defaultTier;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function resolveSteering(root, model) {
|
|
42
|
+
const cfg = loadConfig(root);
|
|
43
|
+
const tier = tierFor(model, cfg);
|
|
44
|
+
return { model: String(model || ''), tier, ...(cfg.tiers[tier] || {}), invariants: cfg.invariants || [] };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function argOf(argv, name, fallback = null) {
|
|
48
|
+
const i = argv.indexOf(`--${name}`);
|
|
49
|
+
if (i < 0) return fallback;
|
|
50
|
+
const v = argv[i + 1];
|
|
51
|
+
return v == null || v.startsWith('--') ? true : v;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function main() {
|
|
55
|
+
const argv = process.argv.slice(2);
|
|
56
|
+
const root = path.resolve(argOf(argv, 'root', process.cwd()));
|
|
57
|
+
const cmd = argv[0] || 'show';
|
|
58
|
+
if (cmd === 'resolve') {
|
|
59
|
+
const res = resolveSteering(root, argOf(argv, 'model', ''));
|
|
60
|
+
if (argv.includes('--json')) console.log(JSON.stringify(res, null, 2));
|
|
61
|
+
else console.log(`${res.model || '(default)'} -> ${res.tier} · scaffolding=${res.scaffolding} · judgment=${res.judgmentLatitude}\n${res.guidance}`);
|
|
62
|
+
return 0;
|
|
63
|
+
}
|
|
64
|
+
const cfg = loadConfig(root);
|
|
65
|
+
if (argv.includes('--json')) console.log(JSON.stringify(cfg, null, 2));
|
|
66
|
+
else {
|
|
67
|
+
console.log(`defaultTier=${cfg.defaultTier}`);
|
|
68
|
+
for (const [name, t] of Object.entries(cfg.tiers)) console.log(`- ${name}: scaffolding=${t.scaffolding} judgment=${t.judgmentLatitude}`);
|
|
69
|
+
console.log(`invariants: ${(cfg.invariants || []).join(' / ') || '—'}`);
|
|
70
|
+
}
|
|
71
|
+
return 0;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const isMain = process.argv[1] && process.argv[1].endsWith('steering.mjs');
|
|
75
|
+
if (isMain) process.exit(main());
|
|
@@ -171,7 +171,7 @@ if (process.env.RUN_E2E === '1') {
|
|
|
171
171
|
}
|
|
172
172
|
|
|
173
173
|
// Stack profile(agentic-flow-kit 通用化):基础门表里的 product 门是 dotnet/Vue 专用。
|
|
174
|
-
//
|
|
174
|
+
// 其余栈只保留 engine + security + wiki 门,再按栈追加一条产品测试门。
|
|
175
175
|
const STACK = (() => {
|
|
176
176
|
try { return JSON.parse(fs.readFileSync('.agents/config/engine.json', 'utf8')).options?.stack || 'dotnet'; }
|
|
177
177
|
catch { return 'dotnet'; }
|
|
@@ -181,14 +181,25 @@ if (STACK !== 'dotnet') {
|
|
|
181
181
|
if (/^(backend-|frontend-|code-)/.test(GATES[i].id)) GATES.splice(i, 1);
|
|
182
182
|
}
|
|
183
183
|
}
|
|
184
|
-
|
|
184
|
+
const pushProduct = (id, name, cmd) => {
|
|
185
|
+
GATES.push({ id, cohort: 'product', name, cmd, folder: '.', type: 'shell' });
|
|
186
|
+
};
|
|
187
|
+
if (STACK === 'node' || STACK === 'typescript') {
|
|
185
188
|
try {
|
|
186
189
|
const pkg = JSON.parse(fs.readFileSync('package.json', 'utf8'));
|
|
187
|
-
if (pkg.scripts && pkg.scripts.test)
|
|
188
|
-
GATES.push({ id: 'node-test', cohort: 'product', name: 'Node tests', cmd: 'npm test', folder: '.', type: 'shell' });
|
|
189
|
-
}
|
|
190
|
+
if (pkg.scripts && pkg.scripts.test) pushProduct('node-test', 'Node tests', 'npm test');
|
|
190
191
|
} catch { /* not a node project root */ }
|
|
191
192
|
}
|
|
193
|
+
if (STACK === 'rust' && fs.existsSync('Cargo.toml')) pushProduct('cargo-test', 'Cargo tests', 'cargo test');
|
|
194
|
+
if (STACK === 'go' && fs.existsSync('go.mod')) pushProduct('go-test', 'Go tests', 'go test ./...');
|
|
195
|
+
if (STACK === 'java') {
|
|
196
|
+
if (fs.existsSync('pom.xml')) pushProduct('maven-test', 'Maven tests', 'mvn -q test');
|
|
197
|
+
else if (fs.existsSync('gradlew') || fs.existsSync('gradlew.bat')) {
|
|
198
|
+
pushProduct('gradle-test', 'Gradle tests', process.platform === 'win32' ? 'gradlew.bat test' : './gradlew test');
|
|
199
|
+
} else if (fs.existsSync('build.gradle') || fs.existsSync('build.gradle.kts')) {
|
|
200
|
+
pushProduct('gradle-test', 'Gradle tests', 'gradle test');
|
|
201
|
+
}
|
|
202
|
+
}
|
|
192
203
|
|
|
193
204
|
// 计数捕获:优先 run-tests 的「断言合计」汇总行,退路取末条「合计: PASS x / FAIL y」
|
|
194
205
|
//(夹具假命令与单套件输出皆走退路;无匹配则省略计数键,exitCode 才是硬事实)。
|
|
@@ -15,6 +15,10 @@ description: 设计选型 bakeoff——同一 brief 给 N 个独立臂(不同
|
|
|
15
15
|
|
|
16
16
|
**同一 brief,独立臂,裁判选底座 + 嫁接。** 不是并行实现同一个文件;是并行**提案**,再选一个底座。臂之间不共享上下文(防锚定)。
|
|
17
17
|
|
|
18
|
+
**单写者**:每个臂只写自己的提案文件/目录;需要写共享页面前先
|
|
19
|
+
`node .agents/scripts/path-lease.mjs acquire --path <page> --owner <arm>`,写完释放。
|
|
20
|
+
并发写同一页会丢事实(Anthropic 实证:共享表 57 条只剩 41 条)。
|
|
21
|
+
|
|
18
22
|
## 步骤
|
|
19
23
|
|
|
20
24
|
1. **固定 brief**:目标、约束、验收判据、非目标、证据要求。每个臂拿到**逐字相同**的 brief。
|