sillyspec 3.27.1 → 3.27.5
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-execute/SKILL.md +8 -0
- package/.claude/skills/sillyspec-quick/SKILL.md +2 -1
- package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-archive/SKILL.md +87 -0
- package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-auto/SKILL.md +93 -0
- package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-brainstorm/SKILL.md +114 -0
- package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-commit/SKILL.md +67 -0
- package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-continue/SKILL.md +47 -0
- package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-doctor/SKILL.md +76 -0
- package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-execute/SKILL.md +214 -0
- package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-explore/SKILL.md +102 -0
- package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-export/SKILL.md +57 -0
- package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-init/SKILL.md +177 -0
- package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-knowledge/SKILL.md +270 -0
- package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-plan/SKILL.md +156 -0
- package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-quick/SKILL.md +116 -0
- package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-resume/SKILL.md +70 -0
- package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-scan/SKILL.md +79 -0
- package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-state/SKILL.md +64 -0
- package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-status/SKILL.md +54 -0
- package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-verify/SKILL.md +87 -0
- package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-workspace/SKILL.md +175 -0
- package/.claude/worktrees/sillyspec-data-model-research/.gitattributes +2 -0
- package/.claude/worktrees/sillyspec-data-model-research/.husky/pre-push +20 -0
- package/.claude/worktrees/sillyspec-data-model-research/README.md +203 -0
- package/.claude/worktrees/sillyspec-data-model-research/SKILL.md +91 -0
- package/.claude/worktrees/sillyspec-data-model-research/bin/sillyspec.js +2 -0
- package/.claude/worktrees/sillyspec-data-model-research/logo.jpg +0 -0
- package/.claude/worktrees/sillyspec-data-model-research/package-lock.json +977 -0
- package/.claude/worktrees/sillyspec-data-model-research/package.json +39 -0
- package/.claude/worktrees/sillyspec-data-model-research/packages/dashboard/dist/assets/index-Bq_Z2hne.js +7446 -0
- package/.claude/worktrees/sillyspec-data-model-research/packages/dashboard/dist/assets/index-O2W5RV4z.css +1 -0
- package/.claude/worktrees/sillyspec-data-model-research/packages/dashboard/dist/favicon.jpg +0 -0
- package/.claude/worktrees/sillyspec-data-model-research/packages/dashboard/dist/index.html +16 -0
- package/.claude/worktrees/sillyspec-data-model-research/packages/dashboard/dist/logo.jpg +0 -0
- package/.claude/worktrees/sillyspec-data-model-research/packages/dashboard/dist/prototype-dashboard.html +836 -0
- package/.claude/worktrees/sillyspec-data-model-research/packages/dashboard/dist/prototype-overview.html +256 -0
- package/.claude/worktrees/sillyspec-data-model-research/packages/dashboard/package.json +25 -0
- package/.claude/worktrees/sillyspec-data-model-research/packages/dashboard/server/executor.js +95 -0
- package/.claude/worktrees/sillyspec-data-model-research/packages/dashboard/server/index.js +679 -0
- package/.claude/worktrees/sillyspec-data-model-research/packages/dashboard/server/parser.js +526 -0
- package/.claude/worktrees/sillyspec-data-model-research/packages/dashboard/server/watcher.js +357 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/agent-session-log.js +803 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/change-list.js +290 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/change-risk-profile.js +270 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/check-primitives.js +65 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/classify-change.js +109 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/commit-suggest.js +202 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/config-cat.js +88 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/config-schema.js +328 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/constants.js +97 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/contract-matrix.js +480 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/datetime.js +20 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/db-engine.js +55 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/db.js +381 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/decision-distill.js +517 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/dispatch/backends/local-agent.js +155 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/dispatch/backends/sillyhub-mcp.js +245 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/dispatch/probe.js +246 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/dispatch/strategy.js +121 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/docs-check.js +825 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/docs-debt.js +393 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/docs-gate.js +107 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/doctor-diagnostics.js +1099 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/endpoint-extractor.js +420 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/foreign-declared.js +78 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/fs-atomic.js +73 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/git-helper.js +150 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/hooks/claude-pre-tool-use.cjs +130 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/hooks/worktree-guard.js +892 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/index.js +2798 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/init.js +626 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/knowledge-match.js +231 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/local-detect.js +128 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/local-register.js +104 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/machine-interface.js +543 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/migrate.js +127 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/module-impact.js +166 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/module-resolve.js +222 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/modules.js +580 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/progress/change-registry.js +336 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/progress/consistency-doctor.js +355 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/progress/shared.js +36 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/progress/stage-machine.js +594 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/progress/step-store.js +265 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/progress.js +1304 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/quick-recommend.js +115 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/quicklog.js +852 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/review-tier.js +77 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/run/command.js +1595 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/run/complete-handlers.js +1552 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/run/complete.js +1259 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/run/concurrent-detect.js +156 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/run/gates.js +1084 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/run/multi-repo-context.js +208 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/run/next.js +128 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/run/prompt.js +843 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/run/quick-audit.js +186 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/run/scan-profile.js +269 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/run/shared.js +1562 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/run/stage.js +584 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/run.js +23 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/scan-diff.js +388 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/scan-postcheck.js +512 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/scan-staleness.js +95 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/setup.js +414 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/sillyhub-mcp/client.js +597 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/sillyhub-mcp/config.js +62 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/spec-dir-typo.js +50 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/spec-sync.js +358 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/stage-contract-engine.js +171 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/stage-contract-spec.js +550 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/stage-contract.js +889 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/stage-review.js +597 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/stages/archive.js +199 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/stages/brainstorm-auto.js +236 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/stages/brainstorm.js +617 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/stages/cmd-existence.js +145 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/stages/doctor.js +540 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/stages/execute.js +1275 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/stages/explore.js +43 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/stages/index.js +29 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/stages/knowledge.js +514 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/stages/plan-postcheck.js +1344 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/stages/plan.js +650 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/stages/quick.js +167 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/stages/scan.js +608 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/stages/status.js +64 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/stages/verify.js +246 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/sync.js +1470 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/task-review.js +1498 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/taskcard.js +210 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/verify-postcheck.js +1718 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/verify-probes.js +277 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/version.js +19 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/workflow.js +600 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/workspace.js +125 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/worktree-apply.js +1446 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/worktree-deps.js +576 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/worktree.js +1715 -0
- package/.claude/worktrees/sillyspec-data-model-research/templates/claude-instruction.md +23 -0
- package/.claude/worktrees/sillyspec-data-model-research/templates/prompts/taskcard-rules.md +28 -0
- package/.claude/worktrees/sillyspec-data-model-research/templates/prompts/testcase-design.md +12 -0
- package/.claude/worktrees/sillyspec-data-model-research/templates/prompts/verify-probes.md +32 -0
- package/.claude/worktrees/sillyspec-data-model-research/templates/workflows/archive-impact.yaml +79 -0
- package/.claude/worktrees/sillyspec-data-model-research/templates/workflows/scan-docs.yaml +132 -0
- package/README.md +1 -1
- package/eval/README.md +86 -0
- package/eval/eval.config.json +28 -0
- package/eval/lib/adapters/cli.mjs +63 -0
- package/eval/lib/adapters/dryrun.mjs +27 -0
- package/eval/lib/config.mjs +82 -0
- package/eval/lib/db.mjs +76 -0
- package/eval/lib/report.mjs +120 -0
- package/eval/lib/runner.mjs +157 -0
- package/eval/lib/tasks.mjs +73 -0
- package/eval/lib/util.mjs +107 -0
- package/eval/lib/workdir.mjs +30 -0
- package/eval/run.mjs +83 -0
- package/eval/tasks/001-slugify/files/package.json +6 -0
- package/eval/tasks/001-slugify/solution/src/slugify.mjs +26 -0
- package/eval/tasks/001-slugify/task.json +6 -0
- package/eval/tasks/001-slugify/verify.mjs +28 -0
- package/eval/tasks/002-each-day/files/package.json +6 -0
- package/eval/tasks/002-each-day/files/src/dates.mjs +4 -0
- package/eval/tasks/002-each-day/solution/src/dates.mjs +30 -0
- package/eval/tasks/002-each-day/task.json +6 -0
- package/eval/tasks/002-each-day/verify.mjs +31 -0
- package/eval/tasks/003-word-report/files/data/notes.txt +5 -0
- package/eval/tasks/003-word-report/files/package.json +6 -0
- package/eval/tasks/003-word-report/solution/bin/report.mjs +36 -0
- package/eval/tasks/003-word-report/task.json +6 -0
- package/eval/tasks/003-word-report/verify.mjs +38 -0
- package/package.json +4 -2
- package/src/agent-session-log.js +803 -0
- package/src/change-list.js +22 -2
- package/src/change-risk-profile.js +83 -14
- package/src/config-schema.js +18 -3
- package/src/contract-matrix.js +153 -8
- package/src/datetime.js +20 -0
- package/src/decision-distill.js +517 -0
- package/src/dispatch/backends/local-agent.js +4 -4
- package/src/docs-check.js +227 -0
- package/src/docs-debt.js +187 -24
- package/src/doctor-diagnostics.js +102 -2
- package/src/foreign-declared.js +78 -0
- package/src/index.js +99 -10
- package/src/init.js +9 -7
- package/src/knowledge-match.js +107 -6
- package/src/local-register.js +7 -1
- package/src/module-resolve.js +12 -1
- package/src/modules.js +2 -1
- package/src/plan-adopt-waves.js +126 -0
- package/src/quicklog.js +61 -7
- package/src/run/command.js +139 -20
- package/src/run/complete-handlers.js +48 -19
- package/src/run/complete.js +43 -3
- package/src/run/gates.js +87 -22
- package/src/run/prompt.js +84 -42
- package/src/run/quick-audit.js +28 -0
- package/src/run/shared.js +176 -11
- package/src/run/stage.js +23 -0
- package/src/run.js +2 -2
- package/src/scan-postcheck.js +30 -17
- package/src/stage-contract-spec.js +2 -1
- package/src/stage-contract.js +68 -4
- package/src/stages/archive.js +53 -12
- package/src/stages/brainstorm-auto.js +7 -0
- package/src/stages/brainstorm.js +11 -0
- package/src/stages/doctor.js +157 -1
- package/src/stages/execute.js +33 -6
- package/src/stages/plan-postcheck.js +79 -18
- package/src/stages/plan.js +4 -0
- package/src/stages/quick.js +6 -1
- package/src/stages/verify.js +31 -14
- package/src/sync.js +94 -2
- package/src/task-review.js +2 -2
- package/src/taskcard-placeholders.js +21 -0
- package/src/taskcard.js +10 -1
- package/src/verify-postcheck.js +315 -13
- package/src/verify-probes.js +46 -4
- package/src/worktree-apply.js +184 -7
- package/src/worktree-deps.js +69 -0
- package/src/worktree.js +201 -58
- package/templates/prompts/taskcard-rules.md +1 -0
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* plan-adopt-waves.js — `sillyspec plan-adopt-waves` 把 plan.md 的 Wave 段一键重排为
|
|
3
|
+
* depends_on 拓扑分组(坑 wave-manual-mismatch-noise,2026-08-24 用户反馈二期:主控手排
|
|
4
|
+
* Wave 与 CLI 拓扑比对不一致只能手动改;依赖方向违规修复后,本命令给一条零手写出路)。
|
|
5
|
+
*
|
|
6
|
+
* 行为:
|
|
7
|
+
* - depMap(tasks/*.md 的 depends_on,collectTaskDepMap 与 postcheck 同源)→ topoSortWaves
|
|
8
|
+
* - 重写 plan.md 的 `## Wave N` 段块(段内按编号排序;首段「并行,无依赖」、其余「依赖前序 Wave」)
|
|
9
|
+
* - 无显式 Wave 标题 → 插在「## 任务总表」前(无总表则文末追加)
|
|
10
|
+
* - best-effort 同步任务总表的 W 列(仅认 7 列模板行;不匹配的行 warn 不强改)
|
|
11
|
+
* - Wave 段内若含非引用内容(正文/表格)→ 拒绝重写(防误删),提示先手动整理
|
|
12
|
+
* - 写后复跑 validateBlueprintConsistency 打印结果(防 adopt 把手工安全串行收敛出
|
|
13
|
+
* 同 Wave 共享路径冲突——topo 只看依赖不看文件重叠)
|
|
14
|
+
* - --dry-run 只打印将写入的 Wave 段不落盘;幂等(重复跑产出相同结构);行尾统一 LF
|
|
15
|
+
*/
|
|
16
|
+
import { existsSync, readFileSync, writeFileSync } from 'fs'
|
|
17
|
+
import { join } from 'path'
|
|
18
|
+
import { topoSortWaves, collectTaskDepMap, validateBlueprintConsistency } from './stages/plan-postcheck.js'
|
|
19
|
+
|
|
20
|
+
const WAVE_HEADING_RE = /^#+\s*Wave\s+\d+/i
|
|
21
|
+
const TASK_REF_RE = /^[-*]\s+(?:\[[ x]\]\s*)?task-\d+\s*[::\s]*$/i
|
|
22
|
+
const LEGACY_TASK_REF_RE = /^[-*]\s*\[[ x]\]\s*task-\d+/i
|
|
23
|
+
const SUMMARY_HEADING_RE = /^#+\s*任务总表/
|
|
24
|
+
|
|
25
|
+
/** 渲染标准 Wave 段块(段落间空行分隔,结尾不带多余空行——由调用方控制拼接) */
|
|
26
|
+
function renderWaveBlock(waves) {
|
|
27
|
+
const L = []
|
|
28
|
+
waves.forEach((wave, i) => {
|
|
29
|
+
const sorted = [...wave].sort((a, b) =>
|
|
30
|
+
parseInt(String(a).replace(/\D/g, ''), 10) - parseInt(String(b).replace(/\D/g, ''), 10))
|
|
31
|
+
L.push(`## Wave ${i + 1}(${i === 0 ? '并行,无依赖' : '依赖前序 Wave'})`)
|
|
32
|
+
for (const t of sorted) L.push(`- ${t}`)
|
|
33
|
+
if (i < waves.length - 1) L.push('')
|
|
34
|
+
})
|
|
35
|
+
return L
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** best-effort 更新任务总表 W 列:仅认 `| task-XX | … | W1 | … |` 形态的 7+ 列模板行 */
|
|
39
|
+
function updateSummaryWaveColumn(lines, waveOf) {
|
|
40
|
+
let inSummary = false
|
|
41
|
+
let updated = 0
|
|
42
|
+
let skipped = 0
|
|
43
|
+
for (let i = 0; i < lines.length; i++) {
|
|
44
|
+
const line = lines[i]
|
|
45
|
+
if (/^#+\s/.test(line)) { inSummary = SUMMARY_HEADING_RE.test(line); continue }
|
|
46
|
+
if (!inSummary) continue
|
|
47
|
+
const m = line.match(/^\|\s*task-(\d+)\s*\|/)
|
|
48
|
+
if (!m) continue
|
|
49
|
+
const cells = line.split('|')
|
|
50
|
+
// 模板 7 列:| 编号 | 任务 | Wave | 优先级 | 依赖 | 覆盖 FR/D | 说明 | → split 后首尾空串
|
|
51
|
+
if (cells.length < 8 || !/^\s*W\d+\s*$/.test(cells[3] || '')) { skipped++; continue }
|
|
52
|
+
const taskId = `task-${String(m[1]).padStart(2, '0')}`
|
|
53
|
+
const waveIdx = waveOf.get(taskId)
|
|
54
|
+
if (waveIdx === undefined) { skipped++; continue }
|
|
55
|
+
cells[3] = ` W${waveIdx + 1} `
|
|
56
|
+
lines[i] = cells.join('|')
|
|
57
|
+
updated++
|
|
58
|
+
}
|
|
59
|
+
return { updated, skipped }
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* @param {{ changeDir: string, dryRun?: boolean }} opts
|
|
64
|
+
* @returns {{ ok: boolean, error?: string, waves?: string[][], planPath: string, dryRun: boolean,
|
|
65
|
+
* waveBlock?: string[], tableRowsUpdated?: number, tableRowsSkipped?: number,
|
|
66
|
+
* postcheck?: { errors: string[], warnings: string[] } }}
|
|
67
|
+
*/
|
|
68
|
+
export function adoptPlanWaves({ changeDir, dryRun = false }) {
|
|
69
|
+
const planPath = join(changeDir, 'plan.md')
|
|
70
|
+
if (!existsSync(planPath)) return { ok: false, error: `plan.md 不存在: ${planPath}`, planPath, dryRun }
|
|
71
|
+
const tasksDir = join(changeDir, 'tasks')
|
|
72
|
+
if (!existsSync(tasksDir)) return { ok: false, error: `tasks/ 目录不存在: ${tasksDir}`, planPath, dryRun }
|
|
73
|
+
const depMap = collectTaskDepMap(tasksDir)
|
|
74
|
+
if (depMap.size === 0) return { ok: false, error: 'tasks/ 下没有 task-NN.md 卡片,无从推导 Wave 分组', planPath, dryRun }
|
|
75
|
+
|
|
76
|
+
const { waves, error: topoError } = topoSortWaves(depMap)
|
|
77
|
+
if (topoError) return { ok: false, error: `拓扑排序失败: ${topoError}`, planPath, dryRun }
|
|
78
|
+
|
|
79
|
+
const original = readFileSync(planPath, 'utf8')
|
|
80
|
+
const lines = original.split(/\r?\n/)
|
|
81
|
+
const waveBlock = renderWaveBlock(waves)
|
|
82
|
+
const waveOf = new Map()
|
|
83
|
+
waves.forEach((ws, i) => { for (const t of ws) waveOf.set(t, i) })
|
|
84
|
+
|
|
85
|
+
const firstWaveIdx = lines.findIndex(l => WAVE_HEADING_RE.test(l))
|
|
86
|
+
if (firstWaveIdx >= 0) {
|
|
87
|
+
// Wave 段块末尾 = 最后一个 Wave 标题之后连续的任务引用行(空行容忍但不断段;
|
|
88
|
+
// 遇非 Wave 标题即止)。段内混入非引用内容 → 拒绝重写(防误删正文/表格)。
|
|
89
|
+
let endIdx = firstWaveIdx
|
|
90
|
+
for (let i = firstWaveIdx; i < lines.length; i++) {
|
|
91
|
+
if (WAVE_HEADING_RE.test(lines[i])) { endIdx = i; continue }
|
|
92
|
+
if (TASK_REF_RE.test(lines[i]) || LEGACY_TASK_REF_RE.test(lines[i])) { endIdx = i; continue }
|
|
93
|
+
if (lines[i].trim() === '') continue
|
|
94
|
+
if (/^#+\s/.test(lines[i])) break
|
|
95
|
+
return { ok: false, error: `Wave 段内含非引用内容(第 ${i + 1} 行「${lines[i].slice(0, 40)}」)——为防误删拒绝重写,请先把 Wave 段整理为纯 "- task-XX" 引用行`, planPath, dryRun }
|
|
96
|
+
}
|
|
97
|
+
lines.splice(firstWaveIdx, endIdx - firstWaveIdx + 1, ...waveBlock)
|
|
98
|
+
} else {
|
|
99
|
+
// 无显式 Wave 标题:插在「## 任务总表」前;无总表则文末追加
|
|
100
|
+
const summaryIdx = lines.findIndex(l => SUMMARY_HEADING_RE.test(l))
|
|
101
|
+
if (summaryIdx >= 0) {
|
|
102
|
+
lines.splice(summaryIdx, 0, ...waveBlock, '')
|
|
103
|
+
} else {
|
|
104
|
+
while (lines.length > 0 && lines[lines.length - 1].trim() === '') lines.pop()
|
|
105
|
+
lines.push('', ...waveBlock)
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const { updated, skipped } = updateSummaryWaveColumn(lines, waveOf)
|
|
110
|
+
const content = lines.join('\n').replace(/\n*$/, '\n')
|
|
111
|
+
|
|
112
|
+
if (dryRun) {
|
|
113
|
+
return { ok: true, waves, planPath, dryRun: true, waveBlock, tableRowsUpdated: updated, tableRowsSkipped: skipped }
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
writeFileSync(planPath, content, 'utf8')
|
|
117
|
+
// 复跑 blueprint 一致性(advisory 打印):topo 只看依赖不看文件重叠——adopt 可能把手工
|
|
118
|
+
// 安全串行收敛出同 Wave 共享路径冲突,立即暴露而非留到 execute 撞
|
|
119
|
+
let postcheck = null
|
|
120
|
+
try {
|
|
121
|
+
postcheck = validateBlueprintConsistency(changeDir)
|
|
122
|
+
} catch (e) {
|
|
123
|
+
postcheck = { errors: [`复跑校验异常: ${e?.message || e}`], warnings: [] }
|
|
124
|
+
}
|
|
125
|
+
return { ok: true, waves, planPath, dryRun: false, waveBlock, tableRowsUpdated: updated, tableRowsSkipped: skipped, postcheck }
|
|
126
|
+
}
|
package/src/quicklog.js
CHANGED
|
@@ -32,13 +32,15 @@ const sleep = (ms) => new Promise(r => setTimeout(r, ms))
|
|
|
32
32
|
* 与 rename 之间),相比旧实现的必现双进已可忽略。
|
|
33
33
|
* @param {string} lockPath 锁文件路径(建议与被保护文件同目录)
|
|
34
34
|
* @param {() => (any|Promise<any>)} fn 临界区
|
|
35
|
-
* @param {{staleMs?:number, timeoutMs?:number, retryMs?:number}} opts
|
|
35
|
+
* @param {{staleMs?:number, timeoutMs?:number, retryMs?:number, content?:string}} opts
|
|
36
|
+
* content:锁文件内容(默认 pid-randomId)。供超时报错时展示持有者语义(如 apply 锁写
|
|
37
|
+
* {pid, changeName})——仅展示用途,抢占/偷锁逻辑只比对内容一致性不解析语义。
|
|
36
38
|
*/
|
|
37
39
|
export async function withFileLock(lockPath, fn, opts = {}) {
|
|
38
|
-
const { staleMs = 30000, timeoutMs = 10000, retryMs = 50 } = opts
|
|
40
|
+
const { staleMs = 30000, timeoutMs = 10000, retryMs = 50, content = null } = opts
|
|
39
41
|
mkdirSync(dirname(lockPath), { recursive: true })
|
|
40
42
|
const start = Date.now()
|
|
41
|
-
const myId = `${process.pid}-${randomBytes(6).toString('hex')}`
|
|
43
|
+
const myId = content || `${process.pid}-${randomBytes(6).toString('hex')}`
|
|
42
44
|
let fd = null
|
|
43
45
|
// 抢锁循环
|
|
44
46
|
while (true) {
|
|
@@ -174,6 +176,9 @@ function buildPushPayloadFromRaw(rawBlock, { ql_id, author_raw, status, linked_c
|
|
|
174
176
|
payload.timestamp = parts[1] || null
|
|
175
177
|
payload.title = parts.slice(2).join('|').trim() || payload.title
|
|
176
178
|
}
|
|
179
|
+
// 顶层标签白名单(^ 行首锚定):D-004@v1 根因块嵌套四子字段(- 现象:/- 根因:/- 护栏:/- 证据: 列表行)
|
|
180
|
+
// 因「- 」前缀不匹配本正则——它们是根因块正文内的列表行续行,不是新顶层标签,经 lastLabel 挂载
|
|
181
|
+
// 进 body_sections[根因](见下方 inFiles 复位注释)。顶层四字段边界解析不受影响(R-03)。
|
|
177
182
|
const labelRe = /^(状态|关联变更|文件|审计|需求|根因|方案|结果)\s*[::]\s*(.*)$/
|
|
178
183
|
const bulletRe = /^-\s+(.*)$/
|
|
179
184
|
let inFiles = false
|
|
@@ -190,7 +195,9 @@ function buildPushPayloadFromRaw(rawBlock, { ql_id, author_raw, status, linked_c
|
|
|
190
195
|
else if (label === '关联变更') { payload.linked_changes = value.split(/[,,、+;;]/).map(s => s.trim()).filter(Boolean); inLinked = true; inFiles = false }
|
|
191
196
|
else if (label === '文件') { if (value) payload.files.push(...value.split(/[,,、+;;]/).map(s => s.trim()).filter(Boolean).map(p => ({ path: p, note: null }))); inFiles = true; inLinked = false }
|
|
192
197
|
else if (label === '审计') { /* D-8 advisory 行:只进 raw_block 不进结构化段(body_sections 不扩 schema),并阻断续行误挂 */ lastLabel = null; inFiles = false; inLinked = false }
|
|
193
|
-
|
|
198
|
+
// 进入 需求/根因/方案/结果 字段块须关闭 inFiles/inLinked 续行模式(task-07 / D-004@v1):否则根因块内
|
|
199
|
+
// 嵌套子字段列表行(- 现象:… 等)会命中下方「文件 bullet」分支被劫进 payload.files,从根因正文截断丢失。
|
|
200
|
+
else { payload.body_sections[label] = value; lastLabel = label; inFiles = false; inLinked = false }
|
|
194
201
|
continue
|
|
195
202
|
}
|
|
196
203
|
if (bulletRe.test(stripped) && inFiles) {
|
|
@@ -265,10 +272,18 @@ export function deriveTitleFromLinkedChange(specBase, change) {
|
|
|
265
272
|
try { content = readFileSync(join(specBase, 'changes', change, f), 'utf8') } catch { continue }
|
|
266
273
|
const m = content.match(/^#\s+(.+?)\s*$/m)
|
|
267
274
|
if (!m) continue
|
|
268
|
-
|
|
269
|
-
// 标题惯例「# 提案书(Proposal)— <desc>」「# 设计文档(Design)— <desc>」→ 取破折号后的 desc
|
|
275
|
+
let raw = m[1].trim()
|
|
276
|
+
// 标题惯例「# 提案书(Proposal)— <desc>」「# 设计文档(Design)— <desc>」→ 取破折号后的 desc。
|
|
277
|
+
// 固定前缀显式剥(坑 linked-task-placeholder-title,2026-08-22 实证:标题「# 提案书(Proposal):
|
|
278
|
+
// xxx」或「# 提案书(Proposal)」无破折号时 dash 匹配失败,整串前缀落进 tasks.md 追加行)——
|
|
279
|
+
// 先剥固定模板词(含全/半角冒号分隔形态),再剥破折号;剥完为空(纯模板标题)继续找下一文档
|
|
280
|
+
for (const prefix of [/^提案书(Proposal)\s*[::—\-]?\s*/, /^设计文档(Design)\s*[::—\-]?\s*/, /^Proposal\s*[::—\-]?\s*/i, /^Design\s*[::—\-]?\s*/i]) {
|
|
281
|
+
raw = raw.replace(prefix, '').trim()
|
|
282
|
+
}
|
|
283
|
+
if (!raw) continue
|
|
270
284
|
const dash = raw.match(/[—-]\s*(.+)$/)
|
|
271
|
-
|
|
285
|
+
const title = dash ? dash[1].trim() : raw
|
|
286
|
+
if (title) return title
|
|
272
287
|
}
|
|
273
288
|
return ''
|
|
274
289
|
}
|
|
@@ -324,6 +339,37 @@ export function parseFileNotes(raw) {
|
|
|
324
339
|
.filter(Boolean)
|
|
325
340
|
}
|
|
326
341
|
|
|
342
|
+
/**
|
|
343
|
+
* 校验 --file-notes 原文格式(坑 quick-file-notes-format-silent,2026-08-24 用户反馈二期②:
|
|
344
|
+
* 多文件分隔符是 `||` 双竖线,写错成 `|`/`,` 时整段被当成第一个文件的括注静默落盘,只能手工修
|
|
345
|
+
* QUICKLOG)。命令入口 fail-fast 用:每个 `||` 段必须是 `path::括注`(`::` 必填,括注可空);
|
|
346
|
+
* 括注内再出现 `::` 视为疑似漏写 `||` 分隔符。返回问题清单供调用方拼报错。
|
|
347
|
+
* @param {string} raw
|
|
348
|
+
* @returns {{ ok: boolean, problems: Array<{ seg: string, reason: string, hint: string }> }}
|
|
349
|
+
*/
|
|
350
|
+
export function validateFileNotesFormat(raw) {
|
|
351
|
+
const problems = []
|
|
352
|
+
if (!raw) return { ok: true, problems }
|
|
353
|
+
for (const seg of String(raw).split('||')) {
|
|
354
|
+
const s = seg.trim()
|
|
355
|
+
if (!s) continue
|
|
356
|
+
const idx = s.indexOf('::')
|
|
357
|
+
if (idx === -1) {
|
|
358
|
+
problems.push({ seg: s, reason: '缺 `::` 路径/括注分隔符', hint: '格式为 path::括注;不要括注也要写 path::(括注可空)' })
|
|
359
|
+
continue
|
|
360
|
+
}
|
|
361
|
+
const path = s.slice(0, idx).trim()
|
|
362
|
+
if (!path) {
|
|
363
|
+
problems.push({ seg: s, reason: '`::` 前路径为空', hint: '格式为 path::括注' })
|
|
364
|
+
continue
|
|
365
|
+
}
|
|
366
|
+
if (s.slice(idx + 2).includes('::')) {
|
|
367
|
+
problems.push({ seg: s, reason: '括注内出现 `::`', hint: '疑似多文件漏写 `||` 分隔符(双竖线)——多文件写法:path1::括注1 || path2::括注2' })
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
return { ok: problems.length === 0, problems }
|
|
371
|
+
}
|
|
372
|
+
|
|
327
373
|
// 结果块必填字段(quick step3 --done --output 的结构化结果模板,见 src/stages/quick.js)。
|
|
328
374
|
// 4 个必填字段标签从 manifest 同源(stage-contract-spec.js quick.result-labels),prompt 事前契约与本校验单源。
|
|
329
375
|
const RESULT_REQUIRED_LABELS = getRule('quick.result-labels').data.literals
|
|
@@ -336,6 +382,8 @@ const RESULT_REQUIRED_LABELS = getRule('quick.result-labels').data.literals
|
|
|
336
382
|
* @returns {{ ok: boolean, missing: string[] }} ok=true 通过;missing=缺失的字段名列表
|
|
337
383
|
*/
|
|
338
384
|
export function validateQuickResult(text) {
|
|
385
|
+
// 子串包含判定:根因块含嵌套四子字段列表行(- 现象:… 等,task-07 / D-004@v1 合法形态)时,
|
|
386
|
+
// 顶层四标签仍须齐备(模板契约),嵌套行不影响判定(「- 」前缀不构成顶层标签,亦不缺字段)。
|
|
339
387
|
const missing = RESULT_REQUIRED_LABELS.filter(label => !String(text || '').includes(label))
|
|
340
388
|
return { ok: missing.length === 0, missing }
|
|
341
389
|
}
|
|
@@ -478,6 +526,9 @@ async function checkTaskCheckbox(specBase, change, qlId) {
|
|
|
478
526
|
// 前缀」或正则 split(/(?=需求:|根因:|方案:|结果:)/))因前导是「/|( 等非边界字符而跳过。严格失败
|
|
479
527
|
// (如真实标签前导是「,」这类弱标点)退回宽松顺序扫描(上一标签之后首次出现)。缺标签返回 null → 落单行
|
|
480
528
|
// 兜底(--done 契约校验仍会拦缺字段)。残余边界:正文引用标签且前导恰好是空白/句末标点时仍可能错位。
|
|
529
|
+
// task-07 / D-004@v1 声明:本切分只作用于「单行四字段压缩归一」路径,嵌套四子字段(- 现象:/- 根因:/
|
|
530
|
+
// - 护栏:/- 证据: 列表行)必须以多行列表行形态写入根因块正文——按序扫描先命中顶层「根因:」标签,
|
|
531
|
+
// 其后的嵌套「- 根因:」等字样落在根因字段正文内不构成新边界,无需改动三个边界函数即天然兼容(Grill C-15)。
|
|
481
532
|
function isFieldBoundary(body, idx) {
|
|
482
533
|
if (idx <= 0) return true
|
|
483
534
|
const prev = body[idx - 1]
|
|
@@ -559,6 +610,9 @@ function flipEntryInContent(content, qlId, result, changedFiles = [], fileNotes
|
|
|
559
610
|
if (segs) body = segs.join('\n')
|
|
560
611
|
}
|
|
561
612
|
// 多行结果(结构化 需求/根因/方案/结果 字段块):逐行插入为独立字段行。
|
|
613
|
+
// 根因块嵌套四子字段(- 现象:/- 根因:/- 护栏:/- 证据: 列表行,D-004@v1 postmortem 形态)属
|
|
614
|
+
// 根因块正文:在此随多行结果逐行原样落盘(带「- 」前缀、保序含换行),不被字段边界扫描误切——
|
|
615
|
+
// 单行归一切分只认「- 」前缀之外的需求:/根因:/方案:/结果: 标签,嵌套行天然不参与边界判定。
|
|
562
616
|
// 单行结果:保持「结果:<一句话>」一行,向后兼容简单用例。
|
|
563
617
|
const resultLines = /\r?\n/.test(body)
|
|
564
618
|
? body.split(/\r?\n/).filter(l => l.trim() !== '')
|
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, PLATFORM_MANAGED_FILENAME } from './shared.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'
|
|
27
27
|
import { resolveQuickLinkedChanges } from './quick-audit.js'
|
|
28
28
|
import { outputStep } from './prompt.js'
|
|
29
29
|
import { completeStep, skipStep, waitStep, continueStep } from './complete.js'
|
|
@@ -163,6 +163,9 @@ sillyspec run ${stageName}${stage && stage.title ? ` — ${stage.title}` : ''}${
|
|
|
163
163
|
* sillyspec run <stage> 主命令
|
|
164
164
|
*/
|
|
165
165
|
export async function runCommand(args, cwd, specDir = null, opts = {}) {
|
|
166
|
+
// 原始 cwd 快照(下方 cwd 纠正前):agent 在子目录启动时,其会话 transcript 挂在子目录
|
|
167
|
+
// 对应的 harness 项目目录下,agent-log 探测须用原始 cwd(纠正后的是项目根,探测会扑空)
|
|
168
|
+
const invokedCwd = cwd
|
|
166
169
|
// W1-B: run <stage> 暂不支持 --json。之前 --json 进 knownFlags 白名单却从不读取(静默吞),
|
|
167
170
|
// agent 按 gate/derive 契约期望 envelope 实则拿到混合人类文本。显式 fail-fast 杜绝歧义;
|
|
168
171
|
// 完整 run envelope 待 outputStep 重构(W6)后支持(prompt + nextAction 结构化)。
|
|
@@ -316,19 +319,27 @@ export async function runCommand(args, cwd, specDir = null, opts = {}) {
|
|
|
316
319
|
try {
|
|
317
320
|
const { readFileSync } = await import('fs')
|
|
318
321
|
const saved = JSON.parse(readFileSync(platformOptsFile, 'utf8'))
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
322
|
+
// 自指指针免疫(FR-4,变更 2026-08-23-repo-native-spec-backfill):saved.specRoot 经
|
|
323
|
+
// realpath 解析回本地 .sillyspec(repo-native junction 回环,旧模板投毒残留)→ 整体
|
|
324
|
+
// 忽略恢复(specRoot/runtimeRoot/workspaceId/scanRunId 全不回填,platformOpts 保持
|
|
325
|
+
// 无平台参数状态),按本地模式运行——内置 sync 走本地链路,specRoot 保持本地取值链。
|
|
326
|
+
if (isSelfReferentialSpecRoot(cwd, saved.specRoot)) {
|
|
327
|
+
console.warn(`⚠️ 检测到自指平台指针(repo-native junction 回环,specRoot 指回本地 .sillyspec),已忽略并按本地模式运行: ${platformOptsFile}`)
|
|
328
|
+
} else {
|
|
329
|
+
if (saved.specRoot) platformOpts.specRoot = saved.specRoot
|
|
330
|
+
if (saved.runtimeRoot) platformOpts.runtimeRoot = saved.runtimeRoot
|
|
331
|
+
if (saved.workspaceId) platformOpts.workspaceId = saved.workspaceId
|
|
332
|
+
if (saved.scanRunId) platformOpts.scanRunId = saved.scanRunId
|
|
333
|
+
// 平台模式 fail-fast:文件存在但缺少 specRoot
|
|
334
|
+
if (!platformOpts.specRoot && !platformOpts.runtimeRoot) {
|
|
335
|
+
console.error(`❌ 平台模式参数文件存在但缺少 specRoot/runtimeRoot: ${platformOptsFile}`)
|
|
336
|
+
console.error(' 可能原因:platform-scan.json 损坏或写入不完整')
|
|
337
|
+
console.error(' 解决:重新运行首次 scan 并传入 --spec-root')
|
|
338
|
+
process.exit(2) // 环境错(平台文件损坏)→ exit 2
|
|
339
|
+
}
|
|
340
|
+
// 恢复成功:更新 specRoot(初始值可能是 cwd/.sillyspec,恢复后应为真实 specDir)
|
|
341
|
+
specRoot = platformOpts.specRoot || specRoot
|
|
329
342
|
}
|
|
330
|
-
// 恢复成功:更新 specRoot(初始值可能是 cwd/.sillyspec,恢复后应为真实 specDir)
|
|
331
|
-
specRoot = platformOpts.specRoot || specRoot
|
|
332
343
|
} catch (e) {
|
|
333
344
|
console.error(`❌ 平台模式参数文件读取失败: ${platformOptsFile}`)
|
|
334
345
|
console.error(` 错误: ${e.message}`)
|
|
@@ -346,12 +357,20 @@ export async function runCommand(args, cwd, specDir = null, opts = {}) {
|
|
|
346
357
|
if (!platformOpts.specRoot && !platformOpts.runtimeRoot && !platformFileExists) {
|
|
347
358
|
const decl = checkPlatformManaged(cwd)
|
|
348
359
|
if (decl) {
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
//
|
|
353
|
-
|
|
354
|
-
|
|
360
|
+
// 陈旧自指声明降级(FR-4,变更 2026-08-23-repo-native-spec-backfill):decl.specRoot 经
|
|
361
|
+
// realpath 解析回本地 .sillyspec(repo-native junction 投毒残留、指针已被清理)→ 视为
|
|
362
|
+
// 陈旧声明,warn 后按本地模式继续(不 exit 1)——声明描述的"平台接管"物理上指向本地
|
|
363
|
+
// .sillyspec,无跨库状态分裂风险;非自指声明维持 fail-closed 原样(状态保护阻断)。
|
|
364
|
+
if (isSelfReferentialSpecRoot(cwd, decl.specRoot)) {
|
|
365
|
+
console.warn(`⚠️ 检测到陈旧的自指平台接管声明(repo-native junction 回环,原 specRoot 指回本地 .sillyspec),已降级并按本地模式运行;可 sillyspec platform disconnect 清理残留声明: ${join(cwd, PLATFORM_MANAGED_FILENAME)}`)
|
|
366
|
+
} else {
|
|
367
|
+
console.error(`❌ 平台接管声明生效:本项目已由平台托管(原 specRoot: ${decl.specRoot || '(未记录)'}),但恢复指针缺失,拒绝静默回退本地模式。`)
|
|
368
|
+
console.error(` 声明文件: ${join(cwd, PLATFORM_MANAGED_FILENAME)}`)
|
|
369
|
+
console.error(' 恢复:① 重跑平台 scan/init(带 --spec-root)重建指针;② 确认不再使用平台:sillyspec platform disconnect(删除接管声明);③ 显式 --spec-dir <路径> 临时指定目录。')
|
|
370
|
+
// exit(1) 而非邻近环境错的 exit(2):这是"状态保护阻断"(对齐 PointerUnreachableError
|
|
371
|
+
// 顶层 catch 语义),非"用法/环境错"——见 design.md §5.3 v2 复审观察 (a)
|
|
372
|
+
process.exit(1)
|
|
373
|
+
}
|
|
355
374
|
}
|
|
356
375
|
}
|
|
357
376
|
// 持久化 platformOpts
|
|
@@ -398,7 +417,11 @@ export async function runCommand(args, cwd, specDir = null, opts = {}) {
|
|
|
398
417
|
// ⚠️ 同 init.js:必须保护真实资产(changes/、projects/、sillyspec.db)。
|
|
399
418
|
// 只在「首次」执行一次——用 cwd 下的 .sillyspec-platform-cleaned 标记文件记录已处理,
|
|
400
419
|
// 后续每次 run 直接跳过,避免重复检查 + 红叉噪声(此清理不阻塞流程、不动真实资产)。
|
|
401
|
-
|
|
420
|
+
// 自指守卫(FR-4,变更 2026-08-23-repo-native-spec-backfill):显式自指 --spec-root
|
|
421
|
+
// (repo-native junction 回环)下 cwd/.sillyspec 与 specRoot 同物理目录——它就是平台
|
|
422
|
+
// 规范目录本体,绝非"旧版本残留";跳过整个清理决策(含 marker 写入,防后续真平台
|
|
423
|
+
// 接入被误标记已清理)。否则 rmSync 会删掉 repo-native 唯一真理源并留下悬空链接。
|
|
424
|
+
if (platformOpts.specRoot && !isSelfReferentialSpecRoot(cwd, platformOpts.specRoot)) {
|
|
402
425
|
const legacyDir = join(cwd, '.sillyspec')
|
|
403
426
|
const cleanedMarker = join(cwd, '.sillyspec-platform-cleaned')
|
|
404
427
|
if (!existsSync(cleanedMarker)) {
|
|
@@ -466,6 +489,20 @@ export async function runCommand(args, cwd, specDir = null, opts = {}) {
|
|
|
466
489
|
const inputValue = getFlagValue('--input')
|
|
467
490
|
if (inputValue !== null) inputText = inputValue
|
|
468
491
|
|
|
492
|
+
// quick 位置参数 → 任务描述(与 auto 模式建议的 `sillyspec run quick "需求"` 用法一致)。
|
|
493
|
+
// 旧行为静默忽略裸 token(QUICKLOG 标题回退占位);现显式消费——跳过值类 flag 的值
|
|
494
|
+
// (--files src/a.js 的 src/a.js 不是位置参数),取第一个真位置 token。配合下方
|
|
495
|
+
// 「新会话必须带描述」启动门(坑 quick-no-input-placeholder-title)。
|
|
496
|
+
if (stageName === 'quick' && inputText === null) {
|
|
497
|
+
for (let i = 0; i < flags.length; i++) {
|
|
498
|
+
const t = flags[i]
|
|
499
|
+
if (typeof t !== 'string') continue
|
|
500
|
+
if (t.startsWith('--')) { if (VALUE_FLAGS.has(t)) i++; continue }
|
|
501
|
+
inputText = t
|
|
502
|
+
break
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
|
|
469
506
|
// 解析 --linked-changes <a,b|none>(quick 专用:显式声明关联变更,CI/脚本友好)
|
|
470
507
|
// 与 --change 解耦:--linked-changes 语义清晰(关联变更),不与「指定变更名」混淆。
|
|
471
508
|
// null = 未指定(走持久化/交互/兼容回退);[] = 显式 none(不关联);[...] = 显式列表
|
|
@@ -520,6 +557,7 @@ export async function runCommand(args, cwd, specDir = null, opts = {}) {
|
|
|
520
557
|
const QUICK_SID_RE = /^quick-[0-9a-f]{8}$/
|
|
521
558
|
let quickSessionId = null
|
|
522
559
|
let quickFallbackUsed = false // Q7: --done 未带 --change 时 fallback 读 current-quick-run-id 命中(区别于显式 --change quick-<hex>)
|
|
560
|
+
let quickSidFresh = false // 本轮刚生成全新 sessionId(非 --change 精确恢复 / 非 fallback)——供下方「新会话必须带 --input」门判定
|
|
523
561
|
if (stageName === 'quick') {
|
|
524
562
|
// 1373-1376 已把 quick 的 --change 值清进 linkedChanges、changeName 置 null。
|
|
525
563
|
// 此处回看 --change 原始值:若恰好是单个 quick-<8hex> → 识别为本会话 sessionId(精确恢复),
|
|
@@ -548,6 +586,7 @@ export async function runCommand(args, cwd, specDir = null, opts = {}) {
|
|
|
548
586
|
}
|
|
549
587
|
if (!quickSessionId) {
|
|
550
588
|
quickSessionId = 'quick-' + randomUUID().slice(0, 8)
|
|
589
|
+
quickSidFresh = true
|
|
551
590
|
}
|
|
552
591
|
changeName = quickSessionId
|
|
553
592
|
} else {
|
|
@@ -556,6 +595,43 @@ export async function runCommand(args, cwd, specDir = null, opts = {}) {
|
|
|
556
595
|
}
|
|
557
596
|
}
|
|
558
597
|
|
|
598
|
+
// ── 本地 agent 会话日志登记 + 上报(平台会话展示用,best-effort)──
|
|
599
|
+
// 平台/daemon 模式下 SillyHub 只能看到 CLI 阶段信息,看不到本地 agent 的实际执行日志。
|
|
600
|
+
// 在此探测当前 agent 环境(Claude Code / Codex / ZCode transcript / SILLYSPEC_AGENT_LOG
|
|
601
|
+
// 覆盖),把日志本地路径登记进 <runtimeRoot>/agent-session-log.json(留底)并立即
|
|
602
|
+
// REST 上报平台(POST /api/agent-logs,与进度上报同风格;协议见
|
|
603
|
+
// docs/platform-agent-log-protocol.md)。探测不到不发不写;任何失败只 warn 一行,
|
|
604
|
+
// 绝不阻断 run 主流程。
|
|
605
|
+
// 位置(2026-08-23-agent-activity-sessions task-01):必须在上方 changeName/quickSessionId
|
|
606
|
+
// 解析完成之后——上报携带会话化上下文 context:entry 级 changeKey/quickId(平台按
|
|
607
|
+
// (workspace, harness, entry.ctx) 聚合建会话;quick 会话 quickId 优先且与 changeKey 互斥)
|
|
608
|
+
// + body 级 hubSessionId(daemon 派发平台会话时注入 env SILLYHUB_SESSION_ID,非空才带)。
|
|
609
|
+
// 两段解析均在顶层作用域,此处统一算一次 context 两值皆可达。
|
|
610
|
+
try {
|
|
611
|
+
const isQuickSession = stageName === 'quick' && Boolean(quickSessionId)
|
|
612
|
+
const hubSessionIdEnv = typeof process.env.SILLYHUB_SESSION_ID === 'string' ? process.env.SILLYHUB_SESSION_ID.trim() : ''
|
|
613
|
+
const agentLogContext = {
|
|
614
|
+
hubSessionId: hubSessionIdEnv || null,
|
|
615
|
+
// quick 会话 → quickId(quick-<8hex> 完整原样,不剥前缀);普通场景 → changeKey(--change 值,无则 null)
|
|
616
|
+
changeKey: isQuickSession ? null : (changeName || null),
|
|
617
|
+
quickId: isQuickSession ? quickSessionId : null,
|
|
618
|
+
}
|
|
619
|
+
const { recordAgentLogInvocation } = await import('../agent-session-log.js')
|
|
620
|
+
const r = await recordAgentLogInvocation({
|
|
621
|
+
cwd,
|
|
622
|
+
invokedCwd,
|
|
623
|
+
platformOpts,
|
|
624
|
+
specBase,
|
|
625
|
+
context: agentLogContext,
|
|
626
|
+
// 只记 flag 名不记值(--output 等 flag 值是 agent 工作文本,不进产物)
|
|
627
|
+
command: [stageName, ...flags.filter(t => typeof t === 'string' && t.startsWith('--'))].join(' '),
|
|
628
|
+
})
|
|
629
|
+
if (r && r.isNew && r.latestLogPath) {
|
|
630
|
+
const pushNote = r.pushed === true ? '已上报平台' : r.pushed === false ? '上报失败(本地已留底)' : '本地留底'
|
|
631
|
+
console.log(`📄 本地 agent 日志已登记(${pushNote}): ${r.latestLogPath}`)
|
|
632
|
+
}
|
|
633
|
+
} catch { /* best-effort:登记失败不影响 run 主流程 */ }
|
|
634
|
+
|
|
559
635
|
// quick 关联变更存在性守卫(坑 quick-change-phantom-linked):quick 的 --change/--linked-changes
|
|
560
636
|
// 被解析为关联变更后静默接受不存在的名字(quick 不建变更,链接幻影名必是笔误/语义误用——想给
|
|
561
637
|
// 会话起名却传了 --change),后果是 QUICKLOG 挂悬空关联污染 change 关联图谱 + --done 时 sessionId
|
|
@@ -594,10 +670,23 @@ export async function runCommand(args, cwd, specDir = null, opts = {}) {
|
|
|
594
670
|
// 解析 --file-notes "path::括注 || path::括注"(quick 专用:QUICKLOG「文件:」行落盘多行括注)。
|
|
595
671
|
// 旁路注入 quicklog.js(per-process setter),不经 complete 收尾路径透传(避碰多 agent 并发改的收尾热点)。
|
|
596
672
|
// completeQuicklogEntry 读后即清;不传 → '' → 回填审计到的实际文件单行(向后兼容)。
|
|
673
|
+
// 格式 fail-fast(坑 quick-file-notes-format-silent,2026-08-24):分隔符写错(`|`/`,` 等)时
|
|
674
|
+
// 整段会被静默挤进第一个文件的括注、QUICKLOG 落盘后只能手工修——解析处直接拦。
|
|
597
675
|
let quickFileNotes = ''
|
|
598
676
|
const fileNotesValue = getFlagValue('--file-notes')
|
|
599
677
|
if (fileNotesValue !== null) {
|
|
600
678
|
quickFileNotes = fileNotesValue
|
|
679
|
+
const { validateFileNotesFormat } = await import('../quicklog.js')
|
|
680
|
+
const fv = validateFileNotesFormat(quickFileNotes)
|
|
681
|
+
if (!fv.ok) {
|
|
682
|
+
console.error(`❌ --file-notes 格式不对(${fv.problems.length} 处),已拒绝执行(旧行为会静默挤错进 QUICKLOG,只能手工修):`)
|
|
683
|
+
for (const p of fv.problems.slice(0, 5)) {
|
|
684
|
+
console.error(` 「${p.seg.slice(0, 80)}」${p.reason}——${p.hint}`)
|
|
685
|
+
}
|
|
686
|
+
if (fv.problems.length > 5) console.error(` …等共 ${fv.problems.length} 处`)
|
|
687
|
+
console.error(' 正确格式: --file-notes "path1::括注1 || path2::括注2"(`||` 双竖线分隔多文件,`::` 分隔路径与括注)')
|
|
688
|
+
process.exit(2) // 用法错(--file-notes 格式非法)→ exit 2
|
|
689
|
+
}
|
|
601
690
|
}
|
|
602
691
|
setQuickFileNotes(quickFileNotes)
|
|
603
692
|
|
|
@@ -667,6 +756,22 @@ export async function runCommand(args, cwd, specDir = null, opts = {}) {
|
|
|
667
756
|
process.exit(0)
|
|
668
757
|
}
|
|
669
758
|
|
|
759
|
+
// 坑 quick-no-input-placeholder-title(2026-08-24 用户反馈二期①):全新 quick 会话(上方
|
|
760
|
+
// 刚生成新 sessionId)不带 --input 且无 --linked-changes 可取标题 → 拒绝启动(exit 2)。
|
|
761
|
+
// 旧行为只 warn 提示「建议放弃重启」,占位标题「(quick 任务)」已落盘、只能 reset 重来——
|
|
762
|
+
// 语义标题要拖到最终 --done 才回填,平台「快速修复」列表全程隐藏。此刻零沉没成本(progress
|
|
763
|
+
// 行/QUICKLOG/guard 均未创建),直接拒掉最便宜。放在 --help 短路之后(help 查询不该被拦)、
|
|
764
|
+
// 任何会话副作用之前。精确恢复(--change quick-<hex>)与 done-like(--done/--reset/--cancel/
|
|
765
|
+
// --status/--skip/--reopen)不受限;--linked-changes 启动可从关联变更 proposal/design 提取标题。
|
|
766
|
+
if (stageName === 'quick' && quickSidFresh
|
|
767
|
+
&& !(isDone || isStatus || isSkip || isReset || isReopen || isCancel)
|
|
768
|
+
&& !inputText && !(Array.isArray(linkedChanges) && linkedChanges.length > 0)) {
|
|
769
|
+
console.error('❌ 新 quick 会话必须带 --input "<一句话任务描述>"(或 --linked-changes <a,b> 从关联变更取标题)。')
|
|
770
|
+
console.error(' 不带描述启动会落「(quick 任务)」占位标题:平台「快速修复」列表隐藏占位条目、语义标题要拖到最终 --done 才回填,长任务全程不可见。')
|
|
771
|
+
console.error(' 用法: sillyspec run quick --input "<一句话任务描述>" [--linked-changes <a,b>] [--files <...>]')
|
|
772
|
+
process.exit(2) // 用法错(新会话无任务描述)→ exit 2
|
|
773
|
+
}
|
|
774
|
+
|
|
670
775
|
const isAuxiliary = auxiliaryStages.includes(stageName)
|
|
671
776
|
// scan 元数据追踪(存储在 stageData.scanMeta 中,completeStep 通过 progress 访问)
|
|
672
777
|
|
|
@@ -1076,6 +1181,20 @@ export async function runCommand(args, cwd, specDir = null, opts = {}) {
|
|
|
1076
1181
|
progress = pm.read(cwd, effectiveChange) || progress
|
|
1077
1182
|
}
|
|
1078
1183
|
|
|
1184
|
+
// --file-notes 非末步静默忽略前置 warn(坑 quick-file-notes-nonfinal-ignored,2026-08-22 实证:
|
|
1185
|
+
// step2 --done 传 --file-notes 被静默丢——CLI 短进程,注入随进程结束即丢,白传一轮)。
|
|
1186
|
+
// 放此处:ensureStageSteps 之后 progress 已就绪可判步骤状态;仅 quick + 带了 --file-notes +
|
|
1187
|
+
// 非「唯一 pending 的末步 --done」时提示。末步 --done 是消费点,不提示。
|
|
1188
|
+
if (quickFileNotes !== '' && stageName === 'quick' && !isStatus) {
|
|
1189
|
+
try {
|
|
1190
|
+
const qSteps = progress?.stages?.quick?.steps
|
|
1191
|
+
const pendingCount = Array.isArray(qSteps) ? qSteps.filter(st => ['pending', 'in-progress'].includes(st?.status)).length : 0
|
|
1192
|
+
if (!isDone || pendingCount > 1) {
|
|
1193
|
+
console.warn(`⚠️ --file-notes 本次不会生效:它只在 quick 末步(暂存和更新记录)--done 时随收尾消费——CLI 是短进程,本次注入随进程结束即丢。请在末步 --done 时连同 --output 一起传。`)
|
|
1194
|
+
}
|
|
1195
|
+
} catch { /* 判定失败不提示(fail-open) */ }
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1079
1198
|
// --status
|
|
1080
1199
|
if (isStatus) {
|
|
1081
1200
|
// D9:状态展示末尾附「下一步该跑什么」。pm 持有状态机,按 progress 实际进度算出精确命令;
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
import { basename, join, resolve, relative, isAbsolute } from 'node:path'
|
|
26
26
|
import { existsSync, readdirSync, readFileSync, writeFileSync, mkdirSync, unlinkSync, rmSync } from 'node:fs'
|
|
27
27
|
import { renameSyncRetry, writeAtomicSync } from '../fs-atomic.js'
|
|
28
|
-
import { resolveChangeDir, resolveQuickSessionsDir, safeGit, auditQuickCompletion, triggerSync, isQuickMetadata, resolveRuntimeRoot } from './shared.js'
|
|
28
|
+
import { resolveChangeDir, resolveQuickSessionsDir, safeGit, auditQuickCompletion, triggerSync, isQuickMetadata, resolveRuntimeRoot, collectOtherQuickSessionDeclarations } from './shared.js'
|
|
29
29
|
import { detectConcurrentChanges, formatConcurrentWarning, resolveConcurrentAnchor } from './concurrent-detect.js'
|
|
30
30
|
import { stageRegistry } from '../stages/index.js'
|
|
31
31
|
import { SCAN_STATUS, POINTER_STATUS } from '../constants.js'
|
|
@@ -431,7 +431,11 @@ export async function handleArchiveConfirmStep({ stageName, steps, currentIdx, c
|
|
|
431
431
|
console.log('⚠️ 请添加 --confirm 确认归档,例如:sillyspec run archive --done --confirm --output "确认归档"')
|
|
432
432
|
return { stageCompleted: false, currentIdx, nextPendingIdx: currentIdx }
|
|
433
433
|
}
|
|
434
|
-
|
|
434
|
+
// 主仓互斥锁(坑 main-repo-no-mutex 二批):archiveChangeDirectory 改主仓共享状态(目录
|
|
435
|
+
// rename + 共享 index 的 git add + marker 删除 + worktree 清理),与并行会话的 apply/cleanup
|
|
436
|
+
// 互踩。exit 钩子兜底:其内部 guard 失败 exit(1) 时锁也会被清(见 withMainRepoLock)。
|
|
437
|
+
const { withMainRepoLock } = await import('../worktree-apply.js')
|
|
438
|
+
const archivedDir = await withMainRepoLock(cwd, changeName, 'archive-finalize', () => archiveChangeDirectory(pm, cwd, progress, specBase, platformOpts))
|
|
435
439
|
if (archivedDir && existsSync(archivedDir)) {
|
|
436
440
|
// 内存快照同步(坑 archive-progress-show-stale,2026-08-21 实证):archiveChangeDirectory 内
|
|
437
441
|
// unregisterChange 已在 DB 写 current_stage='archive'(终态一致化),但本进程 progress 是命令
|
|
@@ -863,6 +867,11 @@ export async function handleQuickStageCompletion({ stageName, steps, currentIdx,
|
|
|
863
867
|
// (平台模式 resolveSpecDir(cwd) 会 miss specRoot 静默丢信号,plan 审查 gap-5)
|
|
864
868
|
specBase,
|
|
865
869
|
projectName: progress?.project || null,
|
|
870
|
+
// 他者 active 会话声明索引(坑 foreign-session-declared-false-block):多 agent 并发时
|
|
871
|
+
// 并行会话 --files 声明的文件退栈归该会话审计,不再误拦本会话。与上方 guard 读取同源
|
|
872
|
+
// (resolveQuickSessionsDir 同参,平台模式 runtimeRoot 对齐);fail-open:采集失败 → 空
|
|
873
|
+
// → 回到无豁免现状。
|
|
874
|
+
otherSessionsDeclared: collectOtherQuickSessionDeclarations(platformOpts, specBase, changeName),
|
|
866
875
|
}
|
|
867
876
|
review = await auditQuickCompletion(cwd, mergedGuard, { isConfirm: confirm })
|
|
868
877
|
printQuickAuditReview(review)
|
|
@@ -913,8 +922,9 @@ export async function handleQuickStageCompletion({ stageName, steps, currentIdx,
|
|
|
913
922
|
const isLastStep = currentIdx === steps.length - 1
|
|
914
923
|
if (isLastStep && !outputText) {
|
|
915
924
|
console.error('\n❌ quick 最后一步 --done 必须带 --output(四字段结果模板)。')
|
|
916
|
-
console.error(' --output 是 QUICKLOG「结果:」归档的唯一来源。补全后重跑 --done
|
|
917
|
-
console.error(' sillyspec run quick --done --change <changeName> --
|
|
925
|
+
console.error(' --output 是 QUICKLOG「结果:」归档的唯一来源。补全后重跑 --done(不丢进度),推荐四参数形式(CLI 自动合成,无嵌套冒号事故面):')
|
|
926
|
+
console.error(' sillyspec run quick --done --change <changeName> --req "一句话语义化短标题" --cause "为什么这样改" --solution "怎么改的" --result "验证情况(测试数 / lint / typecheck / 部署状态)"')
|
|
927
|
+
console.error(' 兼容旧形式:--output "需求:… 根因:… 方案:… 结果:…"')
|
|
918
928
|
steps[currentIdx].status = 'pending'
|
|
919
929
|
steps[currentIdx].completedAt = null
|
|
920
930
|
process.exit(1)
|
|
@@ -924,7 +934,9 @@ export async function handleQuickStageCompletion({ stageName, steps, currentIdx,
|
|
|
924
934
|
if (!resultCheck.ok) {
|
|
925
935
|
console.error('\n' + getRule('quick.result-labels').failMessage.replaceAll('${missing}', resultCheck.missing.join('、')))
|
|
926
936
|
console.error(` --output 是 QUICKLOG「结果:」归档的唯一来源,四个标签必须放在 --output 里(不是 --input)。`)
|
|
927
|
-
console.error(` 补全后重跑 --done
|
|
937
|
+
console.error(` 补全后重跑 --done(不丢进度)。推荐四参数形式(CLI 自动合成结构化模板,避免旧形式嵌套全角冒号被拆分判定缺字段):`)
|
|
938
|
+
console.error(` sillyspec run quick --done --change <changeName> --req "一句话语义化短标题(即 QUICKLOG 条目标题)" --cause "为什么这样改(纯新增/样式则写「无,纯新增/纯样式」)" --solution "怎么改的" --result "验证情况(测试数 / lint / typecheck / 部署状态)"`)
|
|
939
|
+
console.error(` 或照抄旧形式模板:`)
|
|
928
940
|
console.error(` sillyspec run quick --done --change <changeName> --output "需求:用户/任务要什么`)
|
|
929
941
|
console.error(` 根因:为什么这样改(纯新增/样式则写「无,纯新增/纯样式」)`)
|
|
930
942
|
console.error(` 方案:怎么改的`)
|
|
@@ -1331,22 +1343,39 @@ export async function handleExecuteWorktreeCleanup({ stageName, changeName, cwd
|
|
|
1331
1343
|
} else if (meta.mode === 'native-worktree') {
|
|
1332
1344
|
console.log('🔗 Worktree: kept (外部隔离环境)');
|
|
1333
1345
|
} else {
|
|
1334
|
-
// in-place 模式不再短路:cleanup 现在能安全处理 in-place(只清 meta
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
const cleanResult =
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1346
|
+
// in-place 模式不再短路:cleanup 现在能安全处理 in-place(只清 meta,不碰主工作区)。
|
|
1347
|
+
// 主仓互斥锁(坑 main-repo-no-mutex 二批):检查+清理与并行会话的 apply/cleanup 互斥
|
|
1348
|
+
// (防 TOCTOU:检查时他者正在 apply → 判定漂移)。best-effort:锁超时不阻断 execute
|
|
1349
|
+
// 完成,降级为保留 worktree + 手动清理指引。
|
|
1350
|
+
try {
|
|
1351
|
+
const { withMainRepoLock } = await import('../worktree-apply.js')
|
|
1352
|
+
const cleanResult = await withMainRepoLock(cwd, changeName, 'execute-cleanup', () => {
|
|
1353
|
+
const check = wm.hasUnappliedChanges(changeName);
|
|
1354
|
+
if (check.hasChanges) {
|
|
1355
|
+
return { kept: true, check }
|
|
1356
|
+
}
|
|
1357
|
+
return { kept: false, result: wm.cleanup(changeName) }
|
|
1358
|
+
})
|
|
1359
|
+
if (cleanResult.kept) {
|
|
1360
|
+
console.log(`🔗 Worktree: pending apply (${cleanResult.check.changedFiles.length} 个未应用变更)`);
|
|
1361
|
+
console.log(` 下一步: sillyspec worktree apply ${changeName}`);
|
|
1362
|
+
} else {
|
|
1363
|
+
console.log(`🔗 Worktree: ${cleanResult.result.result}`);
|
|
1364
|
+
if (cleanResult.result.residual?.length > 0) {
|
|
1365
|
+
console.warn(` ⚠️ 清理残留: ${cleanResult.result.residual.join('; ')}`);
|
|
1366
|
+
console.warn(` 手动处理: sillyspec worktree cleanup ${changeName} --force`);
|
|
1367
|
+
} else if (cleanResult.result.details?.length > 0) {
|
|
1368
|
+
for (const d of cleanResult.result.details) {
|
|
1369
|
+
if (d.startsWith('⚠️')) console.log(` ${d}`);
|
|
1370
|
+
}
|
|
1348
1371
|
}
|
|
1349
1372
|
}
|
|
1373
|
+
} catch (lockErr) {
|
|
1374
|
+
if (/互斥锁被占用/.test(String(lockErr.message))) {
|
|
1375
|
+
console.log(`🔗 Worktree 自动清理跳过(主仓互斥锁被他者会话持有)——稍后手动: sillyspec worktree cleanup ${changeName}`);
|
|
1376
|
+
} else {
|
|
1377
|
+
console.warn(`⚠️ worktree 清理异常(不阻断): ${lockErr.message}`);
|
|
1378
|
+
}
|
|
1350
1379
|
}
|
|
1351
1380
|
}
|
|
1352
1381
|
} catch (e) {
|