sillyspec 3.32.2 → 3.32.4
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/.claude/skills/sillyspec-auto/SKILL.md +1 -1
- package/.claude/skills/sillyspec-brainstorm/SKILL.md +3 -2
- package/.claude/skills/sillyspec-explore/SKILL.md +0 -2
- package/.claude/skills/sillyspec-flow/SKILL.md +1 -0
- package/README.md +4 -4
- package/SKILL.md +2 -4
- package/package.json +1 -1
- package/src/command-cards.js +1 -1
- package/src/cross-repo-reconcile.js +33 -0
- package/src/flow-parity.js +46 -35
- package/src/flow-review.js +12 -2
- package/src/flow.js +23 -8
- package/src/friction-tally.js +2 -1
- package/src/git-helper.js +23 -1
- package/src/index.js +54 -13
- package/src/init.js +58 -61
- package/src/module-impact.js +3 -1
- package/src/progress/stage-machine.js +8 -1
- package/src/run/code-face-key.js +84 -0
- package/src/run/complete.js +16 -7
- package/src/run/gate-snapshot.js +19 -0
- package/src/run/gates.js +109 -67
- package/src/run/green-cache.js +11 -19
- package/src/run/shared.js +75 -0
- package/src/run/verify-quality-scan.js +88 -45
- package/src/scope-audit.js +50 -24
- package/src/spec-sync.js +98 -20
- package/src/stages/brainstorm.js +2 -1
- package/src/stages/cmd-existence.js +5 -1
- package/src/test-bindings.js +2 -0
- package/src/verify-postcheck.js +141 -18
- package/src/verify-probes.js +141 -0
- package/src/worktree.js +43 -9
- package/src/wt-commit.js +82 -3
- package/.claude/skills/sillyspec-quick/SKILL.md +0 -116
- package/assets/command-cards/run-quick.md +0 -20
package/src/verify-probes.js
CHANGED
|
@@ -2180,6 +2180,9 @@ export function runVerifyProbes({ cwd, changeName, specDir = null }) {
|
|
|
2180
2180
|
task: fmId || f.replace(/\.md$/, ''),
|
|
2181
2181
|
acceptance: parsed.acceptance,
|
|
2182
2182
|
fmError: parsed.status === 'invalid-yaml' ? parsed.error : null,
|
|
2183
|
+
// FR-06(2026-10-09-verify-reuse-friction):行 repo 归属写侧透传(跨仓卡 tests
|
|
2184
|
+
// 仓根相对,读侧按行根解析)
|
|
2185
|
+
repo: parseRepo(raw) || null,
|
|
2183
2186
|
raw,
|
|
2184
2187
|
})
|
|
2185
2188
|
}
|
|
@@ -2280,6 +2283,9 @@ export function runVerifyProbes({ cwd, changeName, specDir = null }) {
|
|
|
2280
2283
|
discovery: 'machine',
|
|
2281
2284
|
confirmed_by: null,
|
|
2282
2285
|
source_change: changeName,
|
|
2286
|
+
// FR-06(2026-10-09-verify-reuse-friction):跨仓卡的 tests 是仓根相对——
|
|
2287
|
+
// 行携 repo 归属供读侧按行根解析(缺省不落键=主仓,D-005@v1)。
|
|
2288
|
+
...(t.repo && t.repo !== 'main' ? { repo: t.repo } : {}),
|
|
2283
2289
|
})
|
|
2284
2290
|
})
|
|
2285
2291
|
}
|
|
@@ -3854,3 +3860,138 @@ export function refreshProbeSections(existingText, freshReportText) {
|
|
|
3854
3860
|
}
|
|
3855
3861
|
return { text: out.join('\n'), replaced, kept, appended, carriedRows }
|
|
3856
3862
|
}
|
|
3863
|
+
|
|
3864
|
+
// ── --force 保人工面(2026-10-09-verify-papercuts ②)──
|
|
3865
|
+
// postmortem(2026-10-09-verify-reuse-friction 收口实证):--force 全骨架重置虽有自动备份,
|
|
3866
|
+
// 但结论槽/移交项表/探针矩阵已填判定/决策矩阵已填格仍全部被 wipe——重填整套人工面是当日
|
|
3867
|
+
// 最大单点浪费(备份在 .runtime 里,找回-回填的来回远比携载贵)。本组合器把 --force 语义
|
|
3868
|
+
// 从「全量重置 + 备份找回」升级为「机器段全量重刷 + 人工面按段携载」:
|
|
3869
|
+
// - 纯人工段(结论/移交项/证据账/集成回执/任务完成度/设计一致性/独立复核/风险等级):
|
|
3870
|
+
// 旧段已填(无 <待填/<TODO 占位)→ 原样保留;未填 → 用新骨架(占位换新占位,无损失);
|
|
3871
|
+
// - 矩阵段(决策追踪矩阵/接口验证覆盖矩阵):表格行按首列键携载(mergeProbeSection 同
|
|
3872
|
+
// 款语义——已填 Evidence/判定行不丢,新增行从新骨架进场);
|
|
3873
|
+
// - 探针容器段(## 探针结果):逐探针号 merge(已填表格行携载,机器结论行刷新);
|
|
3874
|
+
// - 其余机器段:新骨架;旧文独有的自定义段:追加保留(不销毁用户内容)。
|
|
3875
|
+
// 安全方向与 refreshProbeSections 一致:宁可少刷新不误删;stale 由 gate 的
|
|
3876
|
+
// checkProbeConsistency 独立把关。备份照旧保留(兜底通道不撤)。
|
|
3877
|
+
|
|
3878
|
+
/** 顶层段切分(^## 标题行起,至下一 ^## 或 EOF;# 级更细的 #### 归属其容器段) */
|
|
3879
|
+
function splitTopSections(text) {
|
|
3880
|
+
const lines = String(text ?? '').split('\n')
|
|
3881
|
+
const sections = []
|
|
3882
|
+
let cur = null
|
|
3883
|
+
for (let i = 0; i < lines.length; i++) {
|
|
3884
|
+
if (/^## /.test(lines[i])) {
|
|
3885
|
+
if (cur) cur.endLine = i
|
|
3886
|
+
cur = { headingLine: lines[i], startLine: i, endLine: lines.length }
|
|
3887
|
+
sections.push(cur)
|
|
3888
|
+
}
|
|
3889
|
+
}
|
|
3890
|
+
return { lines, sections }
|
|
3891
|
+
}
|
|
3892
|
+
|
|
3893
|
+
/** 段身份键:剥 [层:…] 尾注与(…)括注——同族段跨渲染措辞漂移仍可对上 */
|
|
3894
|
+
function topSectionKey(headingLine) {
|
|
3895
|
+
return headingLine
|
|
3896
|
+
.replace(/^##\s*/, '')
|
|
3897
|
+
.replace(/\s*\[.*\]\s*$/, '')
|
|
3898
|
+
.replace(/(.*$/, '')
|
|
3899
|
+
.trim()
|
|
3900
|
+
}
|
|
3901
|
+
|
|
3902
|
+
const HUMAN_SECTION_KEYS = ['结论', '移交项', '证据账', '集成验证回执', '任务完成度', '设计一致性', '独立复核', '变更风险等级', '技术债务', 'Runtime Evidence', '代码审查']
|
|
3903
|
+
const MATRIX_SECTION_KEYS = ['决策追踪矩阵', '接口验证覆盖矩阵']
|
|
3904
|
+
const PROBE_CONTAINER_KEY = '探针结果'
|
|
3905
|
+
|
|
3906
|
+
/** 探针容器段合并:新骨架容器为基,逐探针号对旧容器做已填行携载;旧独有探针号追加 */
|
|
3907
|
+
function mergeProbeContainer(oldContainerLines, freshContainerLines) {
|
|
3908
|
+
const oldParsed = splitProbeSectionRanges(oldContainerLines.join('\n'))
|
|
3909
|
+
const freshParsed = splitProbeSectionRanges(freshContainerLines.join('\n'))
|
|
3910
|
+
const out = [...freshParsed.lines]
|
|
3911
|
+
let carriedRows = 0
|
|
3912
|
+
const oldByNum = new Map(oldParsed.sections.map(s => [s.num, s]))
|
|
3913
|
+
for (let idx = freshParsed.sections.length - 1; idx >= 0; idx--) {
|
|
3914
|
+
const fs = freshParsed.sections[idx]
|
|
3915
|
+
const os = oldByNum.get(fs.num)
|
|
3916
|
+
if (!os) continue
|
|
3917
|
+
const merged = mergeProbeSection(
|
|
3918
|
+
oldParsed.lines.slice(os.startLine, os.endLine),
|
|
3919
|
+
freshParsed.lines.slice(fs.startLine, fs.endLine),
|
|
3920
|
+
)
|
|
3921
|
+
// 倒序替换(审查 P2 修正,2026-10-09-verify-papercuts):mergeProbeSection 对含 stale
|
|
3922
|
+
// 键的段会在段尾追加(行数 > freshSeg),正序 splice 会使后续探针段 startLine 错位——
|
|
3923
|
+
// 从后往前替换则前面行号不受影响,任一段行数膨胀都安全。
|
|
3924
|
+
carriedRows += merged.carried
|
|
3925
|
+
// 用 merged 文本替换 fresh 容器中该探针段(逐段行数已知,从后往前替换保行号稳定)
|
|
3926
|
+
const freshSeg = freshParsed.lines.slice(fs.startLine, fs.endLine)
|
|
3927
|
+
out.splice(fs.startLine, freshSeg.length, ...merged.text.split('\n'))
|
|
3928
|
+
}
|
|
3929
|
+
const freshNums = new Set(freshParsed.sections.map(s => s.num))
|
|
3930
|
+
for (const os of oldParsed.sections) {
|
|
3931
|
+
if (!freshNums.has(os.num)) {
|
|
3932
|
+
out.push('', ...oldParsed.lines.slice(os.startLine, os.endLine))
|
|
3933
|
+
}
|
|
3934
|
+
}
|
|
3935
|
+
return { text: out.join('\n'), carriedRows }
|
|
3936
|
+
}
|
|
3937
|
+
|
|
3938
|
+
/**
|
|
3939
|
+
* --force 应用骨架:机器段全量重刷 + 人工面携载(纯函数,导出供回归直测)。
|
|
3940
|
+
* @param {string} existingText 现文(含已填人工面)
|
|
3941
|
+
* @param {string} skeletonText generateVerifyResultSkeleton 新骨架
|
|
3942
|
+
* @returns {{ text: string, carriedSections: string[], carriedRows: number }}
|
|
3943
|
+
*/
|
|
3944
|
+
export function applySkeletonPreservingHuman(existingText, skeletonText) {
|
|
3945
|
+
const old = splitTopSections(existingText)
|
|
3946
|
+
const fresh = splitTopSections(skeletonText)
|
|
3947
|
+
const oldByKey = new Map()
|
|
3948
|
+
for (const s of old.sections) {
|
|
3949
|
+
const k = topSectionKey(s.headingLine)
|
|
3950
|
+
if (!oldByKey.has(k)) oldByKey.set(k, s) // 同键段取首个(重复标题形态保首段)
|
|
3951
|
+
}
|
|
3952
|
+
const carriedSections = []
|
|
3953
|
+
let carriedRows = 0
|
|
3954
|
+
const out = []
|
|
3955
|
+
let cursor = 0
|
|
3956
|
+
for (const fs of fresh.sections) {
|
|
3957
|
+
// 骨架段前的行原样带过
|
|
3958
|
+
for (; cursor < fs.startLine; cursor++) out.push(fresh.lines[cursor])
|
|
3959
|
+
const k = topSectionKey(fs.headingLine)
|
|
3960
|
+
const os = oldByKey.get(k)
|
|
3961
|
+
const freshSeg = fresh.lines.slice(fs.startLine, fs.endLine)
|
|
3962
|
+
if (!os) {
|
|
3963
|
+
out.push(...freshSeg)
|
|
3964
|
+
} else if (HUMAN_SECTION_KEYS.includes(k)) {
|
|
3965
|
+
const oldSeg = old.lines.slice(os.startLine, os.endLine)
|
|
3966
|
+
if (/<待填|<!--TODO/.test(oldSeg.join('\n'))) {
|
|
3967
|
+
out.push(...freshSeg) // 未填 → 换新骨架占位
|
|
3968
|
+
} else {
|
|
3969
|
+
out.push(...oldSeg) // 已填 → 原样保留
|
|
3970
|
+
carriedSections.push(k)
|
|
3971
|
+
}
|
|
3972
|
+
} else if (MATRIX_SECTION_KEYS.includes(k)) {
|
|
3973
|
+
const merged = mergeProbeSection(old.lines.slice(os.startLine, os.endLine), freshSeg)
|
|
3974
|
+
carriedRows += merged.carried
|
|
3975
|
+
out.push(...merged.text.split('\n'))
|
|
3976
|
+
if (merged.carried > 0) carriedSections.push(k)
|
|
3977
|
+
} else if (k === PROBE_CONTAINER_KEY) {
|
|
3978
|
+
const merged = mergeProbeContainer(old.lines.slice(os.startLine, os.endLine), freshSeg)
|
|
3979
|
+
carriedRows += merged.carriedRows
|
|
3980
|
+
out.push(...merged.text.split('\n'))
|
|
3981
|
+
if (merged.carriedRows > 0) carriedSections.push(k)
|
|
3982
|
+
} else {
|
|
3983
|
+
out.push(...freshSeg) // 机器段(探针概览/接口声明等)→ 新骨架
|
|
3984
|
+
}
|
|
3985
|
+
cursor = fs.endLine
|
|
3986
|
+
oldByKey.delete(k)
|
|
3987
|
+
}
|
|
3988
|
+
for (; cursor < fresh.lines.length; cursor++) out.push(fresh.lines[cursor])
|
|
3989
|
+
// 旧文独有段(用户自定义节)追加保留
|
|
3990
|
+
if (oldByKey.size > 0) {
|
|
3991
|
+
out.push('')
|
|
3992
|
+
for (const [, s] of oldByKey) {
|
|
3993
|
+
out.push(...old.lines.slice(s.startLine, s.endLine))
|
|
3994
|
+
}
|
|
3995
|
+
}
|
|
3996
|
+
return { text: out.join('\n'), carriedSections, carriedRows }
|
|
3997
|
+
}
|
package/src/worktree.js
CHANGED
|
@@ -1326,9 +1326,17 @@ export class WorktreeManager {
|
|
|
1326
1326
|
* 写进 worktree 内的 .sillyspec——而 apply 的 filterDeliverableFiles 把 .sillyspec/changes/ 排除在
|
|
1327
1327
|
* 交付外(spec 文档不进代码 patch),cleanup 删除 worktree 这些产物即蒸发、主仓流程看不到。
|
|
1328
1328
|
* 删除前扫 worktree 内两棵子树做最后打捞:
|
|
1329
|
-
* changes/<name>/** →
|
|
1330
|
-
*
|
|
1331
|
-
*
|
|
1329
|
+
* changes/<name>/** → 三态判定(坑 archive-resurrect,2026-10-10 下游实证
|
|
1330
|
+
* 2026-10-09-attachment-inline-reference 残留 13 文件:归档把 changes/<name>/
|
|
1331
|
+
* 整体 rename 进 changes/archive/ 后,原路径缺失被误判 worktree 独有,旧快照
|
|
1332
|
+
* 整批复制回原路径复活已归档目录):
|
|
1333
|
+
* · 原路径同名存在 → 内容不同仅列清单 warn 不覆盖(主仓版本可能是更新的)
|
|
1334
|
+
* · 原路径缺、changes/archive/<name>/ 有副本 → 判归档搬运,跳过不复制
|
|
1335
|
+
* (不复活原路径、不覆盖 archive 权威副本)
|
|
1336
|
+
* · 原路径与 archive 均缺 → 真 worktree 独有照捞:归档态捞进
|
|
1337
|
+
* changes/archive/<name>/(原路径已注销禁止重建),未归档态捞回原路径
|
|
1338
|
+
* docs/** → 主仓缺失则 copy 回(模块文档卡片;同名差异不提示——tracked checkout 副本常态;
|
|
1339
|
+
* docs 不参与归档搬运,无三态)
|
|
1332
1340
|
* 只扫本变更子树(其它 change 的副本文件不越权搬运)。异常只 warn 不阻断清理。
|
|
1333
1341
|
* @private
|
|
1334
1342
|
*/
|
|
@@ -1340,7 +1348,11 @@ export class WorktreeManager {
|
|
|
1340
1348
|
if (resolve(wtSpec) === resolve(mainSpec)) return; // in-place:同一目录,无需打捞
|
|
1341
1349
|
const salvaged = [];
|
|
1342
1350
|
const conflicts = [];
|
|
1343
|
-
const
|
|
1351
|
+
const archivedSkipped = [];
|
|
1352
|
+
const salvagedIntoArchive = [];
|
|
1353
|
+
const archivedBase = join('changes', 'archive', name);
|
|
1354
|
+
const archivedBaseExists = existsSync(join(mainSpec, archivedBase));
|
|
1355
|
+
const salvageTree = (subDir, reportConflict, trackArchive) => {
|
|
1344
1356
|
const srcRoot = join(wtSpec, subDir);
|
|
1345
1357
|
if (!existsSync(srcRoot)) return;
|
|
1346
1358
|
const walk = (dir) => {
|
|
@@ -1349,28 +1361,50 @@ export class WorktreeManager {
|
|
|
1349
1361
|
if (entry.isDirectory()) { walk(src); continue; }
|
|
1350
1362
|
if (!entry.isFile()) continue;
|
|
1351
1363
|
const rel = relative(wtSpec, src);
|
|
1364
|
+
const rest = relative(srcRoot, src);
|
|
1352
1365
|
const dst = join(mainSpec, rel);
|
|
1366
|
+
const archivedDst = trackArchive ? join(mainSpec, archivedBase, rest) : null;
|
|
1353
1367
|
try {
|
|
1354
|
-
if (
|
|
1368
|
+
if (existsSync(dst)) {
|
|
1369
|
+
if (reportConflict && readFileSync(src, 'utf8') !== readFileSync(dst, 'utf8')) {
|
|
1370
|
+
conflicts.push(rel);
|
|
1371
|
+
}
|
|
1372
|
+
} else if (archivedDst && existsSync(archivedDst)) {
|
|
1373
|
+
archivedSkipped.push(rest);
|
|
1374
|
+
} else if (archivedDst && archivedBaseExists) {
|
|
1375
|
+
mkdirSync(dirname(archivedDst), { recursive: true });
|
|
1376
|
+
copyFileSync(src, archivedDst);
|
|
1377
|
+
salvagedIntoArchive.push(rest);
|
|
1378
|
+
} else {
|
|
1355
1379
|
mkdirSync(dirname(dst), { recursive: true });
|
|
1356
1380
|
copyFileSync(src, dst);
|
|
1357
1381
|
salvaged.push(rel);
|
|
1358
|
-
} else if (reportConflict && readFileSync(src, 'utf8') !== readFileSync(dst, 'utf8')) {
|
|
1359
|
-
conflicts.push(rel);
|
|
1360
1382
|
}
|
|
1361
1383
|
} catch { /* 单文件失败不拖垮整体打捞 */ }
|
|
1362
1384
|
}
|
|
1363
1385
|
};
|
|
1364
1386
|
walk(srcRoot);
|
|
1365
1387
|
};
|
|
1366
|
-
salvageTree(join('changes', name), true);
|
|
1367
|
-
salvageTree('docs', false);
|
|
1388
|
+
salvageTree(join('changes', name), true, true);
|
|
1389
|
+
salvageTree('docs', false, false);
|
|
1368
1390
|
if (salvaged.length > 0) {
|
|
1369
1391
|
console.warn(`⚠️ worktree 清理前打捞:${salvaged.length} 个 spec 流程产物只存在于 worktree 副本,已复制回主仓(apply 不搬运 .sillyspec/changes,不捞即随清理蒸发):`);
|
|
1370
1392
|
for (const f of salvaged.slice(0, 20)) console.warn(` ${join(mainSpec, f)}`);
|
|
1371
1393
|
if (salvaged.length > 20) console.warn(` …等共 ${salvaged.length} 个`);
|
|
1372
1394
|
details.push(`salvaged ${salvaged.length} spec artifacts to main repo`);
|
|
1373
1395
|
}
|
|
1396
|
+
if (salvagedIntoArchive.length > 0) {
|
|
1397
|
+
console.warn(`⚠️ worktree 清理前打捞:${salvagedIntoArchive.length} 个真独有产物(原路径与归档副本均缺)已捞进归档副本(变更已归档,原路径不重建):`);
|
|
1398
|
+
for (const f of salvagedIntoArchive.slice(0, 20)) console.warn(` ${join(mainSpec, archivedBase, f)}`);
|
|
1399
|
+
if (salvagedIntoArchive.length > 20) console.warn(` …等共 ${salvagedIntoArchive.length} 个`);
|
|
1400
|
+
details.push(`salvaged ${salvagedIntoArchive.length} spec artifacts into archive copy`);
|
|
1401
|
+
}
|
|
1402
|
+
if (archivedSkipped.length > 0) {
|
|
1403
|
+
console.warn(`⏭️ worktree 清理前打捞:${archivedSkipped.length} 个流程产物判定为归档搬运(主仓 changes/${name}/ 原路径不存在、changes/archive/${name}/ 已有副本),跳过不复制——防复活已归档目录:`);
|
|
1404
|
+
for (const f of archivedSkipped.slice(0, 20)) console.warn(` ${join(mainSpec, archivedBase, f)}`);
|
|
1405
|
+
if (archivedSkipped.length > 20) console.warn(` …等共 ${archivedSkipped.length} 个`);
|
|
1406
|
+
details.push(`skipped ${archivedSkipped.length} archived-moved spec artifacts (resurrect guard: changes/${name}/ not rebuilt)`);
|
|
1407
|
+
}
|
|
1374
1408
|
if (conflicts.length > 0) {
|
|
1375
1409
|
console.warn(`⚠️ worktree 与主仓存在同名但内容不同的流程产物(不覆盖,请人工确认是否需要合并):`);
|
|
1376
1410
|
for (const f of conflicts.slice(0, 20)) console.warn(` ${join(mainSpec, f)}`);
|
package/src/wt-commit.js
CHANGED
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
*
|
|
18
18
|
* 成功返回 HEAD hash(review.json 的 head 锚点直接可用);无变更 → ok=true + skipped。
|
|
19
19
|
*/
|
|
20
|
-
import { join, resolve } from 'node:path'
|
|
20
|
+
import { join, resolve, relative, sep, basename, dirname } from 'node:path'
|
|
21
21
|
import { existsSync, readFileSync } from 'node:fs'
|
|
22
22
|
import { withFileLock } from './quicklog.js'
|
|
23
23
|
import { safeGit } from './git-helper.js'
|
|
@@ -55,6 +55,48 @@ async function gitWithLockRetry(cwd, args, label, { retries = 6, waitMs = 1000 }
|
|
|
55
55
|
* @returns {Promise<{ok:boolean,skipped:boolean,head:string,shortHead:string,worktreePath:string,changeName:string,files:string[]}>}
|
|
56
56
|
* ok=false 时抛错(调用方打错误 + exit 1),skipped=无变更可提交(HEAD 不动,不算失败)。
|
|
57
57
|
*/
|
|
58
|
+
/**
|
|
59
|
+
* 从 cwd 推断变更名(wt-commit 免 --change 通道;FR-08 / D-006@v1)。
|
|
60
|
+
* 跨仓 worktree 名 <change>--<repoKey>——仅当后缀命中 repos 注册表(worktrees 上两级
|
|
61
|
+
* local.yaml)且剥出候选是已知变更目录才剥(不猜切分);全段命中主仓已知变更不剥(防
|
|
62
|
+
* 变更真名恰以 --repoKey 结尾);均不中 → 原样透传(现状行为,调用方自然报错引导
|
|
63
|
+
* 显式 --change)。导出供三态回归直测(index.js dispatch 内联形态不可测——独立审查指出的
|
|
64
|
+
* 验证面缺口)。
|
|
65
|
+
*/
|
|
66
|
+
export async function inferChangeFromWorktreeCwd(dir) {
|
|
67
|
+
// 深路径归一(FR-08):cwd 可能在 worktree 内多层深处——specBase 推导须从 worktree 根起算。
|
|
68
|
+
// 找 'worktrees' 目录段在原路径中的位置,根 = 该段后第一段截尾;specBaseGuess = 根上溯
|
|
69
|
+
// 三级(worktrees → .runtime → .sillyspec 的父,即主仓 specBase)。
|
|
70
|
+
const s = String(dir)
|
|
71
|
+
const norm = s.split('\\').join('/')
|
|
72
|
+
const wtIdx = norm.lastIndexOf('/worktrees/')
|
|
73
|
+
let seg = null
|
|
74
|
+
let specBaseGuess = null
|
|
75
|
+
if (wtIdx !== -1) {
|
|
76
|
+
const rest = norm.slice(wtIdx + '/worktrees/'.length)
|
|
77
|
+
const rootRel = rest.split('/')[0]
|
|
78
|
+
seg = rootRel || null
|
|
79
|
+
const rootNorm = norm.slice(0, wtIdx + '/worktrees/'.length + rootRel.length)
|
|
80
|
+
specBaseGuess = rootNorm.split('/').slice(0, -3).join('/')
|
|
81
|
+
}
|
|
82
|
+
if (seg && seg.includes('--')) {
|
|
83
|
+
try {
|
|
84
|
+
if (specBaseGuess) {
|
|
85
|
+
const knownWhole = existsSync(join(specBaseGuess, 'changes', seg))
|
|
86
|
+
const dash = seg.lastIndexOf('--')
|
|
87
|
+
const cand = seg.slice(0, dash)
|
|
88
|
+
const suffix = seg.slice(dash + 2)
|
|
89
|
+
if (!knownWhole && cand && /^[A-Za-z0-9_.-]+$/.test(suffix)) {
|
|
90
|
+
const { parseRepoRegistry } = await import('./stages/plan-postcheck.js')
|
|
91
|
+
const reg = parseRepoRegistry(readFileSync(join(specBaseGuess, 'local.yaml'), 'utf8'))
|
|
92
|
+
if (reg.has(suffix) && existsSync(join(specBaseGuess, 'changes', cand))) seg = cand
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
} catch { /* 注册表不可读 → 原样透传(fail-safe) */ }
|
|
96
|
+
}
|
|
97
|
+
return seg
|
|
98
|
+
}
|
|
99
|
+
|
|
58
100
|
export async function runWtCommit({ changeName, message, pathspecs = [], pathspecFile = null, cwd = process.cwd() }) {
|
|
59
101
|
if (!changeName) throw new Error('缺少 --change <变更名>(worktree 名;在 worktree 内运行时可省略,CLI 从 cwd 推断)')
|
|
60
102
|
if (!message || !message.trim()) throw new Error('缺少 -m/--message <提交信息>(建议 "<task-NN 摘要>")')
|
|
@@ -77,10 +119,47 @@ export async function runWtCommit({ changeName, message, pathspecs = [], pathspe
|
|
|
77
119
|
// 隔离模式(worktree 目录存在)→ 提交目标 = worktree 本体;in-place-fallback(meta 声明无
|
|
78
120
|
// worktree 直改主仓)→ 主仓根(与 MultiRepoContext 主仓 entry 的 worktreePath 兜底口径一致)。
|
|
79
121
|
// 顺序先看目录再看 meta:worktree 目录存在即隔离态,meta 缺失/损坏不改变目标(fail-safe)。
|
|
122
|
+
// FR-08(2026-10-09-verify-reuse-friction / D-006@v1):cwd 已在本变更的 worktree 内 →
|
|
123
|
+
// 提交目标=该 worktree 本体。跨仓 worktree 名 <change>--<repoKey> 不在 getWorktreePath
|
|
124
|
+
// 的主仓命名域(原路径必落「worktree 不存在」错);主仓 worktree 场景与原值同目录零漂移。
|
|
125
|
+
// 归属校验(不猜):worktree 根段 === changeName,或 === changeName--<注册repoKey>(repos
|
|
126
|
+
// 注册表读自 worktreeBase 上两级的 local.yaml;注册表不可读 → 只认精确名,fail-safe)。
|
|
80
127
|
let target = null
|
|
81
|
-
|
|
128
|
+
{
|
|
129
|
+
// 基座发现(目录事实):优先 wm.worktreeBase(主仓);cwd 不在其下时向上找名为 worktrees
|
|
130
|
+
// 的目录作基座(跨仓 worktree 是跨仓仓的 git worktree,WorktreeManager 从其 cwd 算不出
|
|
131
|
+
// 主仓基座——跨仓 worktree 的父目录本身就是 worktrees 基座)。
|
|
132
|
+
let base = resolve(wm.worktreeBase)
|
|
133
|
+
{
|
|
134
|
+
let d = resolve(cwd)
|
|
135
|
+
for (;;) {
|
|
136
|
+
if (dirname(d) === base) break
|
|
137
|
+
const parent = dirname(d)
|
|
138
|
+
if (basename(parent) === 'worktrees') { base = parent; break }
|
|
139
|
+
if (parent === d) break
|
|
140
|
+
d = parent
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
const cwdAbs = resolve(cwd)
|
|
144
|
+
if (cwdAbs.startsWith(base + sep)) {
|
|
145
|
+
const wtRoot = relative(base, cwdAbs).split(sep)[0]
|
|
146
|
+
let own = wtRoot === changeName
|
|
147
|
+
if (!own && wtRoot.startsWith(changeName + '--')) {
|
|
148
|
+
const suffix = wtRoot.slice(changeName.length + 2)
|
|
149
|
+
if (suffix && /^[A-Za-z0-9_.-]+$/.test(suffix)) {
|
|
150
|
+
try {
|
|
151
|
+
const { parseRepoRegistry } = await import('./stages/plan-postcheck.js')
|
|
152
|
+
const reg = parseRepoRegistry(readFileSync(join(base, '..', '..', 'local.yaml'), 'utf8'))
|
|
153
|
+
own = reg.has(suffix)
|
|
154
|
+
} catch { own = false }
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
if (own && existsSync(join(base, wtRoot))) target = join(base, wtRoot)
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
if (!target && existsSync(worktreePath)) {
|
|
82
161
|
target = worktreePath
|
|
83
|
-
} else {
|
|
162
|
+
} else if (!target) {
|
|
84
163
|
const meta = wm.getMeta(changeName)
|
|
85
164
|
if (meta && meta.mode === 'in-place-fallback') {
|
|
86
165
|
target = resolve(wm.worktreeBase, '..', '..', '..') // <main>/.sillyspec/.runtime/worktrees → <main>
|
|
@@ -1,116 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: sillyspec:quick
|
|
3
|
-
description: 用于明确、低风险、范围很小的直接任务。适合用户说"直接改、快速修、顺手调整、改个文案、修个小 bug、更新一个文件、不要完整流程"。跳过 brainstorm/plan,但仍按 sillyspec quick 流程执行。
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
## 何时使用
|
|
7
|
-
|
|
8
|
-
- 明确、低风险、范围小的直接修改:改文案、修小 bug、更新单个文件
|
|
9
|
-
- 用户说"直接改、快速修、顺手调整、不要完整流程"
|
|
10
|
-
- brainstorm 判定 `scale=small` 的变更:带 design.md 进 quick(`sillyspec run quick --linked-changes <变更名>`),design 当背景
|
|
11
|
-
- 跳过 brainstorm/plan,但仍走 quick 的 3 步流程(理解任务 → 实现 → 自检提交)
|
|
12
|
-
|
|
13
|
-
## 多变更说明(quick 特殊,务必注意)
|
|
14
|
-
|
|
15
|
-
quick 阶段的 `--change` 语义是「关联变更」**且会触发步骤重置**,**不要用 `--change` 来指定关联变更**。多活跃变更时改用:
|
|
16
|
-
|
|
17
|
-
- `--linked-changes none`:不关联,仅记 QUICKLOG(CLI 启动时写入)
|
|
18
|
-
- `--linked-changes a,b`:显式关联到变更 a、b
|
|
19
|
-
- `--non-interactive`:CI/脚本环境,默认不关联(避免交互 prompt 崩溃)
|
|
20
|
-
|
|
21
|
-
首次 `sillyspec run quick` 选定的关联会持久化到 `.runtime/quick-sessions/<quick-session-id>/guard.json`(每个 quick 会话独立,多会话并发不互覆盖),后续 `--done` 自动复用,**不会重复弹交互 prompt**。
|
|
22
|
-
|
|
23
|
-
## 步骤生命周期(所有阶段通用)
|
|
24
|
-
|
|
25
|
-
> `sillyspec quick` 是 `sillyspec run quick` 的顶层别名,两者等价。
|
|
26
|
-
|
|
27
|
-
```bash
|
|
28
|
-
sillyspec run quick # 输出当前步骤 prompt(首次会记录 baseline)
|
|
29
|
-
sillyspec run quick --done --output "摘要" # 完成当前步骤
|
|
30
|
-
sillyspec run quick --status # 查看阶段进度
|
|
31
|
-
sillyspec run quick --skip # 跳过可选步骤
|
|
32
|
-
sillyspec run quick --reset # 重置阶段(从头开始)
|
|
33
|
-
sillyspec run quick --reopen --from-step N # 重新打开已完成阶段修订(N=序号或名称)
|
|
34
|
-
```
|
|
35
|
-
|
|
36
|
-
### 已有进行中会话的恢复(sessionId)
|
|
37
|
-
|
|
38
|
-
`sillyspec run quick` / `run quick --done` / `--status` **不带 `--change` 时,是启动一个新 quick 会话**(CLI 分配新 `quick-<hash>` + 新 ql-ID,QUICKLOG 追加「进行中」条目),不是恢复正在进行的旧会话。多会话并发或中断恢复时,必须带上启动时 CLI 打印的 sessionId:
|
|
39
|
-
|
|
40
|
-
```bash
|
|
41
|
-
sillyspec run quick --change quick-<hash> # 恢复查看该会话当前 step prompt
|
|
42
|
-
sillyspec run quick --done --change quick-<hash> --output "…" # 完成该会话步骤
|
|
43
|
-
```
|
|
44
|
-
|
|
45
|
-
- 这里的 `--change quick-<hash>` 是「恢复到该 CLI 生成的 session」,与下方 `--linked-changes` 的关联业务变更语义不同:想关联真实业务变更才用 `--linked-changes`。
|
|
46
|
-
- 误启动的空壳会话:`sillyspec run quick --cancel --change quick-<hash>` 一键取消(QUICKLOG 条目翻「已取消」+ tasks.md 挂载行移除 + 会话目录清理 + db 行注销;已完成/已勾选的拒绝取消)。
|
|
47
|
-
|
|
48
|
-
## 通用参数(所有阶段适用)
|
|
49
|
-
|
|
50
|
-
| 参数 | 说明 |
|
|
51
|
-
|---|---|
|
|
52
|
-
| `--spec-dir <path>` | 指定规范目录(默认 `<项目>/.sillyspec`) |
|
|
53
|
-
| `--input "<一句话任务描述>"` | 通用参数,**quick 启动时强烈建议带**:作为 QUICKLOG 条目标题,条目从第一分钟即语义可读。不带则落「(quick 任务)」占位标题——平台「快速修复」列表默认隐藏进行中的占位条目,语义标题要到最终 `--done` 才回填(关联变更有 proposal/design 标题时可自动提取,免传) |
|
|
54
|
-
| `--non-interactive` | CI/脚本下禁用交互式 prompt |
|
|
55
|
-
| `--interactive` | 强制交互(即便 stdin 非 TTY) |
|
|
56
|
-
| `--skip-approval` | 跳过阶段转换/审批检查(不能跳产物校验 gate——review.json/文档产物硬校验仍在) |
|
|
57
|
-
|
|
58
|
-
## quick 特有参数
|
|
59
|
-
|
|
60
|
-
| 参数 | 说明 |
|
|
61
|
-
|---|---|
|
|
62
|
-
| `--linked-changes none\|a,b` | **显式关联变更(取代 `--change`,推荐)**。none=不关联,a,b=关联列表 |
|
|
63
|
-
| `--files a.js,b.js` | 显式声明本次允许修改的文件(边界保护 + 声明即归属:多 agent 并发仓防他者窗口文件混入 QUICKLOG 文件行,未声明窗口文件进「审计:」行追溯;**他向亦然**——其他 active 会话已声明的文件在你的 `--done` 审计中自动剔除归该会话,不再误拦、无需 `--force-baseline`)。**中途可追加**:发现要改声明外文件(如新写的测试)时,带新增文件恢复会话即并入边界(追加不替换)——`sillyspec run quick --files <新增> --change <sessionId>`;不带 `--files` 恢复则边界原样保留 |
|
|
64
|
-
| `--file-notes "p::注 \|\| p::注"` | quick `--done` 用:QUICKLOG「文件:」行落盘为多行带括注 bullet(省事后手改文件行)。格式 `path::括注`,`\|\|` 分隔多条;**只随 step3 --done 同命令传**(CLI 短进程,step1/step2 传无效,不带到 step3) |
|
|
65
|
-
| `--allow-new` | 允许新增文件(默认禁止,防意外创建) |
|
|
66
|
-
| `--allow-delete` | 允许删除文件(默认 fail-closed,删除是破坏性操作;确认删除带此 flag 显式解锁) |
|
|
67
|
-
| `--force-baseline` | 允许覆盖 baseline 受保护文件 / 压制 `.sillyspec/` 危险判定(危险,慎用) |
|
|
68
|
-
| `--confirm` | ⚠️ 仅打印变更概览,**不解锁 blocked**(blocked 仍 exit 1)。真正解锁用 `--force-baseline`/`--allow-new`/`--allow-delete` |
|
|
69
|
-
|
|
70
|
-
## 审计与并发变更(`--done` 边界审计)
|
|
71
|
-
|
|
72
|
-
`--done` 收尾时对比 step1 baseline 与当前 `git status`,拦「危险文件 / 新增文件 / 覆盖 baseline」。要点:
|
|
73
|
-
|
|
74
|
-
- **并发其他会话的 `.sillyspec/changes/<非关联变更>/` 不再被本 quick 拦截**。quick 自己没有 `changes/` 目录,该路径下非关联内容视为并发会话的工作,整体放行(确定性审计无法区分「并发工作」与「本 quick 偷建变更」,后者这类意图软判定留给 sillyhub)。
|
|
75
|
-
- **其他 active 会话已声明的文件自动豁免(`--files` 的他向语义)**。多 agent 并发时,并行 quick 会话 `--files` 显式声明的文件(如它的 `daemon/router.py`)出现在你的窗口内(它在你启动后改的、不在你的 baseline)——`--done` 审计自动剔除归该会话边界管,不再判危险 BLOCK、无需 `--force-baseline`。放行可见:输出 🔗 软警告逐文件列归属会话;若确系你的改动,恢复会话时 `--files` 追加声明;超 7 天未收尾的僵尸会话声明自动失效(`sillyspec run quick --cancel --change <他者会话ID>` 清理)。豁免只认他者**显式声明**——未声明的他者文件照旧走各门判定(漏声明的补 `--files`,危险文件仍需 `--force-baseline`)。
|
|
76
|
-
- **关联变更的文件仍走审计**:reverse-sync 改自己关联变更的 `design.md` 会被拦,需 `--force-baseline` 显式确认。
|
|
77
|
-
- **baseline 折叠目录前缀匹配**:step1 启动时整片 `changes/` 未跟踪会被 git 折叠成 `?? .sillyspec/changes/`(带尾斜杠 token);审计时若该目录下文件被并发会话跟踪而展开成文件级路径,按尾斜杠 token 前缀放行其下所有文件,不误判。
|
|
78
|
-
- **同文件并发 warn(advisory,不阻断)**:step1 启动时若你的某个 `allowedFile` 已在他者脏文件列表里(他者也改了这文件),--done 时 CLI 会比对当前内容 hash 与启动时录入的 hash——不一致(你也改了它)即判「同文件并发」并 warn:整文件 pathspec 提交会夹带他者 hunk。warn 给出分离指引(`git add -p <file>` 交互选你自己的 hunk,或 `git diff <file> > mine.patch` 编辑后 `git apply --cached mine.patch` 再 commit)。这**不阻断** --done(你可能有意整文件提交),看 warn 后自行决定是否分离。
|
|
79
|
-
- **`--confirm` ≠ 解锁**:它只打印概览。blocked 时真正解锁的是 `--force-baseline`(压制 `.sillyspec/` 危险判定 + baseline 覆盖)/`--allow-new`(放行新增)/`--allow-delete`(放行删除,删除是破坏性操作默认 fail-closed,须显式 opt-in),三者可在 step1 或 `--done` 传,都生效。
|
|
80
|
-
|
|
81
|
-
## 典型用法
|
|
82
|
-
|
|
83
|
-
```bash
|
|
84
|
-
# 推荐启动:带一句话语义标题(QUICKLOG/平台快速修复列表进行中即可见可读)
|
|
85
|
-
sillyspec run quick --input "修复登录限流 INCR 计数误清" --linked-changes none --files src/auth.ts
|
|
86
|
-
|
|
87
|
-
# 单变更项目,直接开始
|
|
88
|
-
sillyspec run quick
|
|
89
|
-
|
|
90
|
-
# 多变更项目,显式不关联
|
|
91
|
-
sillyspec run quick --linked-changes none
|
|
92
|
-
|
|
93
|
-
# 多变更项目,关联到指定变更
|
|
94
|
-
sillyspec run quick --linked-changes 2026-07-03-add-login
|
|
95
|
-
|
|
96
|
-
# CI/脚本(非交互,避免 prompt 崩溃)
|
|
97
|
-
sillyspec run quick --non-interactive
|
|
98
|
-
sillyspec run quick --done --linked-changes none --output "修复手机号校验"
|
|
99
|
-
|
|
100
|
-
# 限定修改文件范围
|
|
101
|
-
sillyspec run quick --files src/phone.ts,src/phone.test.ts
|
|
102
|
-
```
|
|
103
|
-
|
|
104
|
-
## 铁律
|
|
105
|
-
|
|
106
|
-
- **必须用 exec 工具(shell)执行 CLI,不要自己编造流程**
|
|
107
|
-
- quick 直接在主工作区改代码(不创建 worktree),范围必须小且明确
|
|
108
|
-
- 完成后立即 `--done`,不跳过
|
|
109
|
-
- QUICKLOG 记录的**骨架由 CLI 接管**:启动时 CLI 自动分配 ql-ID 并在 `.sillyspec/quicklog/QUICKLOG-<user>.md` 写「进行中」条目(含关联变更 tasks.md),`--done` 时 CLI 自动翻「已完成」+ 勾选 task + 回填文件路径。ql-ID 分配/状态/task 你无需手写,只需用注入的 `<quicklog-id>` 在模块文档变更索引引用
|
|
110
|
-
- **QUICKLOG 落盘已结构化(`--done` 后按需核对,多数无需手改)**:CLI 已落盘结构化条目——标题从 `--output` 的「需求:」自动提取、正文四字段自动分行、文件行用 `--file-notes` 时为多行带括注。`--done` 后只需核对:标题弱才改(禁留 `(quick 任务)` 占位);没用 `--file-notes` 时文件行是单行、可事后补括注(参照同文件早期丰富条目);正文 `需求:`/`根因:`/`方案:`/`结果:` 四段按需充实(禁只留一段「结果:」)。一条 quick = 一条独立 ql,不追加到旧条目
|
|
111
|
-
- **收尾顺序(模块文档在 `--done` 前,QUICKLOG 在 `--done` 后,别记混)**:① 命中模块→改模块文档→`git add`;② `sillyspec run quick --done --change <id> --output "四字段" [--file-notes "..."]`(CLI 自动翻完成 + 勾 task + 落盘 QUICKLOG 标题/文件/正文;若 `--linked-changes` 关联的真实变更其 tasks.md 已全部勾选**且该变更未进入完整流程**(进度库阶段停在 brainstorm 及之前),CLI 会自动将其归档到 `changes/archive/`;已走到 plan/execute/verify/archive 的变更不自动归档(tasks.md 全勾不等于流程收尾),须走原流程收尾);③ 核对 QUICKLOG 标题(CLI 已从「需求:」提取并截断,写成短标题则无需改)→若改了再 `git add`
|
|
112
|
-
- **最后一步 `--done` 必须给全四字段**(逐项一句话):🔧 **推荐四参数形式**(CLI 自动合成结构化模板,无格式事故面):`--done --change <id> --req "一句语义化短标题" --cause "为什么改" --solution "怎么改的" --result "验证结果"`。兼容旧形式 `--output "需求:… 根因:… 方案:… 结果:…"`(手拼须懂截断规则、禁嵌套全角冒号,缺字段被拒 exit 1)。这是 QUICKLOG「结果:」归档的唯一来源。**`--req` 写一句语义化短标题**(写「改了什么」,如「登录限流修复——INCR 计数误清」)——CLI 把它提取为 QUICKLOG 条目标题,截到首个标点、超 80 字截断;写完整需求长句会被截成语义不完整的状语前半段,需求背景放 `--cause`/`--solution`。前两个 step 的 `--output` 是中间摘要,不用此模板
|
|
113
|
-
- **禁止**在没有运行 CLI 的情况下自行决定流程
|
|
114
|
-
|
|
115
|
-
## 用户指令
|
|
116
|
-
$ARGUMENTS
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: sillyspec:run-quick
|
|
3
|
-
description: 已退役重定向 — quick 通道已拒绝新会话(v3.31.0 起),小修补请用 /sillyspec:flow
|
|
4
|
-
---
|
|
5
|
-
# 通道已退役
|
|
6
|
-
|
|
7
|
-
quick 通道已于 v3.31.0 直接退役:`sillyspec run quick` 对新会话一律拒绝(exit 1 并指路 flow start)。本卡只保留触发词重定向,勿执行任何 quick 新会话命令。
|
|
8
|
-
|
|
9
|
-
范围明确的小修补请走**轻量变更**(/sillyspec:flow):
|
|
10
|
-
|
|
11
|
-
```bash
|
|
12
|
-
sillyspec flow start --change <变更名> --input "<动机与背景>
|
|
13
|
-
|
|
14
|
-
成功标准:
|
|
15
|
-
- <可验证标准>"
|
|
16
|
-
# → 直接干活(改代码写测试,治理工件 CLI 机器起草)
|
|
17
|
-
sillyspec flow done --change <变更名>
|
|
18
|
-
```
|
|
19
|
-
|
|
20
|
-
升级前遗留的在途 quick 会话仍可收尾:`sillyspec run quick --change <会话ID>` 续跑 / `--done` 收口 / `--cancel` 取消(以 CLI 拒绝文案里的指引为准)。
|