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
package/src/run/shared.js
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* 本模块仅 re-export,run/ 层现有调用方路径与行为不变。
|
|
12
12
|
*/
|
|
13
13
|
import { basename, join, resolve, dirname, sep } from 'node:path'
|
|
14
|
-
import { existsSync, readdirSync, readFileSync, statSync, mkdirSync, writeFileSync, appendFileSync } from 'node:fs'
|
|
14
|
+
import { existsSync, readdirSync, readFileSync, statSync, mkdirSync, writeFileSync, appendFileSync, realpathSync } from 'node:fs'
|
|
15
15
|
import { fileURLToPath } from 'node:url'
|
|
16
16
|
import os from 'node:os'
|
|
17
17
|
|
|
@@ -329,6 +329,55 @@ export function resolveQuickSessionsDir(platformOpts, localSpecBase) {
|
|
|
329
329
|
return join(resolveRuntimeRoot(platformOpts, localSpecBase), 'quick-sessions')
|
|
330
330
|
}
|
|
331
331
|
|
|
332
|
+
// 他者会话声明豁免的僵尸阈值:超龄会话的声明不再作为豁免依据。与 doctor 幽灵清理
|
|
333
|
+
// GHOST_EMPTY_DIR_STALE_MS 同口径(7 天)——正常并发会话间隔在分钟/小时级,7 天未收尾的
|
|
334
|
+
// 会话按僵尸处理,防崩溃残留的 guard 永久「认领」文件侵蚀危险门。
|
|
335
|
+
const FOREIGN_SESSION_STALE_MS = 7 * 24 * 60 * 60 * 1000
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* 枚举其他 active quick 会话的显式文件声明(坑 foreign-session-declared-false-block)。
|
|
339
|
+
*
|
|
340
|
+
* 多 agent 并发时,并行 quick 会话在**本会话启动后**改的文件不在本会话 baseline →
|
|
341
|
+
* auditQuickCompletion 把它算进本会话窗口 → 命中危险清单(src/run/、package.json 等)或
|
|
342
|
+
* .sillyspec/ 判定即 blocked,只能 --force-baseline 无差别逃生。豁免口径 = 他者会话的
|
|
343
|
+
* **显式声明**(guard.allowedFiles,--files 传入)——他者的 baselineFiles 是「启动时已脏」
|
|
344
|
+
* 快照而非所有权声明,不作豁免依据(否则任何预存脏文件都能被洗白)。
|
|
345
|
+
*
|
|
346
|
+
* fail-open:目录不存在/损坏 guard.json/超龄僵尸/任何异常 → 跳过或整体返回 [],
|
|
347
|
+
* 采集失败回到无豁免现状(不放大豁免面)。
|
|
348
|
+
*
|
|
349
|
+
* @param {object} [platformOpts] 平台选项(resolveQuickSessionsDir 同参)
|
|
350
|
+
* @param {string} localSpecBase 本地 specBase(resolveQuickSessionsDir 同参)
|
|
351
|
+
* @param {string} currentSessionId 当前会话 sessionId(quick-<hex8>),枚举时排除
|
|
352
|
+
* @param {number} [nowMs] 当前时间戳(可注入,测试用)
|
|
353
|
+
* @returns {Array<{sessionId: string, files: string[]}>} 其他 active 会话的声明清单
|
|
354
|
+
*/
|
|
355
|
+
export function collectOtherQuickSessionDeclarations(platformOpts, localSpecBase, currentSessionId, nowMs = Date.now()) {
|
|
356
|
+
const out = []
|
|
357
|
+
try {
|
|
358
|
+
const sessionsDir = resolveQuickSessionsDir(platformOpts, localSpecBase)
|
|
359
|
+
let sessionDirs = []
|
|
360
|
+
try {
|
|
361
|
+
sessionDirs = readdirSync(sessionsDir, { withFileTypes: true }).filter(e => e.isDirectory()).map(e => e.name)
|
|
362
|
+
} catch { return out } // 目录不存在/不可读 → 无他者会话
|
|
363
|
+
for (const sessionName of sessionDirs) {
|
|
364
|
+
if (sessionName === currentSessionId) continue
|
|
365
|
+
const guardFile = join(sessionsDir, sessionName, 'guard.json')
|
|
366
|
+
let guard = null
|
|
367
|
+
try { guard = JSON.parse(readFileSync(guardFile, 'utf8')) } catch { continue } // 损坏/缺失跳过
|
|
368
|
+
// 僵尸防御:超龄会话(崩溃残留,--done/--cancel 都不会再走)的声明不再认领文件
|
|
369
|
+
if (guard.startedAt) {
|
|
370
|
+
const startedAtMs = Date.parse(guard.startedAt)
|
|
371
|
+
if (Number.isFinite(startedAtMs) && nowMs - startedAtMs > FOREIGN_SESSION_STALE_MS) continue
|
|
372
|
+
}
|
|
373
|
+
const files = (Array.isArray(guard.allowedFiles) ? guard.allowedFiles : []).filter(f => typeof f === 'string' && f)
|
|
374
|
+
if (files.length === 0) continue
|
|
375
|
+
out.push({ sessionId: guard.sessionId || sessionName, files })
|
|
376
|
+
}
|
|
377
|
+
} catch { /* fail-open:整体异常返回已采集部分 */ }
|
|
378
|
+
return out
|
|
379
|
+
}
|
|
380
|
+
|
|
332
381
|
/**
|
|
333
382
|
* 平台指针三写(单一数据源,runCommand scan 与 cmdInit 平台模式共用)。
|
|
334
383
|
*
|
|
@@ -355,6 +404,15 @@ export const PLATFORM_MANAGED_FILENAME = '.sillyspec-platform-managed'
|
|
|
355
404
|
|
|
356
405
|
export function writePlatformPointer(cwd, platformOpts, extra = {}) {
|
|
357
406
|
if (!platformOpts || (!platformOpts.specRoot && !platformOpts.runtimeRoot)) return false
|
|
407
|
+
// 自指写入拦截(FR-4,变更 2026-08-23-repo-native-spec-backfill):specRoot 经 realpath
|
|
408
|
+
// 解析回本地 .sillyspec(repo-native junction 回环)时拒绝三写(主文件/指针/声明全不落盘)。
|
|
409
|
+
// 单点收口同时覆盖 runCommand 与 cmdInit(writeInitPlatformPointer) 两调用方——旧模板
|
|
410
|
+
// scan flag / init 平台模式显式传自指 specRoot 也不再重新投毒。runtimeRoot-only 组合
|
|
411
|
+
// 无 specRoot 可判(isSelfReferentialSpecRoot 对 null 恒 false),原语义不变。
|
|
412
|
+
if (isSelfReferentialSpecRoot(cwd, platformOpts.specRoot)) {
|
|
413
|
+
console.warn(`⚠️ 检测到自指平台指针(repo-native junction 回环,specRoot 指回本地 .sillyspec),已跳过指针写入并按本地模式运行: ${platformOpts.specRoot}`)
|
|
414
|
+
return false
|
|
415
|
+
}
|
|
358
416
|
// HUB-05:合并保留既有生命周期字段。scan 完成时指针被写入 status/completedAt/scanStatus
|
|
359
417
|
//(complete-handlers),但下一次任何平台模式 run(含只读 --status)都会重写指针——
|
|
360
418
|
// 恢复链只回填 specRoot 等四字段,不保留 status → 指针永远回 active、isPointerStale
|
|
@@ -427,6 +485,48 @@ export function checkPlatformManaged(cwd) {
|
|
|
427
485
|
}
|
|
428
486
|
}
|
|
429
487
|
|
|
488
|
+
/**
|
|
489
|
+
* 自指 specRoot 判定(repo-native junction 回环检测,变更 2026-08-23-repo-native-spec-backfill)。
|
|
490
|
+
*
|
|
491
|
+
* daemon repo-native 工作区把缓存目录(--spec-root 入参,~/.sillyhub/daemon/specs/{ws})以
|
|
492
|
+
* junction/symlink 指回源项目 <cwd>/.sillyspec——该 specRoot 经 realpath 解析后与本地
|
|
493
|
+
* .sillyspec 同物理目录,属"自指回环":不应让 CLI 进入平台模式(内置 sync / auto-pull
|
|
494
|
+
* 按本地模式语义运行,断链修复见 design.md Phase 2)。
|
|
495
|
+
*
|
|
496
|
+
* 双方 fs.realpathSync(native,junction/symlink 跨平台穿透)后严格相等 → true;
|
|
497
|
+
* specRoot 空 / 任一路径不存在 / realpath 抛错 → false(按外部目录处理,保守不干预)。
|
|
498
|
+
* 约束:cwd 必须为项目根(与指针查找 join(cwd,'.sillyspec-platform.json') 同基准)。
|
|
499
|
+
*
|
|
500
|
+
* @param {string} cwd - 项目根
|
|
501
|
+
* @param {string|null} [specRoot] - 平台 specRoot(可 null/undefined/空串)
|
|
502
|
+
* @returns {boolean} 是否自指回环
|
|
503
|
+
*/
|
|
504
|
+
export function isSelfReferentialSpecRoot(cwd, specRoot) {
|
|
505
|
+
if (!specRoot || typeof specRoot !== 'string') return false
|
|
506
|
+
try {
|
|
507
|
+
return realpathSync(specRoot) === realpathSync(join(cwd, '.sillyspec'))
|
|
508
|
+
} catch {
|
|
509
|
+
return false
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
/**
|
|
514
|
+
* 平台模式判定单一数据源(自指免疫,变更 2026-08-23-repo-native-spec-backfill)。
|
|
515
|
+
*
|
|
516
|
+
* 语义:(specRoot || runtimeRoot) 存在且非自指 → 平台模式;自指回环(repo-native
|
|
517
|
+
* junction)按本地模式处理。本模块内平台模式门禁统一经本函数,裸表达式仅此一处
|
|
518
|
+
* (grep 验证:triggerSync/triggerPull/triggerPullActiveChange/checkApproval 四处收敛)。
|
|
519
|
+
* runtimeRoot 落在 specRoot 内,自指判定以 specRoot 为准即可覆盖;runtimeRoot-only
|
|
520
|
+
* 组合无 specRoot 可判,不触发自指豁免(保持平台模式)。
|
|
521
|
+
*
|
|
522
|
+
* @param {object} [platformOpts] - { specRoot?, runtimeRoot?, ... }(可 null/undefined)
|
|
523
|
+
* @param {string} cwd - 项目根(isSelfReferentialSpecRoot 同基准)
|
|
524
|
+
* @returns {boolean}
|
|
525
|
+
*/
|
|
526
|
+
export function isPlatformMode(platformOpts, cwd) {
|
|
527
|
+
return Boolean((platformOpts?.specRoot || platformOpts?.runtimeRoot) && !isSelfReferentialSpecRoot(cwd, platformOpts?.specRoot))
|
|
528
|
+
}
|
|
529
|
+
|
|
430
530
|
/**
|
|
431
531
|
* 统一查找变更目录(与 progress.js 的变更检测逻辑一致)。
|
|
432
532
|
*/
|
|
@@ -483,8 +583,8 @@ async function raceWithAbort(op, timeoutMs = SYNC_TOTAL_TIMEOUT_MS) {
|
|
|
483
583
|
export const QUICK_SID_RE = /^quick-[0-9a-f]{8}$/
|
|
484
584
|
|
|
485
585
|
export async function triggerSync(cwd, changeName, platformOpts = {}, opts = {}) {
|
|
486
|
-
// 平台模式(SillyHub)走自己的回传链路,不走 CLI 内置 sync
|
|
487
|
-
if (platformOpts
|
|
586
|
+
// 平台模式(SillyHub)走自己的回传链路,不走 CLI 内置 sync;自指回环(repo-native junction)按本地模式处理
|
|
587
|
+
if (isPlatformMode(platformOpts, cwd)) return
|
|
488
588
|
try {
|
|
489
589
|
// ql-20260818-011:quick 会话按设计无实体变更目录(progress.js initChange 同款
|
|
490
590
|
// 跳过建目录),progress/四件套上行对它是孤儿数据;但 spec 树增量(QUICKLOG/
|
|
@@ -556,8 +656,8 @@ function _stampAutoPull(cwd) {
|
|
|
556
656
|
* @param {object} [platformOpts] - 平台模式 opts(specRoot/runtimeRoot 存在则跳过,走平台自有链路)
|
|
557
657
|
*/
|
|
558
658
|
export async function triggerPull(cwd, changeName, platformOpts = {}, opts = {}) {
|
|
559
|
-
// 平台模式(SillyHub
|
|
560
|
-
if (platformOpts
|
|
659
|
+
// 平台模式(SillyHub)走自己的链路,跳过;自指回环(repo-native junction)按本地模式处理
|
|
660
|
+
if (isPlatformMode(platformOpts, cwd)) return
|
|
561
661
|
try {
|
|
562
662
|
const syncMod = await import('../sync.js')
|
|
563
663
|
const sm = new syncMod.SyncManager(cwd)
|
|
@@ -579,7 +679,8 @@ export async function triggerPull(cwd, changeName, platformOpts = {}, opts = {})
|
|
|
579
679
|
* 供 index.js 在 stage 命令(run/--done/archive)与 approve 决策点注入一行调用。
|
|
580
680
|
*/
|
|
581
681
|
export async function triggerPullActiveChange(cwd, platformOpts = {}) {
|
|
582
|
-
|
|
682
|
+
// 平台模式走自己的链路,跳过;自指回环(repo-native junction)按本地模式处理
|
|
683
|
+
if (isPlatformMode(platformOpts, cwd)) return
|
|
583
684
|
// 先检查是否连接平台:未连接直接 return,避免 _ensureDB 在无 local.yaml 的 cwd 创建空 DB 污染
|
|
584
685
|
let sm = null
|
|
585
686
|
try {
|
|
@@ -645,8 +746,8 @@ export function warnApprovalUnknown(cwd, changeName, reason) {
|
|
|
645
746
|
* 平台模式走自己的链路,跳过;否则 await import sync.js。
|
|
646
747
|
* @returns {{ status: string, reason?: string } | null}
|
|
647
748
|
*/export async function checkApproval(cwd, changeName, platformOpts = {}) {
|
|
648
|
-
// 平台模式不需要 CLI
|
|
649
|
-
if (platformOpts
|
|
749
|
+
// 平台模式不需要 CLI 内置审批检查;自指回环(repo-native junction)按本地模式处理
|
|
750
|
+
if (isPlatformMode(platformOpts, cwd)) return null
|
|
650
751
|
try {
|
|
651
752
|
// shared.js 在 src/run/,sync.js 在 src/ → 退一层
|
|
652
753
|
const syncMod = await import('../sync.js')
|
|
@@ -708,7 +809,7 @@ export function isQuickMetadata(p, linkedChanges = []) {
|
|
|
708
809
|
|
|
709
810
|
/**
|
|
710
811
|
* quick 完成审计:对比 baseline 与实际变更。
|
|
711
|
-
* @returns {{ status: 'safe'|'warning'|'blocked', reasons: string[], changedFiles: string[], newFiles: string[], deletedFiles: string[], baselineHit: string[], stagedTotal: number, attributedFiles: string[], undeclaredFiles: string[] }}
|
|
812
|
+
* @returns {{ status: 'safe'|'warning'|'blocked', reasons: string[], changedFiles: string[], newFiles: string[], deletedFiles: string[], baselineHit: string[], stagedTotal: number, attributedFiles: string[], undeclaredFiles: string[], foreignSessionDeclared: Array<{file: string, sessions: string[]}> }}
|
|
712
813
|
*/
|
|
713
814
|
/**
|
|
714
815
|
* D-8 O-1 模块归属(2026-08-15 docs-signals-o12):quick 欠账 hint 从"改了 N 文件"升级为
|
|
@@ -818,7 +919,7 @@ export async function auditQuickCompletion(cwd, guard, options = {}) {
|
|
|
818
919
|
// stagedTotal:当前所有非 quick 元数据的未提交条目(含前序 baseline 残留)。
|
|
819
920
|
// 与 changedFiles(扣 baseline 后的本轮新增)区分,供审计文案同时展示「本轮新增 vs 累计暂存」,
|
|
820
921
|
// 避免叠加 quick 会话时把前序会话未提交文件误读为「本会话只动了 N 个」。
|
|
821
|
-
const result = { status: 'safe', reasons: [], changedFiles: [], newFiles: [], deletedFiles: [], baselineHit: [], stagedTotal: 0, attributedFiles: [], undeclaredFiles: [] }
|
|
922
|
+
const result = { status: 'safe', reasons: [], changedFiles: [], newFiles: [], deletedFiles: [], baselineHit: [], stagedTotal: 0, attributedFiles: [], undeclaredFiles: [], foreignSessionDeclared: [] }
|
|
822
923
|
|
|
823
924
|
try {
|
|
824
925
|
// safeGit 带 -c safe.directory,避免 linked worktree/容器异 uid/挂载点下裸 `git status` 抛错被
|
|
@@ -851,6 +952,38 @@ export async function auditQuickCompletion(cwd, guard, options = {}) {
|
|
|
851
952
|
const f = normalizeGitPath(p)
|
|
852
953
|
return baselineExact.has(f) || baselineDirs.some(d => f.startsWith(d))
|
|
853
954
|
}
|
|
955
|
+
// 他者 active 会话声明索引(坑 foreign-session-declared-false-block,2026-08-23 实证:多 agent
|
|
956
|
+
// 并发时并行会话窗口内改的文件——尤其命中危险清单的(daemon/router.py 之于本仓是 src/run/*、
|
|
957
|
+
// package.json)或 .sillyspec/ 下的——不在本会话 baseline,被算进本会话窗口直接 blocked,
|
|
958
|
+
// 只能 --force-baseline 无差别逃生)。声明即归属的他向版本:他者会话显式声明(--files)的文件
|
|
959
|
+
// 退栈归该会话审计。本会话也声明同一文件(--files 重叠)时不退栈——同文件并发归本会话审计
|
|
960
|
+
// (sameFileHits advisory 管辖)。豁免面 = 他者显式声明,不含他者 baseline 快照(那是「启动时
|
|
961
|
+
// 已脏」非所有权声明,当豁免依据会洗白任何预存脏文件)。
|
|
962
|
+
const ownDeclaredNorm = new Set((allowedFiles || []).map(f => normalizeGitPath(f)))
|
|
963
|
+
const foreignDeclaredIndex = new Map()
|
|
964
|
+
for (const s of Array.isArray(guard.otherSessionsDeclared) ? guard.otherSessionsDeclared : []) {
|
|
965
|
+
if (!s || typeof s !== 'object') continue
|
|
966
|
+
for (const f of Array.isArray(s.files) ? s.files : []) {
|
|
967
|
+
const nf = normalizeGitPath(f)
|
|
968
|
+
if (!nf || ownDeclaredNorm.has(nf)) continue
|
|
969
|
+
if (!foreignDeclaredIndex.has(nf)) foreignDeclaredIndex.set(nf, [])
|
|
970
|
+
if (!foreignDeclaredIndex.get(nf).includes(s.sessionId)) foreignDeclaredIndex.get(nf).push(s.sessionId)
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
const findForeignDeclaredOwner = (p) => {
|
|
974
|
+
const f = normalizeGitPath(p)
|
|
975
|
+
if (f.endsWith('/')) {
|
|
976
|
+
// 折叠目录 token(porcelain 把全新未跟踪目录折成 `dir/`):目录下任一文件被他者声明 →
|
|
977
|
+
// 整目录归他者;但本会话在同目录下有声明时不退栈——目录内混有双方文件,留给各门判定
|
|
978
|
+
// (fail-closed:宁可误拦不可漏放,误拦有 --files 追加声明的明确出口)。
|
|
979
|
+
if ([...ownDeclaredNorm].some(x => x.startsWith(f))) return null
|
|
980
|
+
for (const [nf, owners] of foreignDeclaredIndex) {
|
|
981
|
+
if (nf.startsWith(f)) return owners
|
|
982
|
+
}
|
|
983
|
+
return null
|
|
984
|
+
}
|
|
985
|
+
return foreignDeclaredIndex.get(f) || null
|
|
986
|
+
}
|
|
854
987
|
const DANGEROUS_PATTERNS = [
|
|
855
988
|
'package.json',
|
|
856
989
|
'package-lock.json',
|
|
@@ -891,6 +1024,15 @@ export async function auditQuickCompletion(cwd, guard, options = {}) {
|
|
|
891
1024
|
// 预存脏文件:step1 baseline 已记录,非本次 quick 产生,跳过审计(含折叠目录前缀匹配)
|
|
892
1025
|
if (isBaselineFile(file)) continue
|
|
893
1026
|
|
|
1027
|
+
// 他者 active 会话已声明 → 完全退栈归该会话审计(危险/删除/新增/越界/baseline 全部门跳过),
|
|
1028
|
+
// 收进 foreignSessionDeclared 供 printQuickAuditReview 软警告——放行可见可审计不静默
|
|
1029
|
+
// (与 linkedChangeLeftovers 同哲学)。stagedTotal 已在上方计数,累计暂存口径不受退栈影响。
|
|
1030
|
+
const foreignOwners = findForeignDeclaredOwner(file)
|
|
1031
|
+
if (foreignOwners) {
|
|
1032
|
+
result.foreignSessionDeclared.push({ file, sessions: foreignOwners })
|
|
1033
|
+
continue
|
|
1034
|
+
}
|
|
1035
|
+
|
|
894
1036
|
result.changedFiles.push(file)
|
|
895
1037
|
if (rawStatus[0] === 'D' || rawStatus[1] === 'D') result.deletedFiles.push(file)
|
|
896
1038
|
if (status === '??') result.newFiles.push(file)
|
|
@@ -902,10 +1044,26 @@ export async function auditQuickCompletion(cwd, guard, options = {}) {
|
|
|
902
1044
|
|
|
903
1045
|
// 检查危险文件(除非 force-baseline)
|
|
904
1046
|
if (file.startsWith('.sillyspec/') && !isQuickMetadata(file, guard.linkedChanges) && !forceBaseline) {
|
|
1047
|
+
// 关联变更目录遗留放行(坑 linked-change-leftover-false-block,2026-08-22 实证:并发下
|
|
1048
|
+
// 关联变更目录里他者/上个 session 的遗留脏文件——审计时不在本会话 baseline(启动后出现)
|
|
1049
|
+
// 且 isQuickMetadata 对关联变更目录不豁免 → 落「危险文件」blocked,只能 --force-baseline。
|
|
1050
|
+
// quick 无法区分「他者遗留」与「本次偷改」,按用户裁决放行降 warning:完全剔除出本会话
|
|
1051
|
+
// 归属(changedFiles/newFiles/deletedFiles 均退栈),不计文件行、不触发危险/新增/越界
|
|
1052
|
+
// 三道门,遗留给关联变更自己的流程收尾。非关联的 .sillyspec/ 仍 blocked。
|
|
1053
|
+
const linkedDir = (Array.isArray(guard.linkedChanges) ? guard.linkedChanges : [])
|
|
1054
|
+
.find(lc => file.startsWith(`.sillyspec/changes/${String(lc).replace(/\\/g, '/')}/`))
|
|
1055
|
+
if (linkedDir) {
|
|
1056
|
+
result.changedFiles.pop()
|
|
1057
|
+
if (status === '??') result.newFiles.pop()
|
|
1058
|
+
if (rawStatus[0] === 'D' || rawStatus[1] === 'D') result.deletedFiles.pop()
|
|
1059
|
+
result.linkedChangeLeftovers = result.linkedChangeLeftovers || []
|
|
1060
|
+
result.linkedChangeLeftovers.push(file)
|
|
1061
|
+
continue
|
|
1062
|
+
}
|
|
905
1063
|
result.reasons.push(`危险文件变更: ${file}`)
|
|
906
1064
|
}
|
|
907
1065
|
|
|
908
|
-
if (DANGEROUS_PATTERNS.some(p =>
|
|
1066
|
+
if (DANGEROUS_PATTERNS.some(p => p === file || file.startsWith(p)) && !forceBaseline) {
|
|
909
1067
|
result.reasons.push(`危险文件变更: ${file}`)
|
|
910
1068
|
}
|
|
911
1069
|
}
|
|
@@ -1069,6 +1227,13 @@ export async function auditQuickCompletion(cwd, guard, options = {}) {
|
|
|
1069
1227
|
console.warn(` - ${f}`)
|
|
1070
1228
|
console.warn(` 分离:git add -p ${f}(交互选你的 hunk)或 git diff ${f} > mine.patch(编辑留你的)+ git apply --cached mine.patch`)
|
|
1071
1229
|
}
|
|
1230
|
+
// 宽严差异明示(坑 concurrent-policy-inconsistency,2026-08-22 实证:quick 同文件并发只
|
|
1231
|
+
// warn 不阻断、plan 阶段同 Wave 同文件却硬拦——两阶段宽严不一致易误判边界)。差异是设计
|
|
1232
|
+
// 使然:quick 是轻量流程无子代理并行,同文件并发最坏后果是 commit 夹带他者 hunk(git 可
|
|
1233
|
+
// 分离,提示分离命令即可);plan 的同 Wave 同文件会让 execute 的并行子代理互相覆盖(不可
|
|
1234
|
+
// 自动恢复),必须设计期硬拦。
|
|
1235
|
+
console.warn(` ℹ️ 边界说明:quick 对同文件并发只提示不阻断(最坏后果=commit 夹带他者 hunk,git 可分离);`)
|
|
1236
|
+
console.warn(` plan 阶段同 Wave 同文件是硬错误(execute 并行子代理会互相覆盖,不可自动恢复)——两阶段宽严不同是设计使然,勿以 quick 的宽松推断 plan 的边界。`)
|
|
1072
1237
|
}
|
|
1073
1238
|
}
|
|
1074
1239
|
|
package/src/run/stage.js
CHANGED
|
@@ -162,6 +162,29 @@ export async function runStage(pm, progress, stageName, cwd, changeName, skipApp
|
|
|
162
162
|
} catch { /* 预警失败不阻断 execute */ }
|
|
163
163
|
}
|
|
164
164
|
|
|
165
|
+
// ── 共享主仓竞态防护(坑 parallel-shared-main-race,2026-08-23 实证:并行会话共享主仓
|
|
166
|
+
// 工作区/共享部署库的竞态——文件被清、alembic 被推进、staged 混入他者文件)。execute/verify
|
|
167
|
+
// 启动时探测三类信号并 advisory(不阻断——并发是合法工作模式,但要让 agent 知道环境在变):
|
|
168
|
+
// 1. 主仓未提交改动中含非本变更文件(staged 混入:commit 时注意 pathspec)
|
|
169
|
+
// 2. worktree 存在时 base..HEAD 主仓推进(主仓在他者 apply 后已前进,diff 口径注意)
|
|
170
|
+
// best-effort:异常静默。
|
|
171
|
+
if ((stageName === 'execute' || stageName === 'verify') && changeName) {
|
|
172
|
+
try {
|
|
173
|
+
const mainDirty = (safeGit(cwd, ['status', '--porcelain'], { trim: false }).value || '')
|
|
174
|
+
.split('\n').filter(Boolean).map(l => l.slice(3).trim().split(' -> ').pop() || '')
|
|
175
|
+
.map(p => p.replace(/^"|"$/g, '').replace(/\\/g, '/'))
|
|
176
|
+
.filter(p => p && !p.startsWith('.sillyspec/'))
|
|
177
|
+
const myFiles = (await import('../verify-postcheck.js')).resolveVerifyChangedFiles(cwd, changeName, null, { includeWorkingTree: true }) || []
|
|
178
|
+
const mySet = new Set(myFiles.map(f => f.replace(/\\/g, '/')))
|
|
179
|
+
const foreign = mainDirty.filter(p => !mySet.has(p))
|
|
180
|
+
if (foreign.length > 0) {
|
|
181
|
+
console.warn(`⚠️ 主仓含 ${foreign.length} 个非本变更的未提交文件(并行会话工作/部署产物混入)——`)
|
|
182
|
+
console.warn(` 提交时用精确 pathspec(勿 git add -A / 目录级 add 夹带):${foreign.slice(0, 4).join(', ')}${foreign.length > 4 ? ` …` : ''}`)
|
|
183
|
+
console.warn(` 交接物核对(文件被清/迁移被推进类异常)先确认是否他者会话所谓,再判定本变更问题。`)
|
|
184
|
+
}
|
|
185
|
+
} catch { /* 探测失败不阻断 */ }
|
|
186
|
+
}
|
|
187
|
+
|
|
165
188
|
// 自动探测 currentChange
|
|
166
189
|
if (autoDetectChange(progress, cwd)) {
|
|
167
190
|
progress.lastActive = new Date().toLocaleString('zh-CN', { hour12: false })
|
package/src/run.js
CHANGED
|
@@ -15,8 +15,8 @@
|
|
|
15
15
|
*/
|
|
16
16
|
// W6 Step8c: 主命令分发(runCommand)+ auto 模式 + ensureStageSteps 搬至 ./run/command.js
|
|
17
17
|
export { runCommand, ensureStageSteps } from './run/command.js'
|
|
18
|
-
// barrel re-export: parsePorcelainPath + auditQuickCompletion + isQuickMetadata 被 test 直接 import(契约保留)
|
|
19
|
-
export { parsePorcelainPath, auditQuickCompletion, isQuickMetadata } from './run/shared.js'
|
|
18
|
+
// barrel re-export: parsePorcelainPath + auditQuickCompletion + isQuickMetadata + collectOtherQuickSessionDeclarations 被 test 直接 import(契约保留)
|
|
19
|
+
export { parsePorcelainPath, auditQuickCompletion, isQuickMetadata, collectOtherQuickSessionDeclarations } from './run/shared.js'
|
|
20
20
|
// barrel re-export: applyRootPlaceholders 被 test/prompt-placeholders.test.mjs 直接 import(契约保留)
|
|
21
21
|
export { applyRootPlaceholders } from './run/prompt.js'
|
|
22
22
|
// barrel re-export: sanitizeProjectName + validateParsedProjects 被 test 直接 import(随 handleScan 搬走,契约保留)
|
package/src/scan-postcheck.js
CHANGED
|
@@ -10,6 +10,7 @@ import { join, basename } from 'path'
|
|
|
10
10
|
import { SCAN_STATUS, CHECK_SEVERITY, SCAN_REQUIRED_DOCS, SCAN_REQUIRED_DOCS_QUICK } from './constants.js'
|
|
11
11
|
import { validateScriptCommands } from './stages/cmd-existence.js'
|
|
12
12
|
import { git } from './git-helper.js'
|
|
13
|
+
import { nowWallClock } from './datetime.js'
|
|
13
14
|
|
|
14
15
|
const REQUIRED_SCAN_DOCS = SCAN_REQUIRED_DOCS
|
|
15
16
|
|
|
@@ -394,7 +395,7 @@ export function fixScanDocHeaders({ cwd, specDir = null, project = null }) {
|
|
|
394
395
|
try {
|
|
395
396
|
author = git(cwd, ['config', 'user.name']) || 'unknown'
|
|
396
397
|
} catch { /* git 不可用 → unknown */ }
|
|
397
|
-
const now =
|
|
398
|
+
const now = nowWallClock()
|
|
398
399
|
|
|
399
400
|
const fixed = []
|
|
400
401
|
const skipped = []
|
|
@@ -410,26 +411,13 @@ export function fixScanDocHeaders({ cwd, specDir = null, project = null }) {
|
|
|
410
411
|
} catch {
|
|
411
412
|
continue
|
|
412
413
|
}
|
|
413
|
-
const
|
|
414
|
-
|
|
415
|
-
const hasCreated = /created_at\s*:/.test(header)
|
|
416
|
-
if (hasAuthor && hasCreated) {
|
|
414
|
+
const r = backfillFrontmatter(content, { author, createdAt: now })
|
|
415
|
+
if (!r.changed) {
|
|
417
416
|
skipped.push(filePath)
|
|
418
417
|
continue
|
|
419
418
|
}
|
|
420
|
-
const inserts = []
|
|
421
|
-
if (!hasAuthor) inserts.push(`author: ${author}`)
|
|
422
|
-
if (!hasCreated) inserts.push(`created_at: ${now}`)
|
|
423
|
-
let newContent
|
|
424
|
-
if (content.startsWith('---\n') || content.startsWith('---\r\n')) {
|
|
425
|
-
// 有 frontmatter:插到开标签后(CRLF 归一写回,与其它 CLI 写入口径一致)
|
|
426
|
-
const normalized = content.replace(/\r\n?/g, '\n')
|
|
427
|
-
newContent = normalized.replace(/^---\n/, `---\n${inserts.join('\n')}\n`)
|
|
428
|
-
} else {
|
|
429
|
-
newContent = `---\n${inserts.join('\n')}\n---\n\n` + content
|
|
430
|
-
}
|
|
431
419
|
try {
|
|
432
|
-
writeFileSync(filePath,
|
|
420
|
+
writeFileSync(filePath, r.content)
|
|
433
421
|
fixed.push(filePath)
|
|
434
422
|
} catch { /* 只读文件等写失败 → 跳过不抛 */ }
|
|
435
423
|
}
|
|
@@ -437,6 +425,31 @@ export function fixScanDocHeaders({ cwd, specDir = null, project = null }) {
|
|
|
437
425
|
return { fixed, skipped }
|
|
438
426
|
}
|
|
439
427
|
|
|
428
|
+
/**
|
|
429
|
+
* 通用 frontmatter 补齐(纯函数):首 512 字节缺 author:/created_at: 时补缺失键。
|
|
430
|
+
* 有 frontmatter(--- 开头)→ 插到开标签后(CRLF 归一 LF 写回);无 → 头部整块前置。
|
|
431
|
+
* fixScanDocHeaders 与 decisions.md 自动补(stage-contract.js ensureDecisionDocHeader)共用,
|
|
432
|
+
* 单一实现杜绝两处行尾/插入位置漂移。
|
|
433
|
+
* @param {string} content
|
|
434
|
+
* @param {{ author: string, createdAt: string }} p
|
|
435
|
+
* @returns {{ changed: boolean, content: string }}
|
|
436
|
+
*/
|
|
437
|
+
export function backfillFrontmatter(content, { author, createdAt }) {
|
|
438
|
+
const header = String(content || '').slice(0, 512)
|
|
439
|
+
const hasAuthor = /author\s*:/.test(header)
|
|
440
|
+
const hasCreated = /created_at\s*:/.test(header)
|
|
441
|
+
if (hasAuthor && hasCreated) return { changed: false, content }
|
|
442
|
+
const inserts = []
|
|
443
|
+
if (!hasAuthor) inserts.push(`author: ${author}`)
|
|
444
|
+
if (!hasCreated) inserts.push(`created_at: ${createdAt}`)
|
|
445
|
+
if (content.startsWith('---\n') || content.startsWith('---\r\n')) {
|
|
446
|
+
// 有 frontmatter:插到开标签后(CRLF 归一写回,与其它 CLI 写入口径一致)
|
|
447
|
+
const normalized = content.replace(/\r\n?/g, '\n')
|
|
448
|
+
return { changed: true, content: normalized.replace(/^---\n/, `---\n${inserts.join('\n')}\n`) }
|
|
449
|
+
}
|
|
450
|
+
return { changed: true, content: `---\n${inserts.join('\n')}\n---\n\n` + content.replace(/\r\n?/g, '\n') }
|
|
451
|
+
}
|
|
452
|
+
|
|
440
453
|
/**
|
|
441
454
|
* source_root 污染一键修复(2026-08-21 agent-手工产出审计第四批 C-3)。
|
|
442
455
|
*
|
|
@@ -403,8 +403,9 @@ const PLAN_RULES = [
|
|
|
403
403
|
messageVerify: '${id}: 缺少 verify 字段',
|
|
404
404
|
messageConstraints: '${id}: 缺少 constraints 字段',
|
|
405
405
|
messageDependsOnMissing: '${id}: depends_on 引用了不存在的 ${dep}',
|
|
406
|
+
messagePlaceholder: '${id}: task 卡仍是未填充的生成骨架(${fields} 里的占位符未替换)——占位符视同缺字段。用 Edit 填充 tasks/${id}.md 后重跑(勿删字段勿留 FR-XX/src/example/file.ts/具体步骤 等占位值)',
|
|
406
407
|
},
|
|
407
|
-
spec: 'task 卡片完整 TaskCard schema(可行性,只认 frontmatter 字段,均 error):YAML frontmatter 必备;frontmatter 需 id、title;allowed_paths 非空;frontmatter 需 goal、implementation、acceptance、verify、constraints 五字段;depends_on 引用的 task
|
|
408
|
+
spec: 'task 卡片完整 TaskCard schema(可行性,只认 frontmatter 字段,均 error):YAML frontmatter 必备;frontmatter 需 id、title;allowed_paths 非空;frontmatter 需 goal、implementation、acceptance、verify、constraints 五字段;depends_on 引用的 task 必须存在;骨架占位符(FR-XX/D-XXX/src/example/file.ts/一句话说明这个 task/具体步骤 1/可验证的验收条件 1/边界约束 1)未替换视同缺字段。',
|
|
408
409
|
failMessage: '${id}: task 卡片 frontmatter 缺少必要字段(id/title/allowed_paths/goal/implementation/acceptance/verify/constraints)',
|
|
409
410
|
},
|
|
410
411
|
]
|
package/src/stage-contract.js
CHANGED
|
@@ -5,13 +5,15 @@
|
|
|
5
5
|
* CLI 不再相信 prompt 完成,completeStep 后必须过 validator。
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import { existsSync, readdirSync, readFileSync } from 'fs'
|
|
8
|
+
import { existsSync, readdirSync, readFileSync, writeFileSync } from 'fs'
|
|
9
9
|
import { join, basename } from 'path'
|
|
10
10
|
import { safeGit } from './git-helper.js'
|
|
11
|
+
import { nowWallClock } from './datetime.js'
|
|
11
12
|
import { detectChangeRisk, checkIntegrationEvidence, VERIFICATION_NEEDS, RISK_LEVEL_CAUSES } from './change-risk-profile.js'
|
|
12
13
|
import { SCAN_REQUIRED_DOCS, AUXILIARY_STAGES } from './constants.js'
|
|
13
14
|
import { evaluateRules } from './stage-contract-engine.js'
|
|
14
15
|
import { getRule } from './stage-contract-spec.js'
|
|
16
|
+
import { backfillFrontmatter } from './scan-postcheck.js'
|
|
15
17
|
import { parseAllowedPaths } from './stages/plan-postcheck.js'
|
|
16
18
|
|
|
17
19
|
/**
|
|
@@ -163,6 +165,31 @@ function parseDecisionRecords(content) {
|
|
|
163
165
|
return records
|
|
164
166
|
}
|
|
165
167
|
|
|
168
|
+
/**
|
|
169
|
+
* decisions.md 缺 author/created_at header 时自动补齐(坑 decisions-header-late-warning,
|
|
170
|
+
* 2026-08-24 用户反馈二期:brainstorm step8 模板曾自带无 frontmatter 的 decisions 样例——
|
|
171
|
+
* agent 照抄生成必缺 header,拖到后续环节才提示。模板已修;本函数兜存量:gate 前幂等补
|
|
172
|
+
* 机械字段(author=git user / created_at=now),与 scan-fix-headers 同一 backfillFrontmatter 实现。
|
|
173
|
+
* @param {string} changeDir - changes/<name> 目录
|
|
174
|
+
* @returns {boolean} true = 本次补齐过(false = 无 decisions.md / header 已齐 / 写失败)
|
|
175
|
+
*/
|
|
176
|
+
export function ensureDecisionDocHeader(changeDir) {
|
|
177
|
+
try {
|
|
178
|
+
const decisionPath = join(changeDir, 'decisions.md')
|
|
179
|
+
if (!existsSync(decisionPath)) return false
|
|
180
|
+
const content = readFileSync(decisionPath, 'utf8')
|
|
181
|
+
let author = 'unknown'
|
|
182
|
+
const gitUser = safeGit(changeDir, ['config', 'user.name'])
|
|
183
|
+
if (gitUser && gitUser.value) author = String(gitUser.value).trim() || 'unknown'
|
|
184
|
+
const r = backfillFrontmatter(content, { author, createdAt: nowWallClock() })
|
|
185
|
+
if (!r.changed) return false
|
|
186
|
+
writeFileSync(decisionPath, r.content)
|
|
187
|
+
console.log(`ℹ️ 已自动补齐 decisions.md 缺失的 author/created_at header: ${decisionPath}`)
|
|
188
|
+
return true
|
|
189
|
+
} catch { /* 补齐失败不阻断 gate——decisions 内容校验照常进行 */ }
|
|
190
|
+
return false
|
|
191
|
+
}
|
|
192
|
+
|
|
166
193
|
function extractCurrentDecisionIds(content) {
|
|
167
194
|
const records = parseDecisionRecords(content)
|
|
168
195
|
if (records.length === 0) return extractIds(content, 'D')
|
|
@@ -286,6 +313,27 @@ function validateBrainstormOutputs(cwd, changeName, context = {}) {
|
|
|
286
313
|
}
|
|
287
314
|
}
|
|
288
315
|
}
|
|
316
|
+
|
|
317
|
+
// ── 风险判级提前提示(坑 risk-first-use-opaque,2026-08-24 用户实证)──
|
|
318
|
+
// verify --done 的 risk gate 既往只在流程末段暴露,首次使用者撞墙后才知道 frontmatter
|
|
319
|
+
// risk_level 覆盖通道。design --done 即按同款 detectChangeRisk 预判一次(此时 plan.md 尚未
|
|
320
|
+
// 产出,verify 时会连 plan 一起重判、级别可能上浮——本提示 advisory 不作为定论):
|
|
321
|
+
// 高危关键词命中 / 存在否定抑制 → 透出判级结果 + 覆盖指引,把机制可见性提前到 design 阶段。
|
|
322
|
+
{
|
|
323
|
+
const profile = detectChangeRisk({ designContent: content })
|
|
324
|
+
if (['integration-critical', 'deployment-critical'].includes(profile.level) && !profile.explicit) {
|
|
325
|
+
warnings.push(
|
|
326
|
+
`[risk] 本次 design 判级 ${profile.level}(命中关键词:${profile.triggers.join('、')})——verify --done 将强制真实集成证据门控。` +
|
|
327
|
+
`若属关键词误伤(实际未触碰 daemon/session/启动入口),现在就在 design.md frontmatter 加 risk_level: <真实等级>(如 unit-sufficient)显式覆盖,免得到 verify 阶段才发现而返工。`
|
|
328
|
+
)
|
|
329
|
+
}
|
|
330
|
+
if ((profile.suppressedTriggers || []).length > 0) {
|
|
331
|
+
warnings.push(
|
|
332
|
+
`[risk] 关键词命中 ${profile.suppressedTriggers.join('、')} 已被同句否定语境抑制,当前判级 ${profile.level}。` +
|
|
333
|
+
`若实际涉及对应改动,请在 design.md frontmatter 声明高危级(抑制可审计,防静默降级逃证据门控)。`
|
|
334
|
+
)
|
|
335
|
+
}
|
|
336
|
+
}
|
|
289
337
|
}
|
|
290
338
|
|
|
291
339
|
const decisionsFile = join(changeDir, 'decisions.md')
|
|
@@ -461,11 +509,22 @@ function validateVerifyOutputs(cwd, changeName, context = {}) {
|
|
|
461
509
|
planContent: readIfExists(join(changeDir, 'plan.md')),
|
|
462
510
|
})
|
|
463
511
|
const conclusion = extractVerifyConclusion(verify)
|
|
512
|
+
// ── 否定抑制审计(坑 risk-negation-blindness)──
|
|
513
|
+
// 关键词命中被同句否定语境抑制而不参与判级时必须透出:抑制是把双刃剑(「本次不新增
|
|
514
|
+
// daemon」真豁免 vs 靠堆否定措辞静默降级逃证据门控),这里让每次抑制可见可核对;
|
|
515
|
+
// frontmatter risk_level 仍是双向权威覆盖通道。
|
|
516
|
+
if ((changeRiskProfile.suppressedTriggers || []).length > 0) {
|
|
517
|
+
warnings.push(
|
|
518
|
+
`[${changeRiskProfile.level}] 风险判级:关键词命中 ${changeRiskProfile.suppressedTriggers.join(', ')} 已被同句否定语境(不/未/无/避免…)抑制,未参与判级。` +
|
|
519
|
+
`若本变更实际涉及跨进程/状态机/启动入口改动,请在 design.md frontmatter 加 risk_level: integration-critical(或 deployment-critical)显式声明或提供真实集成证据——抑制可审计,不许用来静默降级。`
|
|
520
|
+
)
|
|
521
|
+
}
|
|
464
522
|
if (['integration-critical', 'deployment-critical'].includes(changeRiskProfile.level)) {
|
|
465
523
|
// 关键词误伤早期引导(坑2,FR-02):命中高危关键词且无 frontmatter risk_level 时,
|
|
466
524
|
// 无条件透出 frontmatter 覆盖指引(不依赖 conclusion / evidence),让 agent 早期即可
|
|
467
|
-
// 判断是否属误判并显式覆盖,而非撞到 evidence gate
|
|
468
|
-
//
|
|
525
|
+
// 判断是否属误判并显式覆盖,而非撞到 evidence gate 末尾才发现出路③。含否定语境的
|
|
526
|
+
// 命中已在 detectChangeRisk 内被同句否定抑制(见 change-risk-profile.js NEGATION_CUES),
|
|
527
|
+
// 此处命中的都是无否定语境的真命中;仍误伤的走 frontmatter 显式覆盖通道。
|
|
469
528
|
if (!changeRiskProfile.explicit) {
|
|
470
529
|
warnings.push(
|
|
471
530
|
`[${changeRiskProfile.level}] 本次变更被关键词判级(命中:${changeRiskProfile.triggers.join(', ')})。` +
|
|
@@ -485,7 +544,12 @@ function validateVerifyOutputs(cwd, changeName, context = {}) {
|
|
|
485
544
|
// 的结构化证据,作为附加匹配文本——有回执不依赖 agent 措辞;无回执维持原字面匹配
|
|
486
545
|
let receiptText = ''
|
|
487
546
|
try {
|
|
488
|
-
|
|
547
|
+
// 回执按变更分片(坑 verify-pids-cross-session-kill:原单份被并行会话后写覆盖),
|
|
548
|
+
// 优先读本变更分片,兼容旧单份名(升级过渡期 receipt.change 过滤仍生效)
|
|
549
|
+
const runtimeDir = join(cwd, '.sillyspec', '.runtime')
|
|
550
|
+
const receiptPath = existsSync(join(runtimeDir, `verify-services-${changeName}.receipt.json`))
|
|
551
|
+
? join(runtimeDir, `verify-services-${changeName}.receipt.json`)
|
|
552
|
+
: join(runtimeDir, 'verify-services.receipt.json')
|
|
489
553
|
if (existsSync(receiptPath)) {
|
|
490
554
|
const receipt = JSON.parse(readFileSync(receiptPath, 'utf8'))
|
|
491
555
|
if (receipt && (receipt.reapedPidCount || 0) > 0 && (!receipt.change || receipt.change === changeName)) {
|
package/src/stages/archive.js
CHANGED
|
@@ -44,9 +44,17 @@ export const definition = {
|
|
|
44
44
|
},
|
|
45
45
|
{
|
|
46
46
|
name: 'sync-module-docs',
|
|
47
|
-
//
|
|
48
|
-
//
|
|
49
|
-
|
|
47
|
+
// conditionalWait(坑 archive-subconfirm-redundant,2026-08-23 实证:requiresWait 硬门强制
|
|
48
|
+
// 三段式 --wait → --continue --answer → --done,与 verify 文档同步阻断门、归档移动前死信
|
|
49
|
+
// 校验、下一步「确认归档 --confirm」四层确认重复,交互碎。改 conditionalWait(brainstorm-auto
|
|
50
|
+
// 同款先例):常规同步直接写入 + --done(agent 写入动作在 --done 前完成,坑 verify-archive-
|
|
51
|
+
// flow-pitfalls 坑4 的「无机会写入」不回归);仅异常(needs_review/未映射/标记缺失)才 --wait
|
|
52
|
+
// 请用户裁决。用户确认收敛到「确认归档 --confirm」一处。
|
|
53
|
+
conditionalWait: true,
|
|
54
|
+
repeatableWait: true,
|
|
55
|
+
maxWaitRounds: 3,
|
|
56
|
+
waitReason: '等待用户裁决模块文档同步异常',
|
|
57
|
+
waitOptions: ['确认写入', '跳过同步'],
|
|
50
58
|
prompt: `根据 module-impact.md 同步更新模块索引和卡片文档。
|
|
51
59
|
|
|
52
60
|
### ⚠️ 核心原则:结构化事实改 _module-map.yaml,语义解释改模块卡片
|
|
@@ -109,20 +117,52 @@ created_at: <now-datetime>
|
|
|
109
117
|
<!-- MANUAL_NOTES_END -->
|
|
110
118
|
\`\`\`
|
|
111
119
|
|
|
112
|
-
4. 展示所有更新内容(diff
|
|
113
|
-
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
120
|
+
4. 展示所有更新内容(diff 摘要),按同步状态分流:
|
|
121
|
+
- **常规**(无 needs_review 影响、无 unmapped 模块、人工备注标记齐全)→ **直接写入** _module-map.yaml 和受影响的模块卡片,diff 摘要写入 --output 后 --done(本步不单独弹确认——用户确认收敛到下一步「确认归档 --confirm」,该步展示同一 diff 摘要)
|
|
122
|
+
- **异常**(含 needs_review 影响项 / unmapped 模块 / 人工备注标记缺失或重复 / 覆盖会丢失手动维护字段)→ 暂停请用户裁决:
|
|
123
|
+
\`sillyspec run archive --wait --reason "等待用户裁决模块文档同步异常" --options "确认写入,跳过同步" --output "diff 摘要 + 异常说明"\`
|
|
124
|
+
用户 --continue --answer "确认写入" 后写入;"跳过同步" 则不写入,提示 "module-impact.md 已保留,可稍后手动同步"
|
|
125
|
+
5. 回填 module-impact.md 的"更新结果"表格,区分目标:
|
|
118
126
|
- 目标列写 "\`_module-map.yaml: <module-id>\`" 或 "\`modules/<module-id>.md\`"
|
|
119
|
-
|
|
127
|
+
6. **同步完成后**,如需刷新 \`_module-map.yaml\` 索引:\`sillyspec modules rebuild --force\`(默认 dry-run 只预览不写;\`--force\` 才覆盖,但会清空 tags/entrypoints/main_symbols/depends_on/used_by 等手动维护字段——与 archive-impact 的「人工备注保护」约束冲突,仅当手动字段已并入骨架或可接受覆盖时用;优先手动更新 dependencies.md / 模块卡片)
|
|
120
128
|
|
|
121
129
|
### 输出
|
|
122
|
-
已更新的文件路径列表 +
|
|
130
|
+
已更新的文件路径列表 + diff 摘要(异常时附裁决结果)`,
|
|
123
131
|
outputHint: '模块文档更新结果',
|
|
124
132
|
optional: false
|
|
125
133
|
},
|
|
134
|
+
{
|
|
135
|
+
name: 'decision-distill 决策提炼',
|
|
136
|
+
// conditionalWait(同 sync-module-docs 先例):常规提炼直接写入 + --done;仅异常(rejected
|
|
137
|
+
// 条目缺否决理由/复潮条件 → distillIntoKnowledge 返回 needsWait 非空)才 --wait 请用户裁决
|
|
138
|
+
// (补录后重跑幂等 / 跳过该条)。勿引入 requiresWait 硬门——与下一步「确认归档 --confirm」
|
|
139
|
+
// 重复确认(坑 archive-subconfirm-redundant)。
|
|
140
|
+
conditionalWait: true,
|
|
141
|
+
repeatableWait: true,
|
|
142
|
+
maxWaitRounds: 3,
|
|
143
|
+
waitReason: '等待用户裁决 rejected 决策缺字段',
|
|
144
|
+
waitOptions: ['补录后继续', '跳过该条'],
|
|
145
|
+
prompt: `读取变更 decisions.md,把有实现影响的决策提炼进决策知识库(提炼/幂等/INDEX 路由行本体在 CLI 纯函数 src/decision-distill.js,本步只接线,勿手工改写条目格式)。
|
|
146
|
+
|
|
147
|
+
### 操作
|
|
148
|
+
1. 定位参数:changeDir = \`.sillyspec/changes/<change-name>\`(decisions.md 所在——本步在「确认归档」移动目录之前,仍在原位);knowledgeRoot = \`.sillyspec/knowledge\`;headHash = \`git rev-parse --short HEAD\`(落条目「最近确认」)
|
|
149
|
+
2. 调用纯函数 \`distillIntoKnowledge(changeDir, knowledgeRoot, headHash)\`,按返回的 \`{ written, skipped, needsWait }\` 分流(见第 3 点):
|
|
150
|
+
- 能解析到 sillyspec 源码/安装位置时(如 sillyspec 仓内 dogfood)直接 import 调用:\`node --input-type=module -e "import { distillIntoKnowledge } from './src/decision-distill.js'; console.log(JSON.stringify(distillIntoKnowledge(changeDir, knowledgeRoot, headHash), null, 2))"\`
|
|
151
|
+
- 解析不到(consumer 仓库无源码,node import 会 ERR_MODULE_NOT_FOUND)→ 按其语义执行:解析 D-xxx@vN 条目;入选规则 = 任意 type 的 status=rejected 留痕 + 五类 type(architecture/compatibility/boundary/definition/process)且 status∈{confirmed,accepted} 提炼 implemented,type=scope 不入选;条目幂等落 \`.sillyspec/knowledge/decisions/<模块域>.md\`(同 ID 同版本重写不重复追加、@vN+1 整段替换旧版并注 supersedes;「模块域」缺失按 impacts 路径与 _module-map.yaml paths 前缀匹配兜底,仍未中归 unmapped);implemented 条目写「状态:implemented + 锚点:<src 路径:行号,未记录则填"未记录"> + 最近确认:<headHash> + 理由:<一句话>」,rejected 条目写「状态:rejected + 否决理由: + 复潮条件:」;\`knowledge/INDEX.md\` 的 decisions 路由行(\`- <域>|decision|决策 → [decisions/<域>.md](decisions/<域>.md)\`)幂等增删
|
|
152
|
+
3. 分流规则:
|
|
153
|
+
- **常规**(needsWait 为空且 written 非空)→ 写入已由函数完成,written 摘要(decisions/<域>.md × D-xxx@vN × append/update/supersede)写入 --output 后直接 --done(本步不单独弹确认——用户确认收敛到下一步「确认归档 --confirm」)
|
|
154
|
+
- **needsWait 非空**(rejected 条目缺否决理由/复潮条件,该条目未写盘、其余条目照常提炼)→ 暂停请用户裁决:
|
|
155
|
+
\`sillyspec run archive --wait --reason "等待用户裁决 rejected 决策缺字段" --options "补录后继续,跳过该条" --output "needsWait 描述 + 已提炼摘要"\`
|
|
156
|
+
- 「补录后继续」→ 按 needsWait 描述在 changes/<change-name>/decisions.md 补齐缺失的否决理由/复潮条件,重跑第 2 步(幂等,重跑安全)
|
|
157
|
+
- 「跳过该条」→ 该条目不入库,--output 注记跳过原因后 --done
|
|
158
|
+
- **零输出**(skipped 非空 = 无 decisions.md / 无入选条目)→ 不创建任何文件、不动 INDEX,--output 注一句 skipped 原因后直接 --done(同 docs-debt 无债零输出原则)
|
|
159
|
+
4. 降级:调用或解析异常(模块加载失败 / decisions.md 结构异常抛错)→ warn 记录异常摘要后跳过本步 --done(best-effort,同 agent-session-log 先例),不阻断归档——决策提炼失败不挡归档移动
|
|
160
|
+
|
|
161
|
+
### 输出
|
|
162
|
+
已提炼条目清单(decisions/<域>.md × D-xxx@vN × action)或零输出/跳过/降级原因`,
|
|
163
|
+
outputHint: '决策提炼结果',
|
|
164
|
+
optional: false
|
|
165
|
+
},
|
|
126
166
|
{
|
|
127
167
|
name: '确认归档',
|
|
128
168
|
prompt: `确认归档内容,由 CLI 执行目录移动。
|
|
@@ -147,7 +187,8 @@ created_at: <now-datetime>
|
|
|
147
187
|
1. 如果 \`.sillyspec/ROADMAP.md\` 存在,标记对应 Phase 为已完成
|
|
148
188
|
2. \`git add .sillyspec/changes/archive/\` — 暂存归档结果(archive/ 下仅本次归档新增,不会裹挟 changes/ 下其他活跃变更;不要 commit,由用户通过统一提交工具处理)
|
|
149
189
|
3. \`git add .sillyspec/docs/<project>/modules/\` — 暂存模块文档更新(如有;精确到本次同步的模块文档,勿 add 整个 .sillyspec/docs/)
|
|
150
|
-
4.
|
|
190
|
+
4. \`git add .sillyspec/knowledge/decisions/\` — 暂存决策知识库更新(如有;decision-distill 步骤的提炼产物,精确到 decisions 子目录,勿 add 整个 .sillyspec/knowledge/;不要 commit,由用户通过统一提交工具处理)
|
|
191
|
+
5. 确认 sillyspec.db 中该变更已不再 active(确认归档步骤由 CLI 调用 unregisterChange)
|
|
151
192
|
|
|
152
193
|
### 输出
|
|
153
194
|
归档完成确认 + 累积规范统计`,
|