sillyspec 3.27.7 → 3.27.9
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/contract-matrix.js +39 -11
- package/src/index.js +25 -0
- package/src/progress/stage-machine.js +3 -1
- package/src/run/command.js +46 -15
- package/src/run/complete-handlers.js +20 -0
- package/src/run/complete.js +10 -1
- package/src/run/gates.js +38 -3
- package/src/run/multi-repo-context.js +42 -4
- package/src/run/shared.js +1 -1
- package/src/run/stage.js +28 -2
- package/src/stage-contract.js +20 -7
- package/src/stages/execute.js +35 -15
- package/src/stages/plan.js +8 -0
- package/src/task-review.js +7 -0
- package/src/worktree-apply.js +196 -4
- package/src/worktree-cross.js +294 -0
- package/src/worktree-deps.js +156 -58
package/package.json
CHANGED
package/src/contract-matrix.js
CHANGED
|
@@ -117,22 +117,50 @@ export function buildContractMatrix(planContent, changeDir) {
|
|
|
117
117
|
function parseTaskDependencies(registryContent) {
|
|
118
118
|
const deps = {}
|
|
119
119
|
|
|
120
|
-
// 方式 1:
|
|
121
|
-
//
|
|
122
|
-
//
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
120
|
+
// 方式 1: 任务总表表格形式(两种常见格式)
|
|
121
|
+
// A: | task-04 | Wave | 优先级 | 说明 | 依赖 | (task-NN 在独立列)
|
|
122
|
+
// B: | task | desc | deps | (task-NN 在任意列,bare 数字依赖)
|
|
123
|
+
// 逐行按 | 分列解析,避免旧单正则 [^|]* 跨列贪婪误读说明列数字(409/HTTP 状态码)
|
|
124
|
+
// 为假依赖(坑 contract-matrix-false-deps)。
|
|
125
|
+
// deps 列判定:从行尾倒数第一个「非分隔符、非 task 自身、非单数字(Wave/优先级)」的列,
|
|
126
|
+
// 且须含逗号/task- 引用/bare 两位以上数字。
|
|
127
|
+
const lines = registryContent.split('\n')
|
|
128
|
+
for (const line of lines) {
|
|
129
|
+
const taskIdMatch = line.match(/task-(\d+)/i)
|
|
130
|
+
if (!taskIdMatch) continue
|
|
131
|
+
const taskId = `task-${taskIdMatch[1]}`
|
|
132
|
+
|
|
133
|
+
const cells = line.split('|').map(c => c.trim()).filter(c => c.length > 0)
|
|
134
|
+
if (cells.length < 2) continue
|
|
135
|
+
|
|
136
|
+
// 从行尾倒数找 deps 列
|
|
137
|
+
let depCell = null
|
|
138
|
+
for (let i = cells.length - 1; i >= 0; i--) {
|
|
139
|
+
const c = cells[i]
|
|
140
|
+
if (/^-+$/.test(c)) continue // 分隔符 "-"
|
|
141
|
+
if (c.toLowerCase() === taskId.toLowerCase()) continue // task 自身列
|
|
142
|
+
// deps 列候选:含逗号(多依赖)或含 task- 引用
|
|
143
|
+
if (/[,,]/.test(c) || /task-\d+/i.test(c)) { depCell = c; break }
|
|
144
|
+
// bare 数字:须 ≥2 位且位于行尾第2+列(跳过优先级/Wave 列的个位数)
|
|
145
|
+
if (/^\d{2,}$/.test(c) && i >= cells.length - 2) { depCell = c; break }
|
|
146
|
+
}
|
|
147
|
+
if (!depCell) continue
|
|
148
|
+
const depNums = depCell.match(/\d+/g) || []
|
|
149
|
+
deps[taskId] = depNums.map(d => `task-${d.padStart(2, '0')}`)
|
|
128
150
|
}
|
|
129
151
|
|
|
130
152
|
// 方式 2: tasks.md 行内标注 "- [ ] task-04: 名称 (depends_on: task-01,02)"(新家,D-003@v1;
|
|
131
|
-
// 旧 plan.md checkbox
|
|
132
|
-
|
|
153
|
+
// 旧 plan.md checkbox 行内标注同格式兼容)。仅匹配 (depends_on: ...) 括号内内容,
|
|
154
|
+
// 不在自由文本上全文正则(坑 contract-matrix-false-deps:说明列数字被误读为依赖)。
|
|
155
|
+
const dependsPattern = registryContent.matchAll(/task-(\d+)\b[^)]*?\(depends_on:\s*([^)]+)\)/gi)
|
|
133
156
|
for (const match of dependsPattern) {
|
|
134
157
|
const task = `task-${match[1]}`
|
|
135
|
-
const
|
|
158
|
+
const depStr = match[2]
|
|
159
|
+
// 兼容两种写法:task-01, task-02 或 01, 02
|
|
160
|
+
const depList = depStr.split(/[,,]\s*/).map(s => {
|
|
161
|
+
const num = s.trim().match(/\d+/)
|
|
162
|
+
return num ? `task-${num[0].padStart(2, '0')}` : null
|
|
163
|
+
}).filter(Boolean)
|
|
136
164
|
if (!deps[task]) deps[task] = []
|
|
137
165
|
for (const d of depList) {
|
|
138
166
|
if (!deps[task].includes(d)) deps[task].push(d)
|
package/src/index.js
CHANGED
|
@@ -1692,6 +1692,19 @@ SillySpec worktree — git worktree 隔离管理
|
|
|
1692
1692
|
for (const f of result.changedFiles) {
|
|
1693
1693
|
console.log(` ${f}`);
|
|
1694
1694
|
}
|
|
1695
|
+
// 跨仓 worktree 回落报告(坑 cross-repo-no-worktree-isolation):跨仓文件不进主仓
|
|
1696
|
+
// changedFiles(在其所属仓的 diff 里),单独列出防漏看
|
|
1697
|
+
if (Array.isArray(result.crossRepoApplied) && result.crossRepoApplied.length > 0) {
|
|
1698
|
+
for (const c of result.crossRepoApplied) {
|
|
1699
|
+
if (c.changedFiles.length > 0) {
|
|
1700
|
+
console.log(`✅ 跨仓 repo ${c.repoKey} 已回落 ${c.changedFiles.length} 个文件变更(worktree 已清理,分支保留作 review 锚点):`);
|
|
1701
|
+
for (const f of c.changedFiles.slice(0, 20)) console.log(` ${f}`);
|
|
1702
|
+
if (c.changedFiles.length > 20) console.log(` … 共 ${c.changedFiles.length} 个`);
|
|
1703
|
+
} else {
|
|
1704
|
+
console.log(`ℹ️ 跨仓 repo ${c.repoKey} 无交付变更(worktree 已清理)`);
|
|
1705
|
+
}
|
|
1706
|
+
}
|
|
1707
|
+
}
|
|
1695
1708
|
// filterDeliverableFiles 精细化排除(保留 .sillyspec/docs/,排 changes/+.runtime/+quicklog/ + meta.json):
|
|
1696
1709
|
// 模块文档(.sillyspec/docs/)= 交付物,会 auto-apply 到主工作区;变更文档/运行时/quicklog 不 apply。
|
|
1697
1710
|
// 须告知 agent 哪些未落地,否则它期望这些文件落地却找不到(memory: worktree-apply-excludes-module-docs)。
|
|
@@ -1878,6 +1891,18 @@ SillySpec worktree — git worktree 隔离管理
|
|
|
1878
1891
|
// 与并行会话的 apply/cleanup 互踩会互相清——与其他写主仓操作共用一把 main-repo.lock
|
|
1879
1892
|
const { withMainRepoLock } = await import('./worktree-apply.js');
|
|
1880
1893
|
const result = await withMainRepoLock(dir, wtName, 'worktree-cleanup', () => wm.cleanup(wtName, { force: forceFlag }));
|
|
1894
|
+
// 跨仓 worktree(坑 cross-repo-no-worktree-isolation):与主仓同锁清理——跨仓 worktree
|
|
1895
|
+
// 注册在各跨仓仓 .git 内,但目录/meta 在主仓 .sillyspec 运行时区,同样怕并发互踩
|
|
1896
|
+
const { cleanupCrossWorktrees } = await import('./worktree-cross.js');
|
|
1897
|
+
const crossResult = cleanupCrossWorktrees({ cwd: dir, changeName: wtName, specBase: resolvePlatformSpecDir(dir, specDir), force: forceFlag });
|
|
1898
|
+
for (const cr of crossResult.results) {
|
|
1899
|
+
if (cr.result === 'cleaned') {
|
|
1900
|
+
console.log(`✅ 跨仓 worktree 已清理: repo=${cr.repoKey}(${(cr.details || []).filter(d => d.startsWith('分支')).join(';') || '详情见上'})`);
|
|
1901
|
+
} else if (cr.result === 'partial') {
|
|
1902
|
+
console.error(`🚫 跨仓 repo=${cr.repoKey} 清理受阻:${(cr.details || []).join('; ')}`);
|
|
1903
|
+
for (const r of cr.residual || []) console.error(` 残留: ${r}`);
|
|
1904
|
+
}
|
|
1905
|
+
}
|
|
1881
1906
|
if (result.result === 'cleaned' || result.result === 'force-cleaned') {
|
|
1882
1907
|
console.log(`✅ worktree 已清理: ${wtName} (mode: ${result.mode})`);
|
|
1883
1908
|
if (result.details?.length > 0) {
|
|
@@ -349,7 +349,9 @@ export class StageMachine {
|
|
|
349
349
|
command: `sillyspec run ${s} --continue --answer "用户回答"${cc}`,
|
|
350
350
|
};
|
|
351
351
|
}
|
|
352
|
-
|
|
352
|
+
// blocked(deps/review.json 门控阻断标记)也计入待办(坑 deps-gate-blocked-invisible):
|
|
353
|
+
// 漏计会让全局「下一步」建议跳过被阻断的阶段/步骤,误导 agent 去跑更后面的阶段。
|
|
354
|
+
const hasPending = sd.steps.some(st => ['pending', 'waiting', 'failed', 'blocked'].includes(st.status));
|
|
353
355
|
if (hasPending) {
|
|
354
356
|
return {
|
|
355
357
|
text: `${STAGE_LABELS[s] || s} 进行中,继续执行下一步。`,
|
package/src/run/command.js
CHANGED
|
@@ -23,7 +23,7 @@ import { basename, join, resolve, dirname } from 'node:path'
|
|
|
23
23
|
import { existsSync, readdirSync, mkdirSync, writeFileSync, readFileSync, rmSync } from 'node:fs'
|
|
24
24
|
import { randomBytes, randomUUID } from 'node:crypto'
|
|
25
25
|
import { writeAtomicSync } from '../fs-atomic.js'
|
|
26
|
-
import { resolveSpecDir, countAncestorSpecDirs, resolveChangeDir, triggerSync, getStageSteps, formatWaitOptions, checkApproval, warnApprovalUnknown, didYouMean, assertSafeChangeName, detectQuickSessionDrift, detectWorktreeSpecDrift, resolveRuntimeRoot, writePlatformPointer, checkPlatformManaged, isSelfReferentialSpecRoot, PLATFORM_MANAGED_FILENAME } from './shared.js'
|
|
26
|
+
import { resolveSpecDir, countAncestorSpecDirs, ancestorSpecDirs, resolveAncestorCeiling, resolveChangeDir, triggerSync, getStageSteps, formatWaitOptions, checkApproval, warnApprovalUnknown, didYouMean, assertSafeChangeName, detectQuickSessionDrift, detectWorktreeSpecDrift, resolveRuntimeRoot, writePlatformPointer, checkPlatformManaged, isSelfReferentialSpecRoot, PLATFORM_MANAGED_FILENAME } from './shared.js'
|
|
27
27
|
import { resolveQuickLinkedChanges } from './quick-audit.js'
|
|
28
28
|
import { outputStep, collectStageWaitHistory } from './prompt.js'
|
|
29
29
|
import { completeStep, skipStep, waitStep, continueStep } from './complete.js'
|
|
@@ -385,14 +385,26 @@ export async function runCommand(args, cwd, specDir = null, opts = {}) {
|
|
|
385
385
|
// let:下方 worktree 副本漂移守卫命中时锚回 wt.mainSpecBase(task-05/D-03)
|
|
386
386
|
let specBase = platformOpts.specRoot || join(cwd, '.sillyspec')
|
|
387
387
|
|
|
388
|
-
//
|
|
388
|
+
// 漂移阻断:cwd 祖先链 ≥2 个 .sillyspec = monorepo 多实例,当前命中的「最近」实例
|
|
389
389
|
// 可能不是用户意图的项目(如 cd 进被独立 scan 的子项目跑测试后忘回根)。
|
|
390
|
-
// 平台模式 / 显式 --spec-dir 跳过(已明确指定,无歧义)
|
|
390
|
+
// 平台模式 / 显式 --spec-dir 跳过(已明确指定,无歧义)。
|
|
391
|
+
// 当前实例 ≠ git 顶层仓库实例 → 默认拒绝(坑 monorepo-cwd-wrong-spec-instance)。
|
|
391
392
|
if (!platformOpts.specRoot && !specDir) {
|
|
392
|
-
const
|
|
393
|
-
if (
|
|
394
|
-
|
|
395
|
-
|
|
393
|
+
const ancestorSpecs = ancestorSpecDirs(cwd)
|
|
394
|
+
if (ancestorSpecs.length >= 2) {
|
|
395
|
+
// 判断当前命中的 .sillyspec 是否在 git 顶层仓库根下
|
|
396
|
+
const gitRoot = resolveAncestorCeiling(resolve(cwd))
|
|
397
|
+
const currentSpecParent = dirname(resolve(specBase))
|
|
398
|
+
const isAtGitRoot = gitRoot && resolve(currentSpecParent) === resolve(gitRoot)
|
|
399
|
+
if (!isAtGitRoot) {
|
|
400
|
+
console.error(`❌ 检测到祖先链有 ${ancestorSpecs.length} 个 .sillyspec 实例(monorepo 多实例),当前使用: ${specBase}`)
|
|
401
|
+
console.error(` 当前 cwd 不在 git 顶层仓库根下,静默绑定到子项目实例会导致进度/QUICKLOG 分裂。`)
|
|
402
|
+
console.error(` 修复:cd 回项目根目录,或用 --spec-dir <根>/.sillyspec 显式指定。`)
|
|
403
|
+
process.exit(2)
|
|
404
|
+
}
|
|
405
|
+
// 在 git 根下但有多实例(子项目也有 .sillyspec)→ 仅警告(正确实例已命中)
|
|
406
|
+
console.warn(`⚠️ 检测到祖先链有 ${ancestorSpecs.length} 个 .sillyspec 实例(monorepo 多实例),当前使用: ${specBase}`)
|
|
407
|
+
console.warn(` 当前已绑定 git 顶层仓库实例。若意图是子项目:用 --spec-dir 显式指定。`)
|
|
396
408
|
}
|
|
397
409
|
}
|
|
398
410
|
|
|
@@ -896,7 +908,7 @@ export async function runCommand(args, cwd, specDir = null, opts = {}) {
|
|
|
896
908
|
if (readonlySteps && readonlySteps.length > 0) {
|
|
897
909
|
// 已有进度(历史 seed 过)则渲染当前待办步;无进度从首步开始
|
|
898
910
|
const existingSteps = progress.stages?.[stageName]?.steps
|
|
899
|
-
const pendingIdx = existingSteps ? existingSteps.findIndex(s => s.status === 'pending' || s.status === 'in-progress') : -1
|
|
911
|
+
const pendingIdx = existingSteps ? existingSteps.findIndex(s => s.status === 'pending' || s.status === 'in-progress' || s.status === 'blocked') : -1
|
|
900
912
|
const stepIdx = pendingIdx !== -1 ? pendingIdx : 0
|
|
901
913
|
await outputStep(stageName, stepIdx, readonlySteps, cwd, readOnlyChange, progress.project || null, platformOpts, null, collectStageWaitHistory(progress, stageName))
|
|
902
914
|
}
|
|
@@ -1295,7 +1307,7 @@ function showStatus(progress, stageName, nextSuggestion = null) {
|
|
|
1295
1307
|
|
|
1296
1308
|
console.log('')
|
|
1297
1309
|
|
|
1298
|
-
const firstPending = steps.findIndex(s => s.status === 'pending' || s.status === 'in-progress')
|
|
1310
|
+
const firstPending = steps.findIndex(s => s.status === 'pending' || s.status === 'in-progress' || s.status === 'blocked')
|
|
1299
1311
|
|
|
1300
1312
|
if (progress.batchProgress) {
|
|
1301
1313
|
const bp = progress.batchProgress
|
|
@@ -1314,10 +1326,13 @@ function showStatus(progress, stageName, nextSuggestion = null) {
|
|
|
1314
1326
|
}
|
|
1315
1327
|
|
|
1316
1328
|
steps.forEach((step, i) => {
|
|
1317
|
-
const icon = step.status === 'completed' ? '✅' : step.status === 'skipped' ? '⏭️' : step.status === 'waiting' ? '⏸️' : '⬜'
|
|
1318
|
-
const isCurrent = (step.status === 'pending' || step.status === 'in-progress') && i === firstPending
|
|
1329
|
+
const icon = step.status === 'completed' ? '✅' : step.status === 'skipped' ? '⏭️' : step.status === 'waiting' ? '⏸️' : step.status === 'blocked' ? '🚫' : '⬜'
|
|
1330
|
+
const isCurrent = (step.status === 'pending' || step.status === 'in-progress' || step.status === 'blocked') && i === firstPending
|
|
1319
1331
|
const isWaiting = step.status === 'waiting'
|
|
1320
|
-
console.log(`${icon} Step ${i + 1}: ${step.name}${isCurrent ? ' ← 当前' : ''}${isWaiting ? ' [WAITING]' : ''}`)
|
|
1332
|
+
console.log(`${icon} Step ${i + 1}: ${step.name}${isCurrent ? ' ← 当前' : ''}${isWaiting ? ' [WAITING]' : ''}${step.status === 'blocked' ? ' [BLOCKED]' : ''}`)
|
|
1333
|
+
if (step.status === 'blocked') {
|
|
1334
|
+
console.log(` 此前被门控阻断——${step.blockReason || 'deps 未就绪 / review.json 不完整'};修复后重跑 --done 即恢复,无需 --reset`)
|
|
1335
|
+
}
|
|
1321
1336
|
if (isWaiting) {
|
|
1322
1337
|
if (step.waitReason) console.log(` 原因:${step.waitReason}`)
|
|
1323
1338
|
if (step.waitOptions) console.log(` 选项:${formatWaitOptions(step.waitOptions)}`)
|
|
@@ -1355,6 +1370,22 @@ async function resetStage(pm, progress, stageName, cwd, changeName, platformOpts
|
|
|
1355
1370
|
} catch (e) {
|
|
1356
1371
|
console.warn(`⚠️ reset 清理 worktree 失败(不阻断 reset): ${e.message}`)
|
|
1357
1372
|
}
|
|
1373
|
+
// 跨仓 worktree 一并清理(坑 cross-repo-no-worktree-isolation):否则下次 execute 因 meta
|
|
1374
|
+
// 存在复用带脏状态的旧跨仓 worktree
|
|
1375
|
+
try {
|
|
1376
|
+
const { cleanupCrossWorktrees } = await import('../worktree-cross.js')
|
|
1377
|
+
const cross = cleanupCrossWorktrees({
|
|
1378
|
+
cwd, changeName,
|
|
1379
|
+
specBase: platformOpts?.specRoot || join(cwd, '.sillyspec'),
|
|
1380
|
+
force: true,
|
|
1381
|
+
})
|
|
1382
|
+
for (const cr of cross.results) {
|
|
1383
|
+
if (cr.result === 'cleaned') console.log(`🧹 已清理跨仓 worktree repo=${cr.repoKey},下次 execute 将重建`)
|
|
1384
|
+
else if (cr.result === 'partial') console.warn(`⚠️ 跨仓 repo=${cr.repoKey} 清理残留: ${(cr.details || []).join('; ')} ${(cr.residual || []).join('; ')}`)
|
|
1385
|
+
}
|
|
1386
|
+
} catch (e) {
|
|
1387
|
+
console.warn(`⚠️ reset 清理跨仓 worktree 失败(不阻断 reset): ${e.message}`)
|
|
1388
|
+
}
|
|
1358
1389
|
}
|
|
1359
1390
|
const defSteps = await getStageSteps(stageName, cwd, progress, platformOpts?.specRoot || null)
|
|
1360
1391
|
progress.stages[stageName] = {
|
|
@@ -1484,7 +1515,7 @@ async function runAutoMode(pm, progress, cwd, flags, changeName, platformOpts =
|
|
|
1484
1515
|
console.log('')
|
|
1485
1516
|
|
|
1486
1517
|
const defSteps = await getAutoSteps(currentStage)
|
|
1487
|
-
const pendingIdx = progress.stages[currentStage]?.steps?.findIndex(step => step.status === 'pending' || step.status === 'in-progress') ?? -1
|
|
1518
|
+
const pendingIdx = progress.stages[currentStage]?.steps?.findIndex(step => step.status === 'pending' || step.status === 'in-progress' || step.status === 'blocked') ?? -1
|
|
1488
1519
|
if (pendingIdx === -1) {
|
|
1489
1520
|
const wsIdx = progress.stages[currentStage]?.steps?.findIndex(step => step.status === 'waiting') ?? -1
|
|
1490
1521
|
if (wsIdx !== -1) {
|
|
@@ -1536,7 +1567,7 @@ async function runAutoMode(pm, progress, cwd, flags, changeName, platformOpts =
|
|
|
1536
1567
|
return
|
|
1537
1568
|
}
|
|
1538
1569
|
|
|
1539
|
-
const nextPendingIdx = progress.stages[currentStage]?.steps?.findIndex(step => step.status === 'pending' || step.status === 'in-progress') ?? -1
|
|
1570
|
+
const nextPendingIdx = progress.stages[currentStage]?.steps?.findIndex(step => step.status === 'pending' || step.status === 'in-progress' || step.status === 'blocked') ?? -1
|
|
1540
1571
|
if (nextPendingIdx !== -1) {
|
|
1541
1572
|
const defSteps = await getAutoSteps(currentStage)
|
|
1542
1573
|
// execute 阶段启动前检查审批
|
|
@@ -1615,7 +1646,7 @@ async function runAutoMode(pm, progress, cwd, flags, changeName, platformOpts =
|
|
|
1615
1646
|
|
|
1616
1647
|
console.log(`\n${currentStage} complete. Auto advanced to ${next}.`)
|
|
1617
1648
|
const nextSteps = await getAutoSteps(next)
|
|
1618
|
-
const firstPending = progress.stages[next]?.steps?.findIndex(step => step.status === 'pending' || step.status === 'in-progress') ?? -1
|
|
1649
|
+
const firstPending = progress.stages[next]?.steps?.findIndex(step => step.status === 'pending' || step.status === 'in-progress' || step.status === 'blocked') ?? -1
|
|
1619
1650
|
if (firstPending !== -1) {
|
|
1620
1651
|
// execute 阶段启动前检查审批
|
|
1621
1652
|
if (next === 'execute' && !skipApproval) {
|
|
@@ -1385,6 +1385,26 @@ export async function handleExecuteWorktreeCleanup({ stageName, changeName, cwd
|
|
|
1385
1385
|
} catch (e) {
|
|
1386
1386
|
console.warn(`🔗 Worktree: check failed — ${e.message}`);
|
|
1387
1387
|
}
|
|
1388
|
+
// 跨仓 worktree(坑 cross-repo-no-worktree-isolation):execute 完成时不自动清——交付还在
|
|
1389
|
+
// 跨仓 worktree 分支上未回落主工作副本,与主仓「pending apply」同语义,报状态 + 下一步指引
|
|
1390
|
+
try {
|
|
1391
|
+
const { listCrossWorktreeMetas } = await import('../worktree-cross.js')
|
|
1392
|
+
const specBase = join(cwd, '.sillyspec')
|
|
1393
|
+
for (const { repoKey, meta: cm } of listCrossWorktreeMetas(specBase, changeName)) {
|
|
1394
|
+
let pending = 0
|
|
1395
|
+
try {
|
|
1396
|
+
const d = safeGit(cm.worktreePath, ['diff', '--name-only', cm.baseHash], { timeout: 30000 })
|
|
1397
|
+
const u = safeGit(cm.worktreePath, ['ls-files', '--others', '--exclude-standard'], { timeout: 30000 })
|
|
1398
|
+
pending = ((d.value || '') + '\n' + (u.value || '')).split('\n').filter(Boolean).length
|
|
1399
|
+
} catch { /* 状态探测失败按未知处理 */ }
|
|
1400
|
+
if (pending > 0) {
|
|
1401
|
+
console.log(`🔗 跨仓 worktree repo=${repoKey}: pending apply (${pending} 个未回落变更)`)
|
|
1402
|
+
console.log(` 下一步: sillyspec worktree apply ${changeName}(跨仓与主仓一并回落)`)
|
|
1403
|
+
} else {
|
|
1404
|
+
console.log(`🔗 跨仓 worktree repo=${repoKey}: 无未回落变更(apply 时将自动清理)`)
|
|
1405
|
+
}
|
|
1406
|
+
}
|
|
1407
|
+
} catch { /* 跨仓状态报告 best-effort */ }
|
|
1388
1408
|
}
|
|
1389
1409
|
return null
|
|
1390
1410
|
}
|
package/src/run/complete.js
CHANGED
|
@@ -103,7 +103,16 @@ export async function completeStep(pm, progress, stageName, cwd, outputText, inp
|
|
|
103
103
|
}
|
|
104
104
|
|
|
105
105
|
const steps = stageData.steps
|
|
106
|
-
|
|
106
|
+
// blocked(deps/review.json 门控阻断标记)也视为当前步骤(坑 deps-gate-blocked-invisible,
|
|
107
|
+
// 2026-08-27 实证):门控阻断时 enforceDepsGate/enforceReviewJsonGate 把当前步置 blocked 后
|
|
108
|
+
// exit(1),若谓词漏 blocked,被阻断步骤对 findIndex 永久隐身——重试 --done 落到其后第一个
|
|
109
|
+
// pending 步骤上(错步记账,DB 显示与事实脱节),且 blocked 步骤无任何恢复路径(状态机卡死,
|
|
110
|
+
// 只能 --reset 丢进度)。门控本身在下方会重新校验:deps 已修复 → 正常完成;未修复 → 再次
|
|
111
|
+
// 阻断同一步(幂等,不污染后续步骤)。
|
|
112
|
+
let currentIdx = steps.findIndex(s => s.status === 'pending' || s.status === 'in-progress' || s.status === 'blocked')
|
|
113
|
+
if (currentIdx !== -1 && steps[currentIdx].status === 'blocked') {
|
|
114
|
+
console.log(`⚠️ Step "${steps[currentIdx].name}" 此前被门控阻断(blocked),本次 --done 重新校验门控后继续推进。`)
|
|
115
|
+
}
|
|
107
116
|
// ── 坑1:--done --answer 解「已 waiting 的步骤」──
|
|
108
117
|
// findIndex 仅查 pending/in-progress,排除 waiting;故 --done --answer 落到已 --wait 暂停的
|
|
109
118
|
// requiresWait 步骤时会跳过它、把 --answer 静默丢弃,步骤永久卡 WAITING、末步报「等待用户输入」。
|
package/src/run/gates.js
CHANGED
|
@@ -321,10 +321,42 @@ export async function enforceDepsGate(stageName, cwd, changeName, step, steps, c
|
|
|
321
321
|
console.warn(`⚠️ worktree meta 读取失败(deps gate 将走物理目录判定): ${e.message}`)
|
|
322
322
|
}
|
|
323
323
|
const depsStatus = meta?.depsStatus
|
|
324
|
-
|
|
324
|
+
const mainOk = ['linked', 'installed', 'n/a'].includes(depsStatus)
|
|
325
|
+
if (mainOk) {
|
|
326
|
+
// ── 跨仓 worktree deps 校验(坑 cross-repo-no-worktree-isolation)──
|
|
327
|
+
// 跨仓仓供给失败同样无构建/测试能力(前端仓无 node_modules 建不了 build)。只校验已建
|
|
328
|
+
// worktree 的跨仓仓(legacy 直写模式无 meta,维持原语义,下游 Task Review 兜底)。
|
|
329
|
+
let crossFailed = []
|
|
330
|
+
try {
|
|
331
|
+
const crossSpecBase = platformOpts?.specRoot || join(cwd, '.sillyspec')
|
|
332
|
+
const { listCrossWorktreeMetas } = await import('../worktree-cross.js')
|
|
333
|
+
crossFailed = listCrossWorktreeMetas(crossSpecBase, changeName)
|
|
334
|
+
.filter(({ meta: cm }) => cm.depsStatus && !['linked', 'installed', 'n/a'].includes(cm.depsStatus))
|
|
335
|
+
} catch (e) {
|
|
336
|
+
console.warn(`⚠️ 跨仓 worktree deps 校验跳过(${e.message})`)
|
|
337
|
+
}
|
|
338
|
+
if (crossFailed.length === 0) return true
|
|
339
|
+
if (steps && steps[currentIdx]) {
|
|
340
|
+
steps[currentIdx].status = 'blocked'
|
|
341
|
+
steps[currentIdx].blockReason = `跨仓 deps 门控阻断(${crossFailed.map(f => f.repoKey).join(', ')}):修复后重试 --done`
|
|
342
|
+
}
|
|
343
|
+
console.error('❌ ── deps 门控阻断(本次 --done 未完成,进度未推进)──')
|
|
344
|
+
for (const { repoKey, meta: cm } of crossFailed) {
|
|
345
|
+
console.error(` 跨仓 repo ${repoKey} 依赖未就绪(depsStatus=${cm.depsStatus || 'unknown'})——workdir: ${cm.worktreePath}`)
|
|
346
|
+
if (cm.depsError) console.error(` 上次供给错误:${cm.depsError}`)
|
|
347
|
+
}
|
|
348
|
+
console.error(' 修复:进上述跨仓 worktree 手动安装依赖(用该仓的包管理器),或重跑 sillyspec run execute 让 CLI 重试供给后 --done。')
|
|
349
|
+
if (persist) { try { await persist() } catch { /* 落盘失败不吞阻断语义 */ } }
|
|
350
|
+
process.exit(1)
|
|
351
|
+
}
|
|
325
352
|
const changeDir = changeName ? join(specBase, 'changes', changeName) : null
|
|
326
353
|
if (isCurrentWaveAllNoDepsVerify(step?.name, changeDir)) return true
|
|
327
|
-
if (steps && steps[currentIdx])
|
|
354
|
+
if (steps && steps[currentIdx]) {
|
|
355
|
+
steps[currentIdx].status = 'blocked'
|
|
356
|
+
// 阻断原因持久化(坑 deps-gate-blocked-invisible):--status/诊断时可见,不依赖阻断当刻的
|
|
357
|
+
// stderr(跨进程即失忆)。completed 时由 completeStep 主流程覆盖整个步骤行,残留字段不外泄。
|
|
358
|
+
steps[currentIdx].blockReason = `deps 门控阻断(depsStatus=${depsStatus || 'unknown'}):worktree doctor --fix 修复后重试 --done`
|
|
359
|
+
}
|
|
328
360
|
// ── 诊断分支(Phase 2,G2/R3 修正:判定基于物理目录而非 !meta)──
|
|
329
361
|
// getMeta 对"目录不存在"与"meta 损坏"都返回 null,后者会误判终态 → 用物理目录存在性判定。
|
|
330
362
|
let worktreeGone = true
|
|
@@ -411,7 +443,10 @@ export async function enforceReviewJsonGate(stageName, cwd, changeName, step, st
|
|
|
411
443
|
}
|
|
412
444
|
const result = validateCheckedTaskReviews({ planContent, runtimeRoot, executeRunId })
|
|
413
445
|
if (result.ok) return true
|
|
414
|
-
if (steps && steps[currentIdx])
|
|
446
|
+
if (steps && steps[currentIdx]) {
|
|
447
|
+
steps[currentIdx].status = 'blocked'
|
|
448
|
+
steps[currentIdx].blockReason = 'review.json 字段校验阻断:补齐后重试 --done(backfill-reviews --adopt 可代填 mechanics)'
|
|
449
|
+
}
|
|
415
450
|
console.error('❌ ── review.json 字段校验阻断(本次 --done 未完成,进度未推进)──')
|
|
416
451
|
console.error(' 已勾选 [x] 的 task review.json 不完整(铁律:勾 checkbox 前必须先写完整 review.json):')
|
|
417
452
|
for (const f of result.failures) {
|
|
@@ -22,6 +22,8 @@
|
|
|
22
22
|
* 不涉及生命周期契约(design §7.5):纯内存对象,不跨进程、不持久化、不入库。
|
|
23
23
|
*/
|
|
24
24
|
|
|
25
|
+
import { existsSync, readFileSync } from 'fs';
|
|
26
|
+
import { join } from 'path';
|
|
25
27
|
import { git, gitQuiet } from '../git-helper.js';
|
|
26
28
|
|
|
27
29
|
/**
|
|
@@ -133,7 +135,9 @@ export class MultiRepoContext {
|
|
|
133
135
|
}
|
|
134
136
|
|
|
135
137
|
/**
|
|
136
|
-
* 跨仓 RepoEntry
|
|
138
|
+
* 跨仓 RepoEntry:有 worktree meta(worktree-cross.js 创建)→ gitDir=worktreePath=worktree,
|
|
139
|
+
* base 锚 meta.baseHash(与主仓同构,坑 cross-repo-no-worktree-isolation);无 meta(legacy
|
|
140
|
+
* 直写主干模式)→ gitDir=projectRoot=worktreePath=跨仓仓根,base 靠 taskBaseCommit 锡点。
|
|
137
141
|
* 构造时 `git -C <crossRepo> rev-parse HEAD` 必须 succeed(约束② fail-closed),
|
|
138
142
|
* 路径不存在/非 git 仓 → throw 阻断 execute。
|
|
139
143
|
* @param {string} repoKey
|
|
@@ -148,14 +152,26 @@ export class MultiRepoContext {
|
|
|
148
152
|
`请补全路径(约束② fail-closed)。`
|
|
149
153
|
);
|
|
150
154
|
}
|
|
155
|
+
// worktree 模式检测:meta 在 = worktree-cross.js 已建隔离(路径公式与其 crossWorktreePath
|
|
156
|
+
// 保持同步;execute 启动时创建,晚于本构造的调用会命中)。
|
|
157
|
+
const specBase = (this.platformOpts && this.platformOpts.specRoot) || join(this.cwd, '.sillyspec');
|
|
158
|
+
let crossMeta = null;
|
|
159
|
+
try {
|
|
160
|
+
// 同步读(构造函数是同步的,不 import worktree-cross 避免 shared↔multi-repo-context 循环)
|
|
161
|
+
const metaPath = join(specBase, '.runtime', 'worktrees', `${this.changeName}--${repoKey}`, 'meta.json');
|
|
162
|
+
if (existsSync(metaPath)) crossMeta = JSON.parse(readFileSync(metaPath, 'utf8'));
|
|
163
|
+
} catch { crossMeta = null }
|
|
164
|
+
const inWorktree = !!(crossMeta && crossMeta.isCross && crossMeta.worktreePath && existsSync(crossMeta.worktreePath));
|
|
165
|
+
const workRoot = inWorktree ? crossMeta.worktreePath : crossRepoPath;
|
|
166
|
+
|
|
151
167
|
// 约束②:跨仓 git 必须可达。失败 = 路径不存在 / 非 git 仓 / git 异常,一律阻断。
|
|
152
168
|
// 不沿用主仓 verifyReviewGitEvidence unavailable 降级(design §9 兼容策略表)。
|
|
153
169
|
let headVerify;
|
|
154
170
|
try {
|
|
155
|
-
headVerify = git(
|
|
171
|
+
headVerify = git(workRoot, ['rev-parse', 'HEAD']);
|
|
156
172
|
} catch (e) {
|
|
157
173
|
throw new Error(
|
|
158
|
-
`MultiRepoContext: 跨仓 repo "${repoKey}" git 不可达(路径:${crossRepoPath})。` +
|
|
174
|
+
`MultiRepoContext: 跨仓 repo "${repoKey}" git 不可达(路径:${workRoot}${inWorktree ? `(worktree,跨仓根 ${crossRepoPath})` : ''})。` +
|
|
159
175
|
`rev-parse HEAD 失败:${e.message}。` +
|
|
160
176
|
`请检查 local.yaml repos: 段路径是否正确、目录是否存在且为 git 仓(约束② fail-closed,配置错误不降级)。`
|
|
161
177
|
);
|
|
@@ -164,17 +180,39 @@ export class MultiRepoContext {
|
|
|
164
180
|
// git() 返回空串属异常态(无 commit 的空仓 rev-parse HEAD 会非零退出,已被 catch 兜底),
|
|
165
181
|
// 此处仅作防御性双保险,触发同样走 fail-closed。
|
|
166
182
|
throw new Error(
|
|
167
|
-
`MultiRepoContext: 跨仓 repo "${repoKey}"(${
|
|
183
|
+
`MultiRepoContext: 跨仓 repo "${repoKey}"(${workRoot})rev-parse HEAD 返回空——` +
|
|
168
184
|
`疑似空 git 仓(无 commit)。跨仓 task 需仓库已有至少一个 commit(约束② fail-closed)。`
|
|
169
185
|
);
|
|
170
186
|
}
|
|
171
187
|
|
|
188
|
+
if (inWorktree) {
|
|
189
|
+
// worktree 模式:子代理在跨仓 worktree 分支上改+commit,apply 阶段由 CLI patch 回跨仓
|
|
190
|
+
// 主工作区。base 锚 meta.baseHash(创建时快照),head 实时取 worktree HEAD(子代理
|
|
191
|
+
// commit 推进后反映最新);baseCommitHint 供派发侧优先取(HEAD 会推进,不能当 base)。
|
|
192
|
+
return {
|
|
193
|
+
repoKey,
|
|
194
|
+
gitDir: workRoot,
|
|
195
|
+
worktreePath: workRoot,
|
|
196
|
+
projectRoot: crossRepoPath,
|
|
197
|
+
isMain: false,
|
|
198
|
+
isWorktree: true,
|
|
199
|
+
baseCommitHint: crossMeta.baseHash || null,
|
|
200
|
+
resolveHead: () => git(workRoot, ['rev-parse', 'HEAD']),
|
|
201
|
+
resolveBase: (taskBaseCommit) => crossMeta.baseHash || taskBaseCommit || (() => {
|
|
202
|
+
throw new Error(
|
|
203
|
+
`MultiRepoContext: 跨仓 repo "${repoKey}"(worktree 模式)meta.baseHash 缺失且未传 taskBaseCommit。`
|
|
204
|
+
);
|
|
205
|
+
})(),
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
|
|
172
209
|
return {
|
|
173
210
|
repoKey,
|
|
174
211
|
gitDir: crossRepoPath,
|
|
175
212
|
worktreePath: crossRepoPath,
|
|
176
213
|
projectRoot: crossRepoPath,
|
|
177
214
|
isMain: false,
|
|
215
|
+
isWorktree: false,
|
|
178
216
|
// 约束①:跨仓 HEAD 实时取,不缓存(task 推进后过期,每次 resolveHead 反映最新 HEAD)。
|
|
179
217
|
resolveHead: () => git(crossRepoPath, ['rev-parse', 'HEAD']),
|
|
180
218
|
// 约束①:跨仓 base = task 卡 base_commit 锡点(必传)。跨仓仓无 meta.json,
|
package/src/run/shared.js
CHANGED
|
@@ -139,7 +139,7 @@ export function resolveSpecDir(cwd, opts = {}) {
|
|
|
139
139
|
// 只缓存"git root 已找到"的结果——非 git 仓库不缓存,防测试流程中途 git init 后读到陈旧 null。
|
|
140
140
|
const _ancestorCeilingCache = new Map()
|
|
141
141
|
|
|
142
|
-
function resolveAncestorCeiling(resolved) {
|
|
142
|
+
export function resolveAncestorCeiling(resolved) {
|
|
143
143
|
if (_ancestorCeilingCache.has(resolved)) return _ancestorCeilingCache.get(resolved)
|
|
144
144
|
// 一次 spawn 拿两值(rev-parse 按参数序输出,每值一行)
|
|
145
145
|
const out = safeGit(resolved, ['rev-parse', '--show-toplevel', '--git-common-dir']).value
|
package/src/run/stage.js
CHANGED
|
@@ -93,6 +93,26 @@ export async function runStage(pm, progress, stageName, cwd, changeName, skipApp
|
|
|
93
93
|
const execSpecBase = platformOpts?.specRoot || join(cwd, '.sillyspec')
|
|
94
94
|
await ensureDepsFreshness(cwd, effectiveChange, execSpecBase, existingMeta)
|
|
95
95
|
}
|
|
96
|
+
|
|
97
|
+
// ── 跨仓仓 worktree 隔离(坑 cross-repo-no-worktree-isolation,2026-08-27 用户实证)──
|
|
98
|
+
// 主仓 worktree 就绪后,为 plan 声明的每个跨仓仓建同构 worktree(幂等)。此前跨仓 task 直写
|
|
99
|
+
// 跨仓主工作副本,与用户在途改动混流、无 base 锚、无回滚面。fail-closed:建不起来不开工
|
|
100
|
+
//(对齐主仓 create 语义)。
|
|
101
|
+
try {
|
|
102
|
+
const { ensureCrossWorktrees } = await import('../worktree-cross.js')
|
|
103
|
+
const execSpecBase = platformOpts?.specRoot || join(cwd, '.sillyspec')
|
|
104
|
+
const cross = ensureCrossWorktrees({ cwd, changeName: effectiveChange, specBase: execSpecBase })
|
|
105
|
+
for (const c of cross.created) {
|
|
106
|
+
console.log(`🔗 跨仓 worktree 已创建: repo=${c.repoKey} → ${c.worktreePath}`)
|
|
107
|
+
}
|
|
108
|
+
for (const r of cross.reused) {
|
|
109
|
+
console.log(`🔗 跨仓 worktree 已存在: repo=${r.repoKey} → ${r.worktreePath}`)
|
|
110
|
+
}
|
|
111
|
+
} catch (e) {
|
|
112
|
+
console.error(`❌ 跨仓 worktree 创建失败: ${e.message}`)
|
|
113
|
+
console.error(` 修复后重跑本命令(已建成功的仓会幂等复用)。`)
|
|
114
|
+
process.exit(1)
|
|
115
|
+
}
|
|
96
116
|
}
|
|
97
117
|
|
|
98
118
|
// ── execute 阶段启动时固定 executeRunId(绑定变更名,避免跨变更复用) ──
|
|
@@ -228,8 +248,14 @@ export async function runStage(pm, progress, stageName, cwd, changeName, skipApp
|
|
|
228
248
|
|
|
229
249
|
let currentIdx = steps.findIndex(s => s.status !== 'completed' && s.status !== 'skipped')
|
|
230
250
|
|
|
231
|
-
// stale 步骤视为可执行(等同于 pending
|
|
232
|
-
|
|
251
|
+
// stale/blocked 步骤视为可执行(等同于 pending):
|
|
252
|
+
// stale = reopen 后未重做(原语义);blocked = 门控(deps/review.json)阻断标记。
|
|
253
|
+
// blocked 必须一并转换(坑 deps-gate-blocked-invisible 的路径①失效面):reopen 后首个未完成
|
|
254
|
+
// 步骤若是 blocked(早前门控阻断污染),只转 stale 会让 completeStep 的 findIndex(pending)
|
|
255
|
+
// 跳过它、--done 永远撞 stale 门控报错——「逐个真实执行」指引失效。门控在 --done 时会
|
|
256
|
+
// 重新校验,此处拉回 pending 不会绕过任何校验。
|
|
257
|
+
if (currentIdx !== -1 && (steps[currentIdx].status === 'stale' || steps[currentIdx].status === 'blocked')) {
|
|
258
|
+
console.log(`⚠️ Step "${steps[currentIdx].name}" 处于 ${steps[currentIdx].status},已拉回待执行。`)
|
|
233
259
|
steps[currentIdx].status = 'pending'
|
|
234
260
|
pm._write(cwd, progress, changeName)
|
|
235
261
|
triggerSync(cwd, changeName, platformOpts)
|
package/src/stage-contract.js
CHANGED
|
@@ -459,13 +459,26 @@ function validatePlanOutputs(cwd, changeName, context = {}) {
|
|
|
459
459
|
* 历史教训:原正则锚定确切「## 结论」,用户写「## 验收结论:✅ PASS」不被识别。
|
|
460
460
|
*/
|
|
461
461
|
function extractVerifyConclusion(verify) {
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
const
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
462
|
+
// 遍历所有含关键词的二级标题,取其 400 字符窗口内含 PASS/FAIL 的那个(坑
|
|
463
|
+
// verify-conclusion-heading-hijack:旧逻辑取首个匹配,"## 测试结果"等普通标题
|
|
464
|
+
// 排在真结论前时劫持识别)。优先精确匹配「## 结论」类窄标题。
|
|
465
|
+
const headingRe = /^##\s[^\n]*(?:结论|conclusion|result|结果)/gim
|
|
466
|
+
let best = null
|
|
467
|
+
let bestPriority = -1
|
|
468
|
+
for (const headingMatch of verify.matchAll(headingRe)) {
|
|
469
|
+
const start = headingMatch.index
|
|
470
|
+
const slice = verify.slice(start, start + 400)
|
|
471
|
+
const kw = slice.match(/\b(PASS(?:\s+WITH\s+NOTES)?|FAIL)\b/i)
|
|
472
|
+
if (!kw) continue
|
|
473
|
+
// 优先级:精确含「结论/conclusion」> 宽泛含「result/结果」
|
|
474
|
+
const text = headingMatch[0].toLowerCase()
|
|
475
|
+
const priority = (/结论|conclusion/i.test(text)) ? 1 : 0
|
|
476
|
+
if (priority > bestPriority || (priority === bestPriority && !best)) {
|
|
477
|
+
best = kw[1].toUpperCase().replace(/\s+/g, ' ')
|
|
478
|
+
bestPriority = priority
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
return best || ''
|
|
469
482
|
}
|
|
470
483
|
|
|
471
484
|
function validateVerifyOutputs(cwd, changeName, context = {}) {
|
package/src/stages/execute.js
CHANGED
|
@@ -1006,14 +1006,19 @@ ${indexLines}
|
|
|
1006
1006
|
return { taskNum, repo, workdir }
|
|
1007
1007
|
})
|
|
1008
1008
|
|
|
1009
|
-
// D-010 跨仓 task base
|
|
1010
|
-
// 仅对跨仓 task(repo≠'main')落盘,主仓 task 锚 meta.baseHash(MultiRepoContext
|
|
1009
|
+
// D-010 跨仓 task base 锡点:派发前落 task 卡 base_commit
|
|
1010
|
+
// 仅对跨仓 task(repo≠'main')落盘,主仓 task 锚 meta.baseHash(MultiRepoContext 已处理)。
|
|
1011
|
+
// worktree 模式(跨仓 worktree 隔离,坑 cross-repo-no-worktree-isolation)优先 entry.baseCommitHint
|
|
1012
|
+
//(= meta.baseHash 创建时快照)——worktree HEAD 会随子代理 commit 推进,rev-parse HEAD 当 base
|
|
1013
|
+
// 会把 Wave 内前序 task 的交付错算进后续 task 的 base;legacy 直写模式沿用跨仓根实时 HEAD。
|
|
1011
1014
|
const baseCommitCache = new Map() // 同仓多 task 只 spawn 一次
|
|
1012
1015
|
for (const item of perTaskWorkdirs) {
|
|
1013
1016
|
if (item.repo === 'main') continue
|
|
1014
1017
|
const entry = ctx.resolve(item.repo)
|
|
1015
1018
|
if (!entry) continue
|
|
1016
|
-
if (!baseCommitCache.has(entry.gitDir))
|
|
1019
|
+
if (!baseCommitCache.has(entry.gitDir)) {
|
|
1020
|
+
baseCommitCache.set(entry.gitDir, entry.baseCommitHint || gitQuiet(entry.gitDir, ['rev-parse', 'HEAD']))
|
|
1021
|
+
}
|
|
1017
1022
|
const baseCommit = baseCommitCache.get(entry.gitDir)
|
|
1018
1023
|
if (!baseCommit) continue // git 不可达已由 MultiRepoContext 构造时 fail-closed 拦截,此处置防御
|
|
1019
1024
|
const taskFile = changeDir
|
|
@@ -1023,28 +1028,43 @@ ${indexLines}
|
|
|
1023
1028
|
baseCommitWrites.push({ taskNum: item.taskNum, repo: item.repo, taskFile, written, baseCommit })
|
|
1024
1029
|
}
|
|
1025
1030
|
|
|
1026
|
-
// 跨仓 task commit 指引 + head_commit 落盘指引(D-010
|
|
1031
|
+
// 跨仓 task commit 指引 + head_commit 落盘指引(D-010 回收时机)。
|
|
1032
|
+
// worktree 模式(跨仓 worktree 隔离):子代理在跨仓 worktree 分支改+commit,apply 阶段 CLI
|
|
1033
|
+
// 统一 patch 回跨仓主工作区(与主仓同构);legacy 直写模式保留原文案(commit 直落跨仓主干)。
|
|
1034
|
+
const crossWorktreeItems = perTaskWorkdirs.filter(i => {
|
|
1035
|
+
const e = i.repo !== 'main' ? ctx.resolve(i.repo) : null
|
|
1036
|
+
return e && e.isWorktree
|
|
1037
|
+
})
|
|
1038
|
+
const crossLegacyItems = perTaskWorkdirs.filter(i => i.repo !== 'main' && !crossWorktreeItems.some(w => w.taskNum === i.taskNum))
|
|
1027
1039
|
const crossLines = perTaskWorkdirs
|
|
1028
1040
|
.filter(i => i.repo !== 'main')
|
|
1029
|
-
.map(i =>
|
|
1041
|
+
.map(i => {
|
|
1042
|
+
const e = ctx.resolve(i.repo)
|
|
1043
|
+
return `- task-${i.taskNum} → repo \`${i.repo}\`,workdir=\`${i.workdir}\`${e && e.isWorktree ? '(worktree 隔离)' : ''}`
|
|
1044
|
+
})
|
|
1030
1045
|
.join('\n')
|
|
1031
|
-
|
|
1032
|
-
### 跨仓 task 派发与双锡点
|
|
1033
|
-
|
|
1034
|
-
本 Wave 含跨仓 task(直接改跨仓仓主干,**不经主仓 worktree**):
|
|
1046
|
+
const crossWorktreeSection = crossWorktreeItems.length > 0 ? `
|
|
1035
1047
|
|
|
1036
|
-
|
|
1048
|
+
**跨仓 worktree task(repo 已建 worktree 隔离)——像主仓 task 一样工作:**
|
|
1049
|
+
- 子代理在 workdir(跨仓 worktree)内改+commit(git add + git commit 到 worktree 当前分支),**不要碰跨仓主工作副本**。
|
|
1050
|
+
- apply 阶段 CLI 统一把各跨仓 worktree 的交付 patch 回对应跨仓主工作区(勿手工 apply)。
|
|
1051
|
+
- base 已锚 worktree meta.baseHash(创建时快照),review 的 base/head 由 CLI 按 task 卡锡点解析。` : ''
|
|
1052
|
+
const crossLegacySection = crossLegacyItems.length > 0 ? `
|
|
1037
1053
|
|
|
1038
1054
|
**派发跨仓 task 前(base 锡点,CLI 已在 prompt 构造时落盘):**
|
|
1039
1055
|
- 跨仓 task 卡 frontmatter 的 \`base_commit\` 已由 CLI 实时 \`git -C <跨仓仓根> rev-parse HEAD\` 锁定(base 锡点,约束①)。子代理在此 HEAD 上改+commit,不受同 Wave 其他 task 推进 HEAD 影响。
|
|
1040
1056
|
|
|
1057
|
+
**跨仓 task 子代理 prompt 必须注入(legacy 直写模式):**
|
|
1058
|
+
> 该 task 改的是 \`<repo>\` 仓,workdir=\`<跨仓仓根>\`。**直接在该仓主干工作区改+commit(git add + git commit 到该仓主干),不经主仓 worktree、不建分支。** commit 到该仓主干即落盘,apply 阶段对跨仓 task 为 no-op(design §5.4 G1)。
|
|
1059
|
+
> task 卡 allowed_paths 是相对**该仓根**的路径(如 \`src/routes/x.js\`)——不带仓库名前缀(\`<repo>/src/...\` ❌)、不是绝对路径;在 workdir 下直接按相对路径定位文件。` : ''
|
|
1060
|
+
crossRepoCommitSection = `
|
|
1061
|
+
### 跨仓 task 派发与双锡点
|
|
1062
|
+
|
|
1063
|
+
${crossLines}${crossWorktreeSection}${crossLegacySection}
|
|
1064
|
+
|
|
1041
1065
|
**回收跨仓 task(head 锡点,CLI 自动落盘,勿手写):**
|
|
1042
|
-
- 子代理完成 commit 后,正常写 review.json(verdict/notes)并勾选 checkbox 即可。execute \`--done\` 时 CLI
|
|
1066
|
+
- 子代理完成 commit 后,正常写 review.json(verdict/notes)并勾选 checkbox 即可。execute \`--done\` 时 CLI 自动对跨仓仓(worktree 模式=该仓 worktree,legacy=仓根)\`git rev-parse HEAD\` 写入该 task 卡 \`head_commit:\`(幂等,已存在不覆盖——你若手写了精确锚点则以你的为准)。
|
|
1043
1067
|
- review.json 的 mechanics 字段(\`base\`/\`head\`/\`changedFiles\`)无需手算:\`base\` 取 task 卡 \`base_commit\`、\`head\` 取 task 卡 \`head_commit\`,写完跑 \`sillyspec backfill-reviews --change <变更名> --adopt\` 一键代填(verdict 保留)。
|
|
1044
|
-
|
|
1045
|
-
**跨仓 task 子代理 prompt 必须注入:**
|
|
1046
|
-
> 该 task 改的是 \`<repo>\` 仓,workdir=\`<跨仓仓根>\`。**直接在该仓主干工作区改+commit(git add + git commit 到该仓主干),不经主仓 worktree、不建分支。** commit 到该仓主干即落盘,apply 阶段对跨仓 task 为 no-op(design §5.4 G1)。
|
|
1047
|
-
> task 卡 allowed_paths 是相对**该仓根**的路径(如 \`src/routes/x.js\`)——不带仓库名前缀(\`<repo>/src/...\` ❌)、不是绝对路径;在 workdir 下直接按相对路径定位文件。
|
|
1048
1068
|
`
|
|
1049
1069
|
}
|
|
1050
1070
|
|