sillyspec 3.29.0 → 3.29.2
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/package.json +1 -1
- package/src/agent-session-log.js +54 -0
- package/src/config-schema.js +1 -1
- package/src/fr-index.js +219 -11
- package/src/handoff.js +113 -0
- package/src/index.js +52 -0
- package/src/redlines.js +195 -0
- package/src/run/command.js +32 -0
- package/src/run/complete.js +60 -2
- package/src/run/gate-snapshot.js +54 -3
- package/src/run/prompt.js +2 -2
- package/src/run/quick-audit.js +12 -2
- package/src/run/verify-quality-scan.js +1 -1
- package/src/stages/brainstorm-auto.js +1 -1
- package/src/stages/brainstorm.js +7 -4
- package/src/stages/execute.js +2 -2
- package/src/stages/plan.js +16 -12
- package/src/stages/scan.js +1 -1
- package/src/verify-probes.js +77 -0
- package/src/worktree-apply.js +1 -1
- package/src/worktree.js +1 -1
package/src/redlines.js
ADDED
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* redlines.js —— 红线机检评估器(2026-09-20-redline-machine-check)
|
|
3
|
+
*
|
|
4
|
+
* 为什么需要(2026-09-19 双实现对撞实验核心教训):语义级设计红线(「已结束会话
|
|
5
|
+
* 不得假运行」「用量归属不得后写覆盖」)活在历史 design/knowledge 散文里,无机器
|
|
6
|
+
* 可查形态——三层独立评审 + 352 例测试拦不住两处红线失分,靠事后人工逐文件深读
|
|
7
|
+
* 才发现。本模块把红线转成机器可查断言:消费者仓 `.sillyspec/redlines.yaml` 条目
|
|
8
|
+
* = forbid/require 正则 × scope 文件 glob × severity × origin 文档锚。
|
|
9
|
+
*
|
|
10
|
+
* 定位与边界(decisions.md D-001~D-004):
|
|
11
|
+
* - v1 = 模式级断言(人写正则人负责;statement/origin 保留散文溯源);不做
|
|
12
|
+
* AST/语义级,不做散文自动提炼。
|
|
13
|
+
* - 机制与内容分离:清单消费者仓自持;本仓缺清单 = 不适用(零打扰)。
|
|
14
|
+
* - fail-open 全链:坏 yaml→不适用+注记;单条求值异常→跳过+warn;任何红线面
|
|
15
|
+
* 故障不炸 verify 主链(探针 11 接线侧 try/catch fail-soft,同探针 8/9/10)。
|
|
16
|
+
* - advisory:探针渲染 ❌/⚠️ 进 verify-result 骨架供 agent 裁定;不进 PASS
|
|
17
|
+
* 封顶(D-003:攒误报率数据后另案升硬门)。
|
|
18
|
+
*
|
|
19
|
+
* 纯函数纪律:root 注入,不读 env/时钟;fs 只经 resolveScopeFiles 的 walk。
|
|
20
|
+
* 零新增外部依赖(js-yaml 已有)。
|
|
21
|
+
*/
|
|
22
|
+
import { readdirSync, statSync, readFileSync } from 'fs';
|
|
23
|
+
import { join, relative } from 'path';
|
|
24
|
+
import jsYaml from 'js-yaml';
|
|
25
|
+
|
|
26
|
+
/** scope walk 排除目录(依赖/版本库/工具自身产物——含 .sillyspec:清单/进度文件不得喂饱 require 造成假阴性,S2 审查 D1)。 */
|
|
27
|
+
const SKIP_DIRS = new Set(['node_modules', '.git', '.sillyspec', '.runtime', 'dist', 'build', '.next', '__pycache__']);
|
|
28
|
+
|
|
29
|
+
/** 单文件扫描上限:2MB(大产物/锁文件跳过,warn 留痕)。 */
|
|
30
|
+
const MAX_FILE_BYTES = 2 * 1024 * 1024;
|
|
31
|
+
|
|
32
|
+
/** glob → 正则:`**` 跨层、`*` 单段(不跨 /);段全等不认长名(src-guide 不蹭 src)。 */
|
|
33
|
+
function globToRegExp(glob) {
|
|
34
|
+
const esc = String(glob).replace(/[.+^${}()|[\]\\]/g, '\\$&');
|
|
35
|
+
return new RegExp(
|
|
36
|
+
'^' + esc.replace(/\*\*/g, '⟦⟦DBL⟧⟧').replace(/\*/g, '⟦SGL⟧')
|
|
37
|
+
.replace(/⟦⟦DBL⟧⟧/g, '(?:.*)').replace(/⟦SGL⟧/g, '[^/]*') + '$',
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** 递归收集 root 下全部文件(排除 SKIP_DIRS;返回仓根相对 POSIX 路径)。 */
|
|
42
|
+
function walkFiles(root) {
|
|
43
|
+
const out = [];
|
|
44
|
+
const visit = (dir) => {
|
|
45
|
+
let entries;
|
|
46
|
+
try {
|
|
47
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
48
|
+
} catch {
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
for (const e of entries) {
|
|
52
|
+
const p = join(dir, e.name);
|
|
53
|
+
if (e.isDirectory()) {
|
|
54
|
+
if (SKIP_DIRS.has(e.name)) continue;
|
|
55
|
+
visit(p);
|
|
56
|
+
} else if (e.isFile()) {
|
|
57
|
+
out.push(relative(root, p).replace(/\\/g, '/'));
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
visit(root);
|
|
62
|
+
return out;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* 解析 redlines.yaml 文本为条目数组(纯函数)。
|
|
67
|
+
* 无效条目四判据(跳过 + warnings 留痕):缺 id / 缺 scope / forbid 与 require
|
|
68
|
+
* 全空 / 任一正则编译失败。severity 缺省 'error'。
|
|
69
|
+
* @param {string} yamlText
|
|
70
|
+
* @returns {{ entries: Array<object>, warnings: string[] }}
|
|
71
|
+
*/
|
|
72
|
+
export function parseRedlines(yamlText) {
|
|
73
|
+
const warnings = [];
|
|
74
|
+
const doc = jsYaml.load(yamlText);
|
|
75
|
+
const rawList = doc && Array.isArray(doc.redlines) ? doc.redlines : [];
|
|
76
|
+
const entries = [];
|
|
77
|
+
for (const raw of rawList) {
|
|
78
|
+
if (!raw || typeof raw !== 'object') {
|
|
79
|
+
warnings.push('跳过非对象条目');
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
const id = typeof raw.id === 'string' && raw.id.trim() !== '' ? raw.id.trim() : null;
|
|
83
|
+
if (id === null) { warnings.push('跳过缺 id 条目'); continue; }
|
|
84
|
+
const scope = Array.isArray(raw.scope) ? raw.scope.filter((s) => typeof s === 'string' && s !== '') : [];
|
|
85
|
+
if (scope.length === 0) { warnings.push(`条目 ${id} 缺 scope,跳过`); continue; }
|
|
86
|
+
const forbid = Array.isArray(raw.forbid) ? raw.forbid.filter((s) => typeof s === 'string' && s !== '') : [];
|
|
87
|
+
const require_ = Array.isArray(raw.require) ? raw.require.filter((s) => typeof s === 'string' && s !== '') : [];
|
|
88
|
+
if (forbid.length === 0 && require_.length === 0) {
|
|
89
|
+
warnings.push(`条目 ${id} 的 forbid 与 require 全空,跳过`);
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
const severity = raw.severity === 'warning' ? 'warning' : 'error';
|
|
93
|
+
const entry = {
|
|
94
|
+
id,
|
|
95
|
+
statement: typeof raw.statement === 'string' ? raw.statement : '',
|
|
96
|
+
scope,
|
|
97
|
+
forbid,
|
|
98
|
+
require: require_,
|
|
99
|
+
severity,
|
|
100
|
+
origin: typeof raw.origin === 'string' ? raw.origin : null,
|
|
101
|
+
_res: { forbid: [], require: [] },
|
|
102
|
+
};
|
|
103
|
+
let badRe = false;
|
|
104
|
+
for (const re of forbid) {
|
|
105
|
+
try { entry._res.forbid.push(new RegExp(re, 'm')); } catch (e) { warnings.push(`条目 ${id} forbid 正则编译失败(${re}),跳过该条`); badRe = true; }
|
|
106
|
+
}
|
|
107
|
+
for (const re of require_) {
|
|
108
|
+
try { entry._res.require.push(new RegExp(re, 'm')); } catch (e) { warnings.push(`条目 ${id} require 正则编译失败(${re}),跳过该条`); badRe = true; }
|
|
109
|
+
}
|
|
110
|
+
if (badRe) continue;
|
|
111
|
+
entries.push(entry);
|
|
112
|
+
}
|
|
113
|
+
return { entries, warnings };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* scope glob 列表 → 命中文件集(root 下仓根相对 POSIX 路径,排序去重)。
|
|
118
|
+
* @param {string[]} scopeGlobs
|
|
119
|
+
* @param {string} root
|
|
120
|
+
* @returns {string[]}
|
|
121
|
+
*/
|
|
122
|
+
export function resolveScopeFiles(scopeGlobs, root) {
|
|
123
|
+
const all = walkFiles(root);
|
|
124
|
+
const res = (Array.isArray(scopeGlobs) ? scopeGlobs : [])
|
|
125
|
+
.filter((g) => typeof g === 'string' && g !== '')
|
|
126
|
+
.map((g) => globToRegExp(g));
|
|
127
|
+
return [...new Set(all.filter((f) => res.some((r) => r.test(f))))].sort();
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* 逐条评估红线:forbid 命中收集 file:line + require 缺失 + severity 透传。
|
|
132
|
+
* 单条求值异常跳过 + warnings(fail-open,D-004)。
|
|
133
|
+
* @param {object} opts
|
|
134
|
+
* @param {Array<object>} opts.entries - parseRedlines 产物
|
|
135
|
+
* @param {string} opts.root - 求值根(worktree 或仓根,绝对路径)
|
|
136
|
+
* @returns {{ applicable: boolean, entryCount: number, findings: Array<object>, warnings: string[] }}
|
|
137
|
+
*/
|
|
138
|
+
export function evaluateRedlines({ entries, root }) {
|
|
139
|
+
const warnings = [];
|
|
140
|
+
const findings = [];
|
|
141
|
+
const list = Array.isArray(entries) ? entries : [];
|
|
142
|
+
for (const entry of list) {
|
|
143
|
+
try {
|
|
144
|
+
const files = resolveScopeFiles(entry.scope, root);
|
|
145
|
+
const contents = new Map();
|
|
146
|
+
const readIfBig = (rel) => {
|
|
147
|
+
if (!contents.has(rel)) {
|
|
148
|
+
const abs = join(root, rel);
|
|
149
|
+
let text = '';
|
|
150
|
+
try {
|
|
151
|
+
if (statSync(abs).size > MAX_FILE_BYTES) {
|
|
152
|
+
warnings.push(`条目 ${entry.id}:${rel} 超 2MB 跳过`);
|
|
153
|
+
} else {
|
|
154
|
+
text = readFileSync(abs, 'utf8');
|
|
155
|
+
}
|
|
156
|
+
} catch (e) {
|
|
157
|
+
warnings.push(`条目 ${entry.id}:读 ${rel} 失败(${e && e.code ? e.code : 'error'}),跳过该文件`);
|
|
158
|
+
}
|
|
159
|
+
contents.set(rel, text);
|
|
160
|
+
}
|
|
161
|
+
return contents.get(rel);
|
|
162
|
+
};
|
|
163
|
+
for (const rel of files) {
|
|
164
|
+
const text = readIfBig(rel);
|
|
165
|
+
if (!text) continue;
|
|
166
|
+
const lines = text.split('\n');
|
|
167
|
+
for (const re of entry._res ? entry._res.forbid : []) {
|
|
168
|
+
for (let i = 0; i < lines.length; i++) {
|
|
169
|
+
const m = lines[i] && re.exec(lines[i]);
|
|
170
|
+
if (m) {
|
|
171
|
+
findings.push({
|
|
172
|
+
id: entry.id, severity: entry.severity, kind: 'forbid',
|
|
173
|
+
file: rel, line: i + 1, snippet: String(m[0]).slice(0, 160),
|
|
174
|
+
statement: entry.statement, origin: entry.origin,
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
for (const re of entry._res ? entry._res.require : []) {
|
|
181
|
+
const satisfied = [...contents.values()].some((text) => re.test(text));
|
|
182
|
+
if (!satisfied) {
|
|
183
|
+
findings.push({
|
|
184
|
+
id: entry.id, severity: 'warning', kind: 'require-missing',
|
|
185
|
+
statement: entry.statement, origin: entry.origin,
|
|
186
|
+
detail: `require 模式在 scope 全集(${files.length} 文件)无命中`,
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
} catch (e) {
|
|
191
|
+
warnings.push(`条目 ${entry.id} 求值异常跳过(${e && e.message ? e.message : e})`);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
return { applicable: true, entryCount: list.length, findings, warnings };
|
|
195
|
+
}
|
package/src/run/command.js
CHANGED
|
@@ -1325,6 +1325,38 @@ export async function runCommand(args, cwd, specDir = null, opts = {}) {
|
|
|
1325
1325
|
} catch { /* claim 失败不阻断启动 */ }
|
|
1326
1326
|
}
|
|
1327
1327
|
|
|
1328
|
+
// ── 所有权写路径断言(2026-09-20 双写碰撞实证修复):claim 只「不抢不拒」,写操作还须
|
|
1329
|
+
// assert——今晚实证两会话并行驱动同一 change(owner=zcode-fr-l2 他人活跃中,另一会话的
|
|
1330
|
+
// 三条 --done 直通落库,步骤槽混写)。拦截面=写 flag(--done/--reset/--reopen/--skip/
|
|
1331
|
+
// --answer/--confirm/--files:quick 边界追加也是写);读路径(prompt 显示/--status)不拦
|
|
1332
|
+
// (index.js apply/cleanup 只读不接同款)。语义同接管类:self/no-owner 放行;
|
|
1333
|
+
// takeover-stale(活跃窗外)放行并自动接管重写 owner;blocked-active-owner 拒绝(结构化
|
|
1334
|
+
// 错误 + 指引);--takeover 显式强制(逃生阀,重写留痕)。quick 会话身份=changeName 恒
|
|
1335
|
+
// self 零影响;default 容器跳过(同上方 claim)。fail-open:断言异常不拦(护栏退 no-owner)。
|
|
1336
|
+
if (effectiveChange && effectiveChange !== 'default') {
|
|
1337
|
+
const _isWriteOp = isDone || isReset || isReopen || isSkip
|
|
1338
|
+
|| flags.includes('--answer') || flags.includes('--confirm') || flags.includes('--files');
|
|
1339
|
+
if (_isWriteOp) {
|
|
1340
|
+
try {
|
|
1341
|
+
const ident = resolveSessionIdentity({
|
|
1342
|
+
flagSession: getFlagValue('--session'),
|
|
1343
|
+
quickChangeName: stageName === 'quick' ? effectiveChange : null,
|
|
1344
|
+
cwd: dirname(specRoot),
|
|
1345
|
+
})
|
|
1346
|
+
const check = pm.assertChangeOwnership(cwd, effectiveChange, {
|
|
1347
|
+
selfSession: ident.session, nowMs: Date.now(), forced: flags.includes('--takeover'),
|
|
1348
|
+
})
|
|
1349
|
+
if (check.allowed) {
|
|
1350
|
+
if (check.action !== 'self') pm.setChangeOwner(cwd, effectiveChange, ident.session)
|
|
1351
|
+
} else {
|
|
1352
|
+
console.error(`\n🚫 变更 ${effectiveChange} 正被其他会话持有(owner: ${check.owner},最后活跃: ${check.lastActive || '未知'}),活跃窗口 ${Math.round(check.heartbeatMs / 60000)} 分钟内拒绝写操作(--done/--reset/--reopen/--skip/--answer/--confirm/--files)`)
|
|
1353
|
+
console.error(` 两个会话同时驱动同一 change 会步骤槽混写(2026-09-20 实证)。确认对方已放弃:等活跃窗过期后重试(自动接管),或本条命令加 --takeover 显式强制接管(重写 owner 留痕);或与对方会话协调归属。`)
|
|
1354
|
+
process.exit(1)
|
|
1355
|
+
}
|
|
1356
|
+
} catch { /* 断言异常不拦(fail-open,同 claim) */ }
|
|
1357
|
+
}
|
|
1358
|
+
}
|
|
1359
|
+
|
|
1328
1360
|
// --reset
|
|
1329
1361
|
if (isReset) {
|
|
1330
1362
|
return await resetStage(pm, progress, stageName, cwd, effectiveChange, platformOpts)
|
package/src/run/complete.js
CHANGED
|
@@ -588,6 +588,18 @@ export async function completeStep(pm, progress, stageName, cwd, outputText, inp
|
|
|
588
588
|
}
|
|
589
589
|
}
|
|
590
590
|
|
|
591
|
+
// verify 批量完成检测(P0-1 verify 尾巴瘦身,2026-09-20 对撞实验驱动):报告产物齐
|
|
592
|
+
// (verify-result.md 结论枚举已填 + verify-facts.json 在场)且语义锚定步(step 1-2)
|
|
593
|
+
// 已过 → 剩余 step 一次性标 completed,本次 --done 直接进阶段完成分支——收尾门禁
|
|
594
|
+
// (测试对账/验收矩阵/module-impact 死信/PASS 封顶)照常全跑:批量省的是 --done
|
|
595
|
+
// 肥上下文往返,不省任何门。镜像 execute 批量模式(乐观预标戳 + 失败一并回滚)。
|
|
596
|
+
if (stageName === 'verify' && changeName) {
|
|
597
|
+
const _vb = detectVerifyBatchFinish({ changeName, specBase, steps })
|
|
598
|
+
if (_vb.batched && _vb.aligned > 0) {
|
|
599
|
+
console.log(`\n🚀 verify 批量完成:报告结论已填 + facts 在场,一次性补完 ${_vb.aligned} 个剩余 step → 进入阶段完成分支(收尾门禁照常全跑)`)
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
|
|
591
603
|
const nextPendingIdx = steps.findIndex(s => s.status === 'pending' || s.status === 'in-progress')
|
|
592
604
|
|
|
593
605
|
if (nextPendingIdx === -1) {
|
|
@@ -702,6 +714,13 @@ export async function completeStep(pm, progress, stageName, cwd, outputText, inp
|
|
|
702
714
|
const total = steps.length
|
|
703
715
|
console.log(`✅ ${stageName} 阶段已完成(${total}/${total} 步)`)
|
|
704
716
|
|
|
717
|
+
// 瘦会话交接提示(P1-5 v1,2026-09-20 对撞实验驱动):主流程阶段完成时点提示
|
|
718
|
+
// 「下一阶段可新会话续跑」——CLI prompt 自足(进度/上下文全由进度库与注入提供),
|
|
719
|
+
// 每阶段一个瘦会话省掉肥上下文 × N 请求的重发税。advisory 一行,不强制。
|
|
720
|
+
if (['brainstorm', 'plan', 'execute', 'verify'].includes(stageName) && changeName) {
|
|
721
|
+
console.log(`💡 瘦会话模式:下一阶段可在新会话续跑(sillyspec handoff --change ${changeName} 生成交接块;保持 SILLYSPEC_SESSION_ID 不变)——省肥上下文重发税`)
|
|
722
|
+
}
|
|
723
|
+
|
|
705
724
|
// 平台模式产物落点指针(platform-docs-dual-location,2026-09-15 EHS 实证):完成时点在
|
|
706
725
|
// 工作树 .sillyspec/ 刷新人类可读指针(本地模式零行为,fail-soft 内部全兜)。
|
|
707
726
|
writePlatformDocsPointer(cwd, changeName, platformOpts, stageName)
|
|
@@ -1267,8 +1286,47 @@ export async function autoCheckPlanFromReviews({ stageName, changeName, cwd, pla
|
|
|
1267
1286
|
* 安全门:注册表零 checkbox / 未全勾 / 代码零变更(unchanged)均不批量——信任声明但用代码核验兜底。
|
|
1268
1287
|
* @returns {Promise<{batched:boolean, aligned:number, reason?:string, blockedTasks?:string[]}>}
|
|
1269
1288
|
*/
|
|
1270
|
-
|
|
1271
|
-
|
|
1289
|
+
/**
|
|
1290
|
+
* verify 批量完成检测(P0-1,2026-09-20):镜像 execute 的 detectExecuteBatchFinish。
|
|
1291
|
+
* 条件(全部满足才批量):verify-result.md 结论枚举已填(三态任一,非待填占位)+
|
|
1292
|
+
* verify-facts.json 在场(探针跑过)+ 锚定步(step 1-2)已完成。批量只标剩余 step
|
|
1293
|
+
* completed,收尾门禁由阶段完成分支照常执行(批量不绕门)。
|
|
1294
|
+
*/
|
|
1295
|
+
export function detectVerifyBatchFinish({ changeName, specBase, steps }) {
|
|
1296
|
+
if (!changeName) return { batched: false, aligned: 0 }
|
|
1297
|
+
try {
|
|
1298
|
+
const changeDir = join(specBase, 'changes', changeName)
|
|
1299
|
+
const reportPath = join(changeDir, 'verify-result.md')
|
|
1300
|
+
if (!existsSync(reportPath)) return { batched: false, aligned: 0, reason: 'verify-result.md 缺席(报告未写不批量)' }
|
|
1301
|
+
const report = readFileSync(reportPath, 'utf8')
|
|
1302
|
+
if (!/结论枚举[::]\s*`?\s*(PASS WITH NOTES|PASS|FAIL)/.test(report)) {
|
|
1303
|
+
return { batched: false, aligned: 0, reason: '结论枚举未填(待填占位不批量)' }
|
|
1304
|
+
}
|
|
1305
|
+
if (!existsSync(join(changeDir, 'verify-facts.json'))) {
|
|
1306
|
+
return { batched: false, aligned: 0, reason: 'verify-facts.json 缺席(探针未跑不批量)' }
|
|
1307
|
+
}
|
|
1308
|
+
const firstPendingIdx = steps.findIndex(s => s.status === 'pending' || s.status === 'in-progress')
|
|
1309
|
+
if (firstPendingIdx === -1) return { batched: false, aligned: 0, reason: '无剩余 step' }
|
|
1310
|
+
if (firstPendingIdx < 2) {
|
|
1311
|
+
return { batched: false, aligned: 0, reason: '锚定步未过(step 1-2 须逐步完成,语义锚定不批量)' }
|
|
1312
|
+
}
|
|
1313
|
+
const now = new Date().toLocaleString('zh-CN', { hour12: false })
|
|
1314
|
+
let aligned = 0
|
|
1315
|
+
for (const step of steps) {
|
|
1316
|
+
if (step.status === 'pending' || step.status === 'in-progress') {
|
|
1317
|
+
step.status = 'completed'
|
|
1318
|
+
step.completedAt = now
|
|
1319
|
+
step._batchAligned = true // 乐观预标戳:gate 失败 rollback 一并回滚(execute 同款)
|
|
1320
|
+
aligned++
|
|
1321
|
+
}
|
|
1322
|
+
}
|
|
1323
|
+
return { batched: true, aligned }
|
|
1324
|
+
} catch (e) {
|
|
1325
|
+
return { batched: false, aligned: 0, reason: `批量完成检测异常(fail-open 单步推进):${e && e.message ? e.message : e}` }
|
|
1326
|
+
}
|
|
1327
|
+
}
|
|
1328
|
+
|
|
1329
|
+
async function detectExecuteBatchFinish({ pm, stageName, changeName, cwd, specBase, platformOpts, steps }) { if (stageName !== 'execute' || !changeName) return { batched: false, aligned: 0 }
|
|
1272
1330
|
try {
|
|
1273
1331
|
const changeDir = join(specBase, 'changes', changeName)
|
|
1274
1332
|
const { total: planTotal, checked: planChecked } = pm.readPlanCheckboxStatus(changeDir)
|
package/src/run/gate-snapshot.js
CHANGED
|
@@ -352,7 +352,7 @@ export function applyGateSnapshotCopy(cwd, snapshotRoot) {
|
|
|
352
352
|
* @param {{ cwd: string, files: string[] }} opts cwd=主仓根;files=本会话变更文件(仓库根相对 POSIX)
|
|
353
353
|
* @returns {{ snapshotRoot: string, cleanup: () => void, reason?: string }|null} 失败返回 null(调用方回退主仓)
|
|
354
354
|
*/
|
|
355
|
-
export function createGateSnapshot({ cwd, files, sourceRoot = null, skipImportSmoke = false }) {
|
|
355
|
+
export function createGateSnapshot({ cwd, files, sourceRoot = null, skipImportSmoke = false, mergeBase = null }) {
|
|
356
356
|
let snapshotRoot = null
|
|
357
357
|
try {
|
|
358
358
|
// 前置:主仓须是 git 仓且有 HEAD(无 git 环境回退主仓现行为)
|
|
@@ -381,10 +381,47 @@ export function createGateSnapshot({ cwd, files, sourceRoot = null, skipImportSm
|
|
|
381
381
|
// local.yaml 由下方 cfg 段显式复制(单一来源,不经 overlay)。
|
|
382
382
|
const overlayRoot = sourceRoot || cwd
|
|
383
383
|
let overlaid = 0
|
|
384
|
+
// 双写一致性(2026-09-20 红线机检变更实证:主 agent 在主仓直写而 execute 已建 worktree 时,
|
|
385
|
+
// overlay 源=worktree 拿陈旧内容盖掉快照 HEAD 的新版——快照内 import 找不到新导出→门禁假红)。
|
|
386
|
+
// 祖先判据(fr-index-l2 二次实证升级):基线优先取 merge-base 的每文件祖先内容
|
|
387
|
+
//(git show mb:file——worktree 有自家 checkpoint 提交时,快照 HEAD 不是公共祖先,
|
|
388
|
+
// wtDiff/cwdDiff 同谱不可分;祖先内容可精确分「worktree 停在分叉点=陈旧」与「真有修改」)。
|
|
389
|
+
// 两份都变于祖先且互不相等(真双写分叉)→ 显式警告 + 取 cwd(主仓直写=本会话最新编辑位约定)。
|
|
390
|
+
// 行尾归一(坑 autocrlf-byte-misjudge):本机 autocrlf=true 时快照 checkout 是 CRLF、
|
|
391
|
+
// 工作区文件常是 LF——字节直比会误判「两边都改」。比较一律 CRLF→LF 归一后做。
|
|
392
|
+
const normalizeEol = (buf) => String(buf).replace(/\r\n/g, '\n')
|
|
393
|
+
const headFileContent = (rel) => {
|
|
394
|
+
if (mergeBase) {
|
|
395
|
+
try {
|
|
396
|
+
const anc = git(cwd, ['show', `${mergeBase}:${rel}`])
|
|
397
|
+
if (typeof anc === 'string') return normalizeEol(anc)
|
|
398
|
+
} catch { /* 祖先无此文件(新增文件)→ 落到快照 HEAD 口径 */ }
|
|
399
|
+
}
|
|
400
|
+
try { return normalizeEol(readFileSync(join(snapshotRoot, rel))) } catch { return null }
|
|
401
|
+
}
|
|
384
402
|
for (const f of files) {
|
|
385
403
|
const isSillyspecRuntime = typeof f === 'string' && (f.startsWith('.sillyspec/.runtime/') || f.startsWith('.sillyspec/quicklog/') || f === '.sillyspec/local.yaml' || f === '.sillyspec/.sillyspec-platform.json')
|
|
386
404
|
if (typeof f !== 'string' || f.includes('..') || f.startsWith('/') || isSillyspecRuntime) continue
|
|
387
|
-
|
|
405
|
+
let src = join(overlayRoot, f)
|
|
406
|
+
if (sourceRoot && sourceRoot !== cwd) {
|
|
407
|
+
const wtPath = join(sourceRoot, f), cwdPath = join(cwd, f)
|
|
408
|
+
if (existsSync(wtPath) && existsSync(cwdPath)) {
|
|
409
|
+
let wtText = null, cwdText = null
|
|
410
|
+
try { wtText = normalizeEol(readFileSync(wtPath)) } catch { /* 读失败不介入 */ }
|
|
411
|
+
try { cwdText = normalizeEol(readFileSync(cwdPath)) } catch { /* 读失败不介入 */ }
|
|
412
|
+
if (wtText !== null && cwdText !== null && wtText !== cwdText) {
|
|
413
|
+
const base = headFileContent(f)
|
|
414
|
+
const wtDiff = base === null || wtText !== base
|
|
415
|
+
const cwdDiff = base === null || cwdText !== base
|
|
416
|
+
if (cwdDiff && !wtDiff) {
|
|
417
|
+
src = cwdPath // 主仓直写新、worktree 停在基线 → 取主仓
|
|
418
|
+
} else if (!(wtDiff && !cwdDiff)) {
|
|
419
|
+
console.warn(`⚠️ 快照双写分叉:${f} 在主仓与 worktree 均有不同修改——取主仓(本会话最新编辑位),请尽快对齐两边`)
|
|
420
|
+
src = cwdPath
|
|
421
|
+
} // wtDiff && !cwdDiff → 保持 worktree(正常流零改变)
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
}
|
|
388
425
|
if (!existsSync(src)) continue
|
|
389
426
|
const dst = join(snapshotRoot, f)
|
|
390
427
|
mkdirSync(dirname(dst), { recursive: true })
|
|
@@ -604,16 +641,30 @@ export async function createVerifyGateSnapshot({ cwd, changeName, specBase, plat
|
|
|
604
641
|
|
|
605
642
|
// overlay 源:worktree meta 感知(native worktree 在则从 worktree 根取「本变更分支内容」)
|
|
606
643
|
let sourceRoot = null
|
|
644
|
+
let snapMergeBase = null
|
|
607
645
|
try {
|
|
608
646
|
const { WorktreeManager } = await import('../worktree.js')
|
|
609
647
|
const wm = new WorktreeManager({ cwd })
|
|
610
648
|
const meta = wm.getMeta(changeName)
|
|
611
649
|
if (meta && meta.worktreePath && meta.mode !== 'in-place-fallback' && existsSync(meta.worktreePath)) {
|
|
612
650
|
sourceRoot = meta.worktreePath
|
|
651
|
+
// 陈旧 worktree 判定(2026-09-20 fr-index-l2 实证):主 agent 在主仓直写并提交后,
|
|
652
|
+
// worktree 停在基线——内容三方(worktree/cwd/快照HEAD)判不出「领先」与「落后」
|
|
653
|
+
// (同谱)。精确判据 = merge-base 的每文件祖先内容(git show mb:file):
|
|
654
|
+
// worktree 内容==祖先 → 陈旧(主仓取新);≠ → worktree 有真修改(正常流保 worktree)。
|
|
655
|
+
// 此处只算 mergeBase 一次(拓扑级),每文件祖先内容由 createGateSnapshot 按需取。
|
|
656
|
+
try {
|
|
657
|
+
const wtTip = git(sourceRoot, ['rev-parse', 'HEAD'])
|
|
658
|
+
const mainHead = git(cwd, ['rev-parse', 'HEAD'])
|
|
659
|
+
const mb = (wtTip && mainHead && wtTip !== mainHead) ? git(cwd, ['merge-base', wtTip, mainHead]) : null
|
|
660
|
+
if (wtTip && mainHead && wtTip !== mainHead && mb) {
|
|
661
|
+
snapMergeBase = String(mb).trim()
|
|
662
|
+
}
|
|
663
|
+
} catch { /* 拓扑判定失败退快照HEAD口径(既有兜底) */ }
|
|
613
664
|
}
|
|
614
665
|
} catch { /* meta 读取失败 → 主仓 cwd 源(files 已含本变更 working-tree 改动) */ }
|
|
615
666
|
|
|
616
|
-
const snap = createGateSnapshot({ cwd, files: changeFiles, sourceRoot })
|
|
667
|
+
const snap = createGateSnapshot({ cwd, files: changeFiles, sourceRoot, mergeBase: snapMergeBase })
|
|
617
668
|
if (!snap) return null
|
|
618
669
|
|
|
619
670
|
// 变更文档随快照:module-impact.md(test_strategy=evidence-auto 消费)/tasks.md 等
|
package/src/run/prompt.js
CHANGED
|
@@ -1151,9 +1151,9 @@ export async function outputStep(stageName, stepIndex, steps, cwd, changeName, d
|
|
|
1151
1151
|
if (entries.length === 0) {
|
|
1152
1152
|
promptText = promptText.replace(/\{FR_INDEX_DIGEST\}/g, '(触达域暂无 active FR 索引条目——本变更大概率是这些域的首批需求,照常写作)')
|
|
1153
1153
|
} else {
|
|
1154
|
-
const lines = entries.map((e) => `- ${e.id} ${e.title}(来源 ${e.change}${e.scenarios.length ? ';场景:' + e.scenarios.slice(0, 3).join(',') : ''})`)
|
|
1154
|
+
const lines = entries.map((e) => `- ${e.id} ${e.title}(来源 ${e.change}${e.scenarios.length ? ';场景:' + e.scenarios.slice(0, 3).join(',') : ''}${(e.decisions || []).length ? ';依据:' + e.decisions.slice(0, 3).join('、') : ''})`)
|
|
1155
1155
|
lines.push('')
|
|
1156
|
-
lines.push('> 域解析自本变更 design.md 文件清单(漏域先核对清单)。改写/取代已有行为 → 对应 FR 块加承接行;新行为 → 新 FR 块。superseded 条目默认不列(历史回溯自行读 knowledge/fr
|
|
1156
|
+
lines.push('> 域解析自本变更 design.md 文件清单(漏域先核对清单)。改写/取代已有行为 → 对应 FR 块加承接行;新行为 → 新 FR 块。superseded 条目默认不列(历史回溯自行读 knowledge/fr/)。依据决策(L2)= 当年取舍锚——翻案须先读 knowledge/decisions/<域>.md 的否决理由,满足复潮条件走 D-xxx@vN+1,不得静默改行为。承接行可带退役理由:`承接: FR-<域>-NNN(退役理由:一句话)`——归档时写进被取代条目(理由内禁逗号);条目正文是截断摘要,全文锚(全文:<归档路径>#FR-NN)由 CLI 自动落。')
|
|
1157
1157
|
promptText = promptText.replace(/\{FR_INDEX_DIGEST\}/g, lines.join('\n'))
|
|
1158
1158
|
}
|
|
1159
1159
|
try {
|
package/src/run/quick-audit.js
CHANGED
|
@@ -145,7 +145,7 @@ export function printQuickAuditReview(review) {
|
|
|
145
145
|
if (review.foreignSpecChurn.length > 6) console.warn(` … 共 ${review.foreignSpecChurn.length} 个`)
|
|
146
146
|
}
|
|
147
147
|
if (review.eolOnlyFiles && review.eolOnlyFiles.length > 0) {
|
|
148
|
-
console.warn(`⚠️ ${review.eolOnlyFiles.length}
|
|
148
|
+
console.warn(`⚠️ ${review.eolOnlyFiles.length} 个文件仅行尾重写(内容零变化,生成命令/编辑器 CRLF 假 M——已剔出本会话审计):${review.eolOnlyFiles.slice(0, 5).join('、')}${review.eolOnlyFiles.length > 5 ? ' 等' : ''}`)
|
|
149
149
|
console.warn(` 根治:.gitattributes 声明 text=auto eol=lf(或按文件类型钉死),生成器重写不再制造假 M`)
|
|
150
150
|
}
|
|
151
151
|
if (review.docsCheckHint && review.docsCheckHint.invalid > 0) {
|
|
@@ -415,7 +415,17 @@ export async function runQuickTestLintGate({ cwd, specBase, changedFiles = [], d
|
|
|
415
415
|
// 倒推 B 模式兜底:审计口径为空时回退声明边界(文件早于会话启动被基线吸收的场景)
|
|
416
416
|
const files = audited.length > 0 ? audited : (Array.isArray(declaredFiles) ? declaredFiles : [])
|
|
417
417
|
const fileSource = audited.length > 0 ? '审计' : (files.length > 0 ? '声明边界兜底(倒推 B:文件早于会话启动被基线吸收)' : '无')
|
|
418
|
-
|
|
418
|
+
// 代码文件判定(2026-09-19 monorepo 子包实证修复:multi-agent-platform 回带收口 6 个
|
|
419
|
+
// sillyhub-daemon/src/**、frontend/src/** 文件被「纯 doc/配置」误判跳过实测):旧口径只认
|
|
420
|
+
// 仓根 src/、test/ 前缀——monorepo 子包(<pkg>/src/**、backend/app/**.py)全漏。
|
|
421
|
+
// 新口径双通道,方向取「宁可多跑不可漏跑」(门禁漏跑=静默放行,多跑只是费一次实测):
|
|
422
|
+
// ① 路径段匹配:任意层出现 src / test / tests / __tests__ 段(src-guide 等长名不误蹭——按段全等)
|
|
423
|
+
// ② 代码扩展名兜底:src/test 目录约定外的代码(backend/app/**.py、scripts/*.go 等)
|
|
424
|
+
const CODE_PATH_SEGMENT_RE = /(^|\/)(src|tests?|__tests__)(\/|$)/
|
|
425
|
+
const CODE_EXTENSION_RE = /\.(?:js|mjs|cjs|ts|tsx|jsx|py|pyw|go|rs|java|kt|kts|rb|php|cs|c|h|cpp|cc|hpp|swift|scala|groovy|vue|svelte)$/
|
|
426
|
+
const codeFiles = files.filter(
|
|
427
|
+
(f) => typeof f === 'string' && (CODE_PATH_SEGMENT_RE.test(f) || CODE_EXTENSION_RE.test(f)),
|
|
428
|
+
)
|
|
419
429
|
if (files.length === 0) {
|
|
420
430
|
return { action: 'skip', failed: [], reason: `无变更文件清单(${fileSource}口径均空——brownfield 无 guard 或空审计),跳过`, test: null, lint: null }
|
|
421
431
|
}
|
|
@@ -176,7 +176,7 @@ export async function renderVerifyTestAttribution({ cwd, changeName, specBase, r
|
|
|
176
176
|
const { filterStaleBaselineOverlap } = await import('./gates.js')
|
|
177
177
|
const actionable = filterStaleBaselineOverlap(overlap, dirtySet, foreignMap)
|
|
178
178
|
if (actionable.length > 0) {
|
|
179
|
-
console.error(` ℹ️ 归因提示②:本变更有 ${actionable.length} 个文件与主仓最近 10 条提交重叠且旧内容仍在工作区(${actionable.slice(0, 5).join(', ')}${actionable.length > 5 ? ' 等' : ''}
|
|
179
|
+
console.error(` ℹ️ 归因提示②:本变更有 ${actionable.length} 个文件与主仓最近 10 条提交重叠且旧内容仍在工作区(${actionable.slice(0, 5).join(', ')}${actionable.length > 5 ? ' 等' : ''})——若为生成产物(类型生成/代码生成产物等),本变更可能在旧基线生成并覆盖了已合入内容;先在新基线重跑生成命令再复验。(重叠共 ${overlap.length} 个,其余未在工作区改动或属他者会话在途文件,已归提示①/无需处理)`)
|
|
180
180
|
}
|
|
181
181
|
}
|
|
182
182
|
}
|
|
@@ -106,7 +106,7 @@ AC-002: 未修改数据库 schema
|
|
|
106
106
|
AC-003: 未涉及鉴权/权限
|
|
107
107
|
AC-004: 未扩大 allowed_paths
|
|
108
108
|
AC-005: 未引入新外部依赖
|
|
109
|
-
AC-006:
|
|
109
|
+
AC-006: 未修改核心模块(模块索引中被多方依赖或承担关键流程的模块)
|
|
110
110
|
AC-007: 已有项目约定可复用
|
|
111
111
|
AC-008: 不影响向后兼容
|
|
112
112
|
AC-009: 不涉及数据迁移
|
package/src/stages/brainstorm.js
CHANGED
|
@@ -76,7 +76,10 @@ export const definition = {
|
|
|
76
76
|
maxWaitRounds: 8,
|
|
77
77
|
waitReason: '等待用户回答需求问题或澄清',
|
|
78
78
|
waitOptions: ['回答见--answer', '信息够了,进入方案讨论'],
|
|
79
|
-
prompt:
|
|
79
|
+
prompt: `### ⚡ 薄跑判定(P0-3,2026-09-20 对撞实验驱动——需求已收敛来源走全仪探索是纯税)
|
|
80
|
+
**需求来源已收敛**(满足任一:--input 显式声明承接/已澄清/方案已定;fork 或关联会话已多轮澄清过本需求,你已能不问自答全部 P0 歧义;任务书/评审结论直接给定方案与边界)→ 本步可**薄跑**:A-D 节按已收敛事实直书结论(歧义零清单、范围直裁、决策若已有则引用源会话不重问),一次性完成不再多轮追问。薄跑不降质量底线:范围结论与决策落盘照常,仅省「明知故问」的交互轮。Grill(Step 7)与规范文件(Step 8)**不适用薄跑、永不省**。
|
|
81
|
+
|
|
82
|
+
通过对话探索需求细节、分析原型(如有)、判断拆分/批量、并对歧义点做需求澄清 Grill。本步骤合并了原有的对话探索、原型分析、需求范围评估、需求澄清 Grill 四个环节——按下方 A→D 顺序按需执行。提问遵守「一次只问一个」(见 D 节追问策略):必须问的问题逐个问,能查代码/文档解决的不问;不要把多个歧义打包成一屏清单一次抛给用户(用户记不住、答不全,反而漏掉关键约束);也不要为凑数把单个问题拖成多轮。
|
|
80
83
|
|
|
81
84
|
### A. 原型/设计图分析(如用户提供)
|
|
82
85
|
如果用户提供了截图、图片或 HTML 原型,先分析提取:
|
|
@@ -297,12 +300,12 @@ design.md 第一行标题必须用中文:# 设计文档(Design)— <变更
|
|
|
297
300
|
|
|
298
301
|
**路径存在性核验(NEW: 前缀铁律,2026-09-17 用户反馈⑥-①前移)**:清单条目在「生成规范文件」步 \`--done\` 门禁会逐一核验仓内存在性——**计划新建的文件必须在路径前加 \`NEW:\` 前缀**(冒号后不加空格,\`NEW:src/foo.js\` 合法、\`NEW: src/foo.js\` 属书写错误);未加前缀的新建路径会以 \`design_file_ref_invalid\`(幻觉路径/书写错误)在末步被拦,返工一轮。已有文件写仓根相对正斜杠路径,禁绝对路径/glob。
|
|
299
302
|
**字段数据流标注**(避免新增字段到 execute 才发现没透传):当清单含「新增/修改对外字段、接口、DTO、响应体、事件 payload、配置键」时,对应行「说明」列必须交代 producer→consumer 数据流——产出方(producer)→ 每跳流转/归一化点(序列化/反序列化/字段映射/默认值兜底)→ 消费方(consumer)。漏标 = 字段在某跳 dormant(声明了却没透传),到 execute/verify 才暴露,属设计层缺口。
|
|
300
|
-
- 示例:「修改
|
|
303
|
+
- 示例:「修改 api/server.ts:新增 quota_remaining 字段,producer=服务端 types/api.ts → 消息 envelope 透传 → 客户端 normalize(snake→camel)→ consumer=runtime/quota.ts 写入」
|
|
301
304
|
- 仅改内部实现、无对外字段变动时,说明列照常写「新增 xx 方法」即可,无需数据流。
|
|
302
305
|
|
|
303
306
|
**跨仓变更写法**(本次变更涉及 local.yaml repos: 注册的其他仓时必读;纯主仓变更跳过):
|
|
304
|
-
- 清单**按仓分段**:每个仓一个小节,标题格式固定 \`## <repo-key> 仓变更\`(如 \`##
|
|
305
|
-
- 段内路径相对**该仓根**写(\`src/routes/x.js\`);❌
|
|
307
|
+
- 清单**按仓分段**:每个仓一个小节,标题格式固定 \`## <repo-key> 仓变更\`(如 \`## user-service 仓变更\`;可加编号前缀或(跨仓,X1-X4)这类括号备注后缀,其余后缀/写法解析不了)。plan-postcheck 文件覆盖对账按此段头解析仓归属,写成「前端仓 / backend」等其他标题 → 对账不上且会点名段头格式错误
|
|
308
|
+
- 段内路径相对**该仓根**写(\`src/routes/x.js\`);❌ 禁止带仓库名前缀(\`<repo-key>/src/routes/x.js\`)、❌ 禁止绝对路径/盘符路径(\`C:/repo/src/...\`)——后续 TaskCard 的 allowed_paths 与 review 对账(\`git -C <仓根> diff\` 产仓根相对路径)都用同一口径,前缀/绝对路径永不命中
|
|
306
309
|
- 主仓文件放在第一个段头之前(无段头区域 = main),或单独写 \`## main 仓变更\` 段
|
|
307
310
|
- repo-key 必须是 local.yaml repos: 段已注册的键(未注册先跑 \`sillyspec local register-repo <key> <仓根路径>\`,main 隐式不用注册)
|
|
308
311
|
|
package/src/stages/execute.js
CHANGED
|
@@ -886,7 +886,7 @@ export function buildWavePrompt(wave, waveIndex, changeDir, worktreePath, option
|
|
|
886
886
|
if (relevantContracts.length > 0) {
|
|
887
887
|
contractInjection = `
|
|
888
888
|
### API Contract Matrix
|
|
889
|
-
本 Wave
|
|
889
|
+
本 Wave 存在跨 task 契约(provider → consumer):
|
|
890
890
|
${relevantContracts.map(c => `- **${c.consumer}** 消费 **${c.provider}** 产出的 API`).join('\n')}
|
|
891
891
|
`
|
|
892
892
|
for (const taskName of waveTasks) {
|
|
@@ -1236,7 +1236,7 @@ ${workdirLines}
|
|
|
1236
1236
|
|
|
1237
1237
|
⚠️ **铁律:spec 流程产物只写主仓 {SPEC_ROOT},绝不写进 worktree 副本**——包括 module-impact.md / knowledge 条目 / 模块卡与 \`<module>.changelog.md\` sidecar。在 worktree 内发现 \`.sillyspec/\` 目录是 checkout 副本,写进去的任何内容都会随 worktree cleanup 整目录蒸发(2026-09-10 实证:模块文档写副本、归档被迫 checkout 补救)。子代理 prompt 中涉及此类产物时,必须原样带上主仓绝对路径。
|
|
1238
1238
|
|
|
1239
|
-
⚠️ **Python
|
|
1239
|
+
⚠️ **Python 导入链陷阱(venv editable install,2026-09-18 实证)**:worktree 内跑任何 \`import <项目包>\` / dump_openapi / 代码生成类命令时,若用的是主仓 venv(junction 链接或裸调主仓 python),其 editable install(\`_editable_impl_*.pth\`)指向**主仓**绝对路径——项目包 import 会静默解析到主仓旧代码,产出旧 schema/旧行为且零报错(看似「改动没生效」)。**跑生成链前设 \`PYTHONPATH=<worktree>/<项目源码根>\`**(PYTHONPATH 优先于 .pth),或用 worktree 自建 venv;\`sillyspec worktree doctor\` 的 editable-install-escape 检查可提前暴露。
|
|
1240
1240
|
`
|
|
1241
1241
|
: ''
|
|
1242
1242
|
}
|
package/src/stages/plan.js
CHANGED
|
@@ -92,7 +92,7 @@ const stepClassify = {
|
|
|
92
92
|
判定为 full(满足任一即升为 full):
|
|
93
93
|
- 预计 8 个以上 task
|
|
94
94
|
- 跨 3 个以上模块
|
|
95
|
-
-
|
|
95
|
+
- 跨技术层联动(同一变更需多层协同改造,如前端 + 服务端 + 数据库)
|
|
96
96
|
- 涉及 agent 调度 / worktree / isolation 逻辑
|
|
97
97
|
- 涉及复杂状态恢复(checkpoint / resume)
|
|
98
98
|
- 需要并行 sub-agent 执行
|
|
@@ -387,7 +387,7 @@ execute/verify 阶段会按实际代码变更更新此文档;archive 阶段会
|
|
|
387
387
|
// ═══════════════════════════════════════════════════════════════
|
|
388
388
|
|
|
389
389
|
/**
|
|
390
|
-
* 构建紧凑 TaskCard
|
|
390
|
+
* 构建紧凑 TaskCard 协调器步骤(默认主 agent 直填;>8 任务且跨模块/上下文紧张时按 batch 派子代理)
|
|
391
391
|
* 每个 task 生成 20~40 行紧凑可执行卡片
|
|
392
392
|
* 允许把多个相关 task 合并为一个 batch,由单个子代理一次生成多张卡片,
|
|
393
393
|
* 在保留 plan 任务拆分完整性的同时减少子代理调用数量。
|
|
@@ -416,7 +416,7 @@ allowed_paths: # ⚠️ 路径相对 repo 声明的
|
|
|
416
416
|
target_files: # 可选。本 task 计划要改动的文件清单(文件级意图,供对账;与 allowed_paths 语义不同——那是权限范围)。骨架已预置 [] 占位行,无明确意图保留 [] 不动
|
|
417
417
|
- src/foo.js # ✅ 精确仓根相对路径(口径同 allowed_paths);计划新建文件加 NEW: 前缀(NEW:src/bar.js)。❌ 禁 glob(src/**)/目录前缀(src/dir/)/绝对路径
|
|
418
418
|
provides: # 可选。仅当本 task 给其他 task 提供接口/DTO/响应时填
|
|
419
|
-
- contract: <DTO或响应类型名> # 如
|
|
419
|
+
- contract: <DTO或响应类型名> # 如 UserDTO
|
|
420
420
|
fields: [field_a, field_b]
|
|
421
421
|
expects_from: # 可选。仅当本 task 消费其他 task 的契约时填
|
|
422
422
|
<provider-task-id>: # 如 task-05(占位符,不要照抄)
|
|
@@ -439,7 +439,7 @@ constraints:
|
|
|
439
439
|
- 边界约束 2(如:不修改传入参数)
|
|
440
440
|
related_tests: # 可选。当本 task 改动会导致既有测试断言失效时填(共享源文件/UI文案/常量/签名变更等,判据=测试是否失败非文件是否共享)
|
|
441
441
|
- path: frontend/src/lib/errors.test.ts # 因本次改动断言失效的既有测试文件
|
|
442
|
-
reason:
|
|
442
|
+
reason: 旧断言依赖本次要重构掉的实现细节,需同步
|
|
443
443
|
---
|
|
444
444
|
|
|
445
445
|
{{include: taskcard-rules}}`
|
|
@@ -472,17 +472,21 @@ ${taskList}
|
|
|
472
472
|
\`\`\`
|
|
473
473
|
幂等,已存在的卡跳过不覆盖;骨架带 LF 行尾 + 闭合 frontmatter + 硬校验 9 字段 + depends_on 反填。**只有主 agent 跑这一次,子代理一律不再运行 taskcard CLI。**
|
|
474
474
|
1. 确认 \`${changeDir}/tasks/\` 目录存在(上一步预生成会自动创建)
|
|
475
|
-
2.
|
|
475
|
+
2. **默认主 agent 自己填卡(P0-2,2026-09-20 对撞实验驱动——6 个填卡子代理 6.7M token 做的是主 agent 可自完成的誊写,独立性价值为零):**
|
|
476
|
+
- task 总数 ≤8 或变更单仓单模块 → 主 agent 逐卡 Edit 填充(骨架已预生成,誊写近乎免费),**不派子代理**
|
|
477
|
+
- 仅当 task 总数 >8 **且**跨多模块/跨仓、或主会话上下文已明显吃紧时,才按 batch 分派子代理(此时并行省墙钟有真实收益)
|
|
478
|
+
3. **(分派形态时)按 batch 分派子代理(减少总子代理数量,而不是一个 task 一个子代理):**
|
|
476
479
|
- 把任务按「同一 Wave + 同一模块/相近能力 + 无跨 batch 强依赖」原则分组
|
|
477
480
|
- **每个 batch 包含 2~4 个 task**;Wave 内任务数 ≤4 时整个 Wave 可作为一个 batch
|
|
478
481
|
- 有提供/消费契约的 task 尽量放到同一 batch(子代理能同时看到 consumer 与 provider,避免契约字段漏配)
|
|
479
482
|
- 跨 Wave 依赖的 task 不要放在同一 batch(子代理只需读 plan.md,但 batch 内 task 的 allowed_paths 不应互相阻塞)
|
|
480
|
-
|
|
481
|
-
4.
|
|
482
|
-
5.
|
|
483
|
-
6.
|
|
483
|
+
- batch 数量封顶 ≤3(13 任务≈3 batch 已足;再多是协调税)
|
|
484
|
+
4. 为每个 batch 启动一个独立子代理(Agent tool),可并行启动多个 batch
|
|
485
|
+
5. 每个子代理使用下方「批量 TaskCard 子代理 prompt」,一次生成该 batch 的全部 task-N.md
|
|
486
|
+
6. 等待所有 batch 子代理完成
|
|
487
|
+
7. 验证每个 task-N.md 文件已生成且非空
|
|
484
488
|
|
|
485
|
-
> 设计意图:plan.md 里 task 数可以较多(能力拆分完整),但 TaskCard
|
|
489
|
+
> 设计意图:plan.md 里 task 数可以较多(能力拆分完整),但 TaskCard 生成阶段的**默认形态是主 agent 直填**——卡是 design/plan 的誊写不是创作,主 agent 刚写完 plan 上下文最热;子代理只在规模/上下文压力真实存在时才有净收益(每个子代理都要重读 design+plan+源码,6 个子代理 6.7M token 的实证教训)。分派时合并 batch,避免子代理数量随 task 数线性爆炸。
|
|
486
490
|
|
|
487
491
|
### 批量 TaskCard 子代理 prompt
|
|
488
492
|
\`\`\`
|
|
@@ -510,7 +514,7 @@ ${taskcardTemplate}
|
|
|
510
514
|
## 约束
|
|
511
515
|
- 每个 task 20~40 行;禁止在 TaskCard 里写实现细节之外的冗长设计
|
|
512
516
|
- 同 batch 内 task 的 allowed_paths 不要互相冲突
|
|
513
|
-
- **跨仓 task 路径口径(涉及 local.yaml repos: 注册仓的变更必读)**:跨仓 task frontmatter 必须填 \`repo: <repo-key>\`(local.yaml repos: 注册键,缺省=main 省略不填);allowed_paths 相对**该仓根**写(\`src/routes/x.js\`),从 design.md「\`## <repo-key> 仓变更\`」段内路径原样搬。❌
|
|
517
|
+
- **跨仓 task 路径口径(涉及 local.yaml repos: 注册仓的变更必读)**:跨仓 task frontmatter 必须填 \`repo: <repo-key>\`(local.yaml repos: 注册键,缺省=main 省略不填);allowed_paths 相对**该仓根**写(\`src/routes/x.js\`),从 design.md「\`## <repo-key> 仓变更\`」段内路径原样搬。❌ 禁止带仓库名前缀(\`<repo-key>/src/...\`)、❌ 禁止绝对路径/盘符(\`C:/repo/src/...\`)——review 对账按 \`git -C <仓根> diff\` 的仓根相对路径匹配 allowed_paths,带前缀/绝对路径永不命中 → task 改完却判「无归属」对账不上
|
|
514
518
|
- 若 task 之间有 provides/expects_from 契约,同 batch 生成时必须字段对齐
|
|
515
519
|
- 只生成本 batch 声明的任务,不要多写或少写
|
|
516
520
|
- 不要在 TaskCard 里泄露 plan.md 中未出现的实现假设
|
|
@@ -538,7 +542,7 @@ ${taskcardTemplate}
|
|
|
538
542
|
|
|
539
543
|
return {
|
|
540
544
|
id: 'generate_blueprints',
|
|
541
|
-
name: '生成 TaskCard
|
|
545
|
+
name: '生成 TaskCard(默认主代理直填,大规模才派 batch)',
|
|
542
546
|
prompt,
|
|
543
547
|
outputHint: 'TaskCard 生成结果',
|
|
544
548
|
optional: false
|