sillyspec 3.26.8 → 3.26.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/skills/sillyspec-archive/SKILL.md +1 -2
- package/.claude/skills/sillyspec-brainstorm/SKILL.md +1 -2
- package/.claude/skills/sillyspec-execute/SKILL.md +10 -2
- package/.claude/skills/sillyspec-explore/SKILL.md +0 -1
- package/.claude/skills/sillyspec-plan/SKILL.md +1 -2
- package/.claude/skills/sillyspec-quick/SKILL.md +1 -2
- package/.claude/skills/sillyspec-scan/SKILL.md +2 -2
- package/.claude/skills/sillyspec-status/SKILL.md +0 -1
- package/.claude/skills/sillyspec-verify/SKILL.md +1 -2
- package/README.md +5 -9
- package/package.json +2 -2
- package/packages/dashboard/server/index.js +14 -8
- package/src/change-risk-profile.js +5 -207
- package/src/config-schema.js +6 -1
- package/src/constants.js +97 -106
- package/src/db-engine.js +1 -1
- package/src/docs-check.js +34 -5
- package/src/git-helper.js +2 -2
- package/src/hooks/worktree-guard.js +1 -1
- package/src/index.js +96 -13
- package/src/init.js +14 -3
- package/src/machine-interface.js +7 -4
- package/src/progress/stage-machine.js +3 -0
- package/src/quicklog.js +182 -1
- package/src/run/command.js +101 -3
- package/src/run/complete.js +9 -1
- package/src/run/gates.js +67 -1
- package/src/run/prompt.js +721 -713
- package/src/run/quick-audit.js +146 -137
- package/src/run/shared.js +1197 -1109
- package/src/run/stage.js +17 -1
- package/src/scan-diff.js +388 -0
- package/src/scan-staleness.js +18 -13
- package/src/stage-contract-spec.js +1 -1
- package/src/stage-review.js +1 -1
- package/src/stages/brainstorm.js +2 -2
- package/src/stages/execute.js +991 -985
- package/src/stages/plan.js +22 -7
- package/src/stages/quick.js +1 -1
- package/src/stages/scan.js +2 -2
- package/src/stages/verify.js +10 -11
- package/src/sync.js +12 -0
- package/src/task-review.js +12 -4
- package/src/workflow.js +8 -185
- package/src/worktree-apply.js +122 -0
- package/src/worktree.js +2 -2
- package/templates/claude-instruction.md +1 -1
- package/templates/prompts/taskcard-rules.md +1 -0
- package/templates/prompts/verify-probes.md +2 -2
- package/templates/skills/sillyspec-onboard/SKILL.md +0 -92
package/src/constants.js
CHANGED
|
@@ -1,106 +1,97 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* SillySpec 平台状态枚举
|
|
3
|
-
*
|
|
4
|
-
* 所有平台产物(manifest、pointer、postcheck、workflow-runs)共享这些枚举。
|
|
5
|
-
* SillyHub 侧直接使用常量值,不需要猜字符串。
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
// ── scan 阶段状态 ──
|
|
9
|
-
export const SCAN_STATUS = Object.freeze({
|
|
10
|
-
PENDING: 'pending', // scan 未开始
|
|
11
|
-
IN_PROGRESS: 'in_progress', // scan 进行中
|
|
12
|
-
SUCCESS: 'success', // scan 成功(所有检查通过)
|
|
13
|
-
COMPLETED_WITH_WARNINGS: 'completed_with_warnings', // scan 成功但有警告
|
|
14
|
-
FAILED_POST_CHECK: 'failed_post_check', // scan 失败(post-check 不通过)
|
|
15
|
-
})
|
|
16
|
-
|
|
17
|
-
// ── 平台指针状态 ──
|
|
18
|
-
export const POINTER_STATUS = Object.freeze({
|
|
19
|
-
ACTIVE: 'active', // 指针活跃,任务进行中
|
|
20
|
-
SCAN_COMPLETED: 'scan_completed', // scan 已完成
|
|
21
|
-
STALE: 'stale', // 指针过时(完成超过 24h,建议清理)
|
|
22
|
-
CORRUPTED: 'corrupted', // 指针损坏(缺少必要字段)
|
|
23
|
-
})
|
|
24
|
-
|
|
25
|
-
// ── workflow 检查状态 ──
|
|
26
|
-
export const WORKFLOW_STATUS = Object.freeze({
|
|
27
|
-
PASS: 'pass',
|
|
28
|
-
FAIL: 'fail',
|
|
29
|
-
SKIPPED: 'skipped',
|
|
30
|
-
})
|
|
31
|
-
|
|
32
|
-
// ── postcheck 检查严重级别 ──
|
|
33
|
-
export const CHECK_SEVERITY = Object.freeze({
|
|
34
|
-
FAILED: 'failed',
|
|
35
|
-
WARNING: 'warning',
|
|
36
|
-
PASSED: 'passed',
|
|
37
|
-
})
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
export
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
'
|
|
76
|
-
'
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
'
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
// 各自用途正确,不在本清单范围。)
|
|
99
|
-
export const AUXILIARY_STAGES = Object.freeze([
|
|
100
|
-
'scan',
|
|
101
|
-
'quick',
|
|
102
|
-
'explore',
|
|
103
|
-
'archive',
|
|
104
|
-
'status',
|
|
105
|
-
'doctor',
|
|
106
|
-
])
|
|
1
|
+
/**
|
|
2
|
+
* SillySpec 平台状态枚举
|
|
3
|
+
*
|
|
4
|
+
* 所有平台产物(manifest、pointer、postcheck、workflow-runs)共享这些枚举。
|
|
5
|
+
* SillyHub 侧直接使用常量值,不需要猜字符串。
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
// ── scan 阶段状态 ──
|
|
9
|
+
export const SCAN_STATUS = Object.freeze({
|
|
10
|
+
PENDING: 'pending', // scan 未开始
|
|
11
|
+
IN_PROGRESS: 'in_progress', // scan 进行中
|
|
12
|
+
SUCCESS: 'success', // scan 成功(所有检查通过)
|
|
13
|
+
COMPLETED_WITH_WARNINGS: 'completed_with_warnings', // scan 成功但有警告
|
|
14
|
+
FAILED_POST_CHECK: 'failed_post_check', // scan 失败(post-check 不通过)
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
// ── 平台指针状态 ──
|
|
18
|
+
export const POINTER_STATUS = Object.freeze({
|
|
19
|
+
ACTIVE: 'active', // 指针活跃,任务进行中
|
|
20
|
+
SCAN_COMPLETED: 'scan_completed', // scan 已完成
|
|
21
|
+
STALE: 'stale', // 指针过时(完成超过 24h,建议清理)
|
|
22
|
+
CORRUPTED: 'corrupted', // 指针损坏(缺少必要字段)
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
// ── workflow 检查状态 ──
|
|
26
|
+
export const WORKFLOW_STATUS = Object.freeze({
|
|
27
|
+
PASS: 'pass',
|
|
28
|
+
FAIL: 'fail',
|
|
29
|
+
SKIPPED: 'skipped',
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
// ── postcheck 检查严重级别 ──
|
|
33
|
+
export const CHECK_SEVERITY = Object.freeze({
|
|
34
|
+
FAILED: 'failed',
|
|
35
|
+
WARNING: 'warning',
|
|
36
|
+
PASSED: 'passed',
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* 判断指针是否过时(完成超过 24h)
|
|
41
|
+
*/
|
|
42
|
+
export function isPointerStale(pointer) {
|
|
43
|
+
if (!pointer.completedAt) return false
|
|
44
|
+
const completed = new Date(pointer.completedAt)
|
|
45
|
+
const age = Date.now() - completed.getTime()
|
|
46
|
+
return age > 24 * 60 * 60 * 1000
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* 判断指针是否损坏(缺少必要字段)
|
|
51
|
+
*/
|
|
52
|
+
export function isPointerCorrupted(pointer) {
|
|
53
|
+
return !pointer || !pointer.specRoot || !pointer.savedAt
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// ── scan 必需文档清单(stage-contract validator 与 scan-postcheck 共用单一真相源)──
|
|
57
|
+
// 曾分两处硬编码同一份清单,加新文档需同步改两处,易分叉。现收敛到此处一处。
|
|
58
|
+
export const SCAN_REQUIRED_DOCS = Object.freeze([
|
|
59
|
+
'ARCHITECTURE.md',
|
|
60
|
+
'CONVENTIONS.md',
|
|
61
|
+
'STRUCTURE.md',
|
|
62
|
+
'INTEGRATIONS.md',
|
|
63
|
+
'TESTING.md',
|
|
64
|
+
'CONCERNS.md',
|
|
65
|
+
'PROJECT.md',
|
|
66
|
+
])
|
|
67
|
+
|
|
68
|
+
// ── scan quick profile 必需文档子集(4 份核心文档)──
|
|
69
|
+
// quick 档(--quick 显式 或 小项目自动判定)只生成这 4 份;scan-postcheck 按 profile 取清单:
|
|
70
|
+
// mode==='quick' → SCAN_REQUIRED_DOCS_QUICK,否则 → SCAN_REQUIRED_DOCS(7 份)。
|
|
71
|
+
// 由 scan-postcheck.js + complete-handlers.js 透传的 scanProfile.mode 决定取哪份。
|
|
72
|
+
export const SCAN_REQUIRED_DOCS_QUICK = Object.freeze([
|
|
73
|
+
'PROJECT.md',
|
|
74
|
+
'ARCHITECTURE.md',
|
|
75
|
+
'CONVENTIONS.md',
|
|
76
|
+
'STRUCTURE.md',
|
|
77
|
+
])
|
|
78
|
+
|
|
79
|
+
// ── 辅助 stage 清单(stage-contract 转移规则 与 stages/index 注册表 共用单一真相源)──
|
|
80
|
+
// 曾在 stage-contract.js 与 stages/index.js 逐字重复定义,新增/调整辅助 stage 需同步改两处
|
|
81
|
+
// 易分叉。现收敛一处。(注意:主流程序 STAGE_ORDER/mainFlowStages/flowStages 语义不同、
|
|
82
|
+
// 各自用途正确,不在本清单范围。)
|
|
83
|
+
export const AUXILIARY_STAGES = Object.freeze([
|
|
84
|
+
'scan',
|
|
85
|
+
'quick',
|
|
86
|
+
'explore',
|
|
87
|
+
'archive',
|
|
88
|
+
'status',
|
|
89
|
+
'doctor',
|
|
90
|
+
])
|
|
91
|
+
|
|
92
|
+
// ── 只读辅助 stage 清单(查询型辅助阶段,可读短路不触碰进度库)──
|
|
93
|
+
// 供 command.js 判断 status/doctor 等查询型辅助阶段执行只读短路(FR-04 / D-005@v2)。
|
|
94
|
+
export const READONLY_AUXILIARY_STAGES = Object.freeze([
|
|
95
|
+
'status',
|
|
96
|
+
'doctor',
|
|
97
|
+
])
|
package/src/db-engine.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// 封装 node:sqlite 内置模块 + 消解 better-sqlite3 → node:sqlite 三缺口:
|
|
3
3
|
// pragma → exec / transaction → 手写 SAVEPOINT / pluck → Object.values 取首列
|
|
4
4
|
// 供 db.js 与 doctor-diagnostics 共用(方案 B 单一换引擎点,D-002)。
|
|
5
|
-
// node:sqlite 在 node v22.
|
|
5
|
+
// node:sqlite 在 node v22.13.0+ 无需 --experimental-sqlite flag(仍发 ExperimentalWarning)。
|
|
6
6
|
// Author: sillyspec execute W1 task-02
|
|
7
7
|
// Created: 2026-08-11
|
|
8
8
|
import { DatabaseSync } from 'node:sqlite';
|
package/src/docs-check.js
CHANGED
|
@@ -12,12 +12,15 @@
|
|
|
12
12
|
* 只读校验(不修改任何被校验文件);纯 Node 内置模块零依赖(D-008:glob 手写 walker,
|
|
13
13
|
* 仅「目录递归」「目录单层」「字面路径」三形态);Windows 路径归一化;兼容 CRLF/LF。
|
|
14
14
|
*/
|
|
15
|
-
import { readFileSync, existsSync, readdirSync } from 'node:fs'
|
|
15
|
+
import { readFileSync, existsSync, readdirSync, statSync } from 'node:fs'
|
|
16
16
|
import { join } from 'node:path'
|
|
17
17
|
import jsYaml from 'js-yaml'
|
|
18
18
|
|
|
19
19
|
/** 提取 file.js:line / file.js:start-end 引用(.js/.mjs,反引号包裹与裸文本均命中,全文扫描 D-006) */
|
|
20
|
-
|
|
20
|
+
const REF_RE = /([A-Za-z0-9_.\-\/]+\.(?:js|mjs)):(\d+)(?:-(\d+))?/g
|
|
21
|
+
|
|
22
|
+
/** 缺省扫描范围:docs/ + .sillyspec/docs/(scan/modules 产物同是文档,失效即该暴露;2026-08-16 用户裁决改缺省,见 doc-consistency-debt.md §八) */
|
|
23
|
+
const DEFAULT_DOC_PATHS = ['docs/**/*.md', '.sillyspec/docs/**/*.md']
|
|
21
24
|
|
|
22
25
|
/**
|
|
23
26
|
* 读 local.yaml 的 docs-check 段(best-effort,绝不抛;缺文件/无段 → 全缺省)。
|
|
@@ -187,6 +190,26 @@ export function walkGlob(projectRoot, pattern, skip = []) {
|
|
|
187
190
|
(sn.includes('*') && matchSimple(rel, sn))
|
|
188
191
|
})
|
|
189
192
|
const matchExt = (name, ext) => name.endsWith(ext)
|
|
193
|
+
// 形态 0:**/*.ext(根级递归,无 base 前缀——B11b:原被形态 2 误解析为字面目录 `**` 静默 0 命中)
|
|
194
|
+
let m0 = norm(pattern).match(/^\*\*\/\*(\.[A-Za-z0-9]+)$/)
|
|
195
|
+
if (m0) {
|
|
196
|
+
const [, ext] = m0
|
|
197
|
+
const out = []
|
|
198
|
+
const rec = (dir, rel, depth) => {
|
|
199
|
+
if (depth > 12) return
|
|
200
|
+
let entries
|
|
201
|
+
try { entries = readdirSync(dir, { withFileTypes: true }) } catch { return }
|
|
202
|
+
for (const e of entries) {
|
|
203
|
+
if (e.name === 'node_modules' || e.name === '.git') continue
|
|
204
|
+
const relPath = rel ? `${rel}/${e.name}` : e.name
|
|
205
|
+
if (isSkipped(relPath)) continue
|
|
206
|
+
if (e.isDirectory()) rec(join(dir, e.name), relPath, depth + 1)
|
|
207
|
+
else if (matchExt(e.name, ext)) out.push(relPath)
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
rec(projectRoot, '', 0)
|
|
211
|
+
return out
|
|
212
|
+
}
|
|
190
213
|
// 形态 1:**/*.ext
|
|
191
214
|
let m = norm(pattern).match(/^(.+?)\/\*\*\/\*(\.[A-Za-z0-9]+)$/)
|
|
192
215
|
if (m) {
|
|
@@ -224,7 +247,13 @@ export function walkGlob(projectRoot, pattern, skip = []) {
|
|
|
224
247
|
}
|
|
225
248
|
// 形态 3:字面路径(不含任何通配元字符才直传;含 ?、{ }、[ ] 等不支持形态落 error)
|
|
226
249
|
if (!/[*?{}\[\]]/.test(pattern)) {
|
|
227
|
-
|
|
250
|
+
const abs = join(projectRoot, pattern)
|
|
251
|
+
if (!existsSync(abs)) return []
|
|
252
|
+
// B11b:目录字面量抛配置错误(原直传 readFileSync 撞 EISDIR 裸崩 exit 1;契约应 exit 2)
|
|
253
|
+
if (statSync(abs).isDirectory()) {
|
|
254
|
+
throw new DocsCheckConfigError(`路径是目录不是文档:${pattern}(文档路径应为 .md 文件或 glob 形态 **/*.ext / dir/**/*.ext / dir/*.ext)`)
|
|
255
|
+
}
|
|
256
|
+
return [norm(pattern)]
|
|
228
257
|
}
|
|
229
258
|
throw new DocsCheckConfigError(`不支持的 glob 形态:${pattern}(当前仅支持 dir/**/*.ext、dir/*.ext、字面路径)`)
|
|
230
259
|
}
|
|
@@ -247,8 +276,8 @@ function relDisplay(absPath, projectRoot) {
|
|
|
247
276
|
export function runDocsCheck(opts) {
|
|
248
277
|
// paths/docs 显式传 null(readDocsCheckConfig 无 local.yaml 段时的回退值)须落回缺省 glob,
|
|
249
278
|
// 解构默认值只挡 undefined 不挡 null——docs-check 无配置裸跑曾因此 null.flatMap 崩溃。
|
|
250
|
-
const { projectRoot, docs = null, paths: rawPaths =
|
|
251
|
-
const paths = Array.isArray(rawPaths) && rawPaths.length > 0 ? rawPaths :
|
|
279
|
+
const { projectRoot, docs = null, paths: rawPaths = DEFAULT_DOC_PATHS, skip = [], keywordAssert = true } = opts || {}
|
|
280
|
+
const paths = Array.isArray(rawPaths) && rawPaths.length > 0 ? rawPaths : DEFAULT_DOC_PATHS
|
|
252
281
|
const warnings = []
|
|
253
282
|
if (keywordAssert === false) warnings.push('关键词断言已关闭(keywordAssert=false),仅做存在性校验')
|
|
254
283
|
const docFiles = docs && docs.length > 0
|
package/src/git-helper.js
CHANGED
|
@@ -34,7 +34,7 @@ export function safeGit(cwd, args, opts = {}) {
|
|
|
34
34
|
// 格式化(取首行)推迟到最终返回,避免重试分支重复格式化,并保留原始 code 供重试判定。
|
|
35
35
|
const attempt = (t) => {
|
|
36
36
|
try {
|
|
37
|
-
let value = execFileSync('git', fullArgs, { encoding: 'utf8', timeout: t })
|
|
37
|
+
let value = execFileSync('git', fullArgs, { encoding: 'utf8', timeout: t, stdio: ['ignore', 'pipe', 'pipe'] })
|
|
38
38
|
if (trim) value = value.trim()
|
|
39
39
|
return { value, errorObj: null }
|
|
40
40
|
} catch (e) {
|
|
@@ -59,7 +59,7 @@ export function safeGit(cwd, args, opts = {}) {
|
|
|
59
59
|
export function git(cwd, args, opts = {}) {
|
|
60
60
|
const { trim = true, timeout = 5000 } = opts
|
|
61
61
|
const fullArgs = ['-c', `safe.directory=${cwd}`, '-C', cwd, ...args]
|
|
62
|
-
const value = execFileSync('git', fullArgs, { encoding: 'utf8', timeout })
|
|
62
|
+
const value = execFileSync('git', fullArgs, { encoding: 'utf8', timeout, stdio: ['ignore', 'pipe', 'pipe'] })
|
|
63
63
|
return trim ? value.trim() : value
|
|
64
64
|
}
|
|
65
65
|
|
|
@@ -748,7 +748,7 @@ export function shouldBlockWrite(filePath, cwd) {
|
|
|
748
748
|
* @param {string} cwd - 当前工作目录
|
|
749
749
|
* @returns {{ blocked: boolean, reason?: string }}
|
|
750
750
|
*/
|
|
751
|
-
|
|
751
|
+
function shouldBlockBash(command, cwd) {
|
|
752
752
|
if (!command || !command.trim()) return { blocked: false }
|
|
753
753
|
|
|
754
754
|
const callerCwd = cwd || process.cwd()
|
package/src/index.js
CHANGED
|
@@ -8,16 +8,19 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, statSync, writeFileSync } from 'fs';
|
|
10
10
|
import { writeAtomicSync } from './fs-atomic.js';
|
|
11
|
-
import { join, resolve } from 'path';
|
|
11
|
+
import { basename, join, resolve } from 'path';
|
|
12
12
|
import { execSync } from 'child_process';
|
|
13
13
|
import { git } from './git-helper.js';
|
|
14
14
|
import { getVersion } from './version.js';
|
|
15
|
-
|
|
15
|
+
|
|
16
|
+
// E22(性能#2):progress.js(拖 db.js→node:sqlite 链 ~48ms)与 run/shared.js(拖 stages 全家
|
|
17
|
+
// ~30ms)原先为顶层静态 import——最轻命令(--version/help)白付 ~78ms 启动税。两条链无模块级
|
|
18
|
+
// 使用点(printUsage 纯文本),改在 main() 早退之后动态加载,轻路径零加载;重路径首个使用点
|
|
19
|
+
// 之前完成加载,行为零变化。
|
|
16
20
|
|
|
17
21
|
// ── CLI 入口 ──
|
|
18
22
|
|
|
19
|
-
// did-you-mean 从 ./run/shared.js 复用(命令级 + flag 级 typo 建议)。
|
|
20
|
-
import { didYouMean, assertSafeChangeName, resolveSpecDir, detectWorktreeSpecDrift } from './run/shared.js'
|
|
23
|
+
// did-you-mean 从 ./run/shared.js 复用(命令级 + flag 级 typo 建议)。E22:同上改惰性动态加载。
|
|
21
24
|
function printUsage() {
|
|
22
25
|
console.log(`
|
|
23
26
|
SillySpec CLI — 规范驱动开发工具包
|
|
@@ -56,6 +59,8 @@ SillySpec CLI — 规范驱动开发工具包
|
|
|
56
59
|
--confirm 完成时确认接受变更审计
|
|
57
60
|
scan: --quick | --standard | --deep 显式选择 profile(优先于规模自动判定;三档互斥)
|
|
58
61
|
--force-rescan 覆盖已有 scan 文档保护
|
|
62
|
+
--diff [--base <commit>] [--full] [--report] scan 文档 vs 源码漂移清单(纯只读)
|
|
63
|
+
(子命令: sillyspec scan diff 等价 --diff)
|
|
59
64
|
archive: --confirm 归档确认(必须)
|
|
60
65
|
auto: --mode <模式> 显式指定流程模式
|
|
61
66
|
平台: --runtime-root <path> / --workspace-id <id> / --scan-run-id <id>
|
|
@@ -159,6 +164,10 @@ async function main() {
|
|
|
159
164
|
process.exit(0);
|
|
160
165
|
}
|
|
161
166
|
|
|
167
|
+
// E22:重路径统一加载(轻路径 --version/help 已早退,未付此税)。
|
|
168
|
+
const { ProgressManager, resolvePlatformSpecDir } = await import('./progress.js');
|
|
169
|
+
const { didYouMean, assertSafeChangeName, resolveSpecDir, detectWorktreeSpecDrift } = await import('./run/shared.js');
|
|
170
|
+
|
|
162
171
|
// 解析全局选项
|
|
163
172
|
let json = false;
|
|
164
173
|
let saveWorkflowRunFlag = false;
|
|
@@ -308,21 +317,21 @@ async function main() {
|
|
|
308
317
|
break;
|
|
309
318
|
case 'set-stage': {
|
|
310
319
|
const setStageName = filteredArgs[2];
|
|
311
|
-
if (!setStageName) { console.log('❌ 用法: sillyspec progress set-stage <stage> [--change <name>]');
|
|
320
|
+
if (!setStageName) { console.log('❌ 用法: sillyspec progress set-stage <stage> [--change <name>]'); process.exit(2); }
|
|
312
321
|
pm.setStage(dir, setStageName, progChangeName);
|
|
313
322
|
break;
|
|
314
323
|
}
|
|
315
324
|
case 'add-step': {
|
|
316
325
|
const addStepStage = filteredArgs[2];
|
|
317
326
|
const addStepName = filteredArgs[3];
|
|
318
|
-
if (!addStepStage || !addStepName) { console.log('❌ 用法: sillyspec progress add-step <stage> <step-name> [--change <name>]');
|
|
327
|
+
if (!addStepStage || !addStepName) { console.log('❌ 用法: sillyspec progress add-step <stage> <step-name> [--change <name>]'); process.exit(2); }
|
|
319
328
|
pm.addStep(dir, addStepStage, addStepName, progChangeName);
|
|
320
329
|
break;
|
|
321
330
|
}
|
|
322
331
|
case 'update-step': {
|
|
323
332
|
const updStepStage = filteredArgs[2];
|
|
324
333
|
const updStepName = filteredArgs[3];
|
|
325
|
-
if (!updStepStage || !updStepName) { console.log('❌ 用法: sillyspec progress update-step <stage> <step-name> --status <status> [--output <text>] [--change <name>]');
|
|
334
|
+
if (!updStepStage || !updStepName) { console.log('❌ 用法: sillyspec progress update-step <stage> <step-name> --status <status> [--output <text>] [--change <name>]'); process.exit(2); }
|
|
326
335
|
let updStatus = null, updOutput = undefined;
|
|
327
336
|
for (let ai = 0; ai < args.length; ai++) {
|
|
328
337
|
if (args[ai] === '--status' && args[ai + 1]) { updStatus = args[ai + 1]; ai++; }
|
|
@@ -334,7 +343,7 @@ async function main() {
|
|
|
334
343
|
}
|
|
335
344
|
case 'complete-stage': {
|
|
336
345
|
const compStageName = filteredArgs[2];
|
|
337
|
-
if (!compStageName) { console.log('❌ 用法: sillyspec progress complete-stage <stage> [--force]');
|
|
346
|
+
if (!compStageName) { console.log('❌ 用法: sillyspec progress complete-stage <stage> [--force]'); process.exit(2); }
|
|
338
347
|
pm.completeStage(dir, compStageName, progChangeName, { force: args.includes('--force') });
|
|
339
348
|
break;
|
|
340
349
|
}
|
|
@@ -357,7 +366,7 @@ async function main() {
|
|
|
357
366
|
if (Object.keys(batchData).length === 0) {
|
|
358
367
|
console.log('用法: sillyspec progress batch --total 100 --completed 73');
|
|
359
368
|
console.log(' sillyspec progress batch --status');
|
|
360
|
-
|
|
369
|
+
process.exit(2);
|
|
361
370
|
}
|
|
362
371
|
pm.updateBatchProgress(dir, batchData, progChangeName);
|
|
363
372
|
console.log('✅ 批量进度已更新');
|
|
@@ -366,6 +375,7 @@ async function main() {
|
|
|
366
375
|
}
|
|
367
376
|
default:
|
|
368
377
|
console.log('用法: sillyspec progress <init|show|validate|reset|set-stage|add-step|update-step|complete-stage>');
|
|
378
|
+
process.exit(2);
|
|
369
379
|
}
|
|
370
380
|
break;
|
|
371
381
|
}
|
|
@@ -387,7 +397,11 @@ async function main() {
|
|
|
387
397
|
// 只传 cwd 致 --spec-dir 对 gate verify 无效。不传 --spec-dir 时 specBase 走默认
|
|
388
398
|
// resolveSpecDir(cwd),向后兼容。
|
|
389
399
|
const gateOpts = { cwd: dir };
|
|
390
|
-
|
|
400
|
+
// A4:平台指针 / 显式 --spec-dir / 本地 fallback 三合一解析 specBase——平台模式(指针指向外部
|
|
401
|
+
// specDir)下 gate 也能核验(原只传 cwd 致 runGate 的 resolveSpecDir 指向本地孤儿库,恒
|
|
402
|
+
// 「无法核验」exit 2)。resolvePlatformSpecDir 指针失效时抛 PointerUnreachableError,由顶层 catch 优雅 fail-closed。
|
|
403
|
+
const gateSpecBase = resolvePlatformSpecDir(dir, specDir);
|
|
404
|
+
if (gateSpecBase) gateOpts.specBase = gateSpecBase;
|
|
391
405
|
// drift 锚定(坑 execute-runs-isolation,对齐 command.js execute drift 守卫 + machine-interface
|
|
392
406
|
// runGate 已扩展的 specDriftAnchor 入参):gate 是顶层命令不走 runCommand/drift 守卫,worktree cwd
|
|
393
407
|
// 下 execute 段 marker 读取会落副本 .runtime。仅在未显式 --spec-dir 时算(对齐守卫条件 !specDir),
|
|
@@ -442,7 +456,9 @@ async function main() {
|
|
|
442
456
|
// derive verify-test facet 内部同样调 runVerifyTestCheck 依赖 specBase,平台模式
|
|
443
457
|
// 下需透传;不传 --spec-dir 时走默认 resolveSpecDir(cwd),向后兼容。
|
|
444
458
|
const deriveOpts = { cwd: dir };
|
|
445
|
-
|
|
459
|
+
// A4:同 gate case——平台指针/--spec-dir/本地 fallback 三合一解析 specBase。
|
|
460
|
+
const deriveSpecBase = resolvePlatformSpecDir(dir, specDir);
|
|
461
|
+
if (deriveSpecBase) deriveOpts.specBase = deriveSpecBase;
|
|
446
462
|
// drift 锚定(同 gate case,对齐 command.js drift 守卫 + machine-interface runDerive 的 specDriftAnchor
|
|
447
463
|
// 入参):derive 顶层命令不走 drift 守卫,worktree cwd 下 task-reviews facet 的 execute-run-id marker
|
|
448
464
|
// 读取会落副本。未显式 --spec-dir 时算 anchor,runDerive task-reviews 的 resolveRuntimeRoot 读主仓 marker。
|
|
@@ -629,8 +645,54 @@ async function main() {
|
|
|
629
645
|
}
|
|
630
646
|
throw e;
|
|
631
647
|
}
|
|
648
|
+
} else if (docsSubCmd === 'gate') {
|
|
649
|
+
// docs gate ratchet(docs-signals-o12 cff7479 产品化,pre-push 第三道关):docs check
|
|
650
|
+
// 失效数 ≤ 基线放行,超基线拦。基线 .sillyspec/docs-check-baseline 首次须显式
|
|
651
|
+
// --init-baseline 立线(fail-closed,不悄悄合法化存量)。
|
|
652
|
+
// ⚠ 2026-08-16 补接线:cff7479 落地时漏了本 CLI 分支,.husky/pre-push 一直在跑
|
|
653
|
+
// usage 输出且 exit 0——gate 形同虚设(docs-signals 范围内文件+hook 已就位,仅此分支缺)。
|
|
654
|
+
// B9(2026-08-16):flag 白名单化(对齐 docs check 分支)——未知 --xxx 显式 exit 2
|
|
655
|
+
// (interface-contract §1.3b 宣称),并接线 --paths 透传 runDocsGate.checkOpts(原被忽略)。
|
|
656
|
+
const GATE_BARE_FLAGS = ['--init-baseline'];
|
|
657
|
+
const GATE_PAIRED_FLAGS = ['--paths'];
|
|
658
|
+
const rawGateArgs = filteredArgs.slice(2);
|
|
659
|
+
let initBaseline = false;
|
|
660
|
+
let cliGatePaths = null;
|
|
661
|
+
for (let i = 0; i < rawGateArgs.length; i++) {
|
|
662
|
+
const a = rawGateArgs[i];
|
|
663
|
+
if (a === '--paths' && rawGateArgs[i + 1] !== undefined) {
|
|
664
|
+
cliGatePaths = rawGateArgs[i + 1].split(',').map(s => s.trim()).filter(Boolean);
|
|
665
|
+
i++;
|
|
666
|
+
} else if (a === '--paths') {
|
|
667
|
+
console.error('❌ docs gate: --paths 缺值(逗号分隔 glob,如 --paths "docs/**/*.md")');
|
|
668
|
+
process.exit(2);
|
|
669
|
+
} else if (GATE_BARE_FLAGS.includes(a)) {
|
|
670
|
+
if (a === '--init-baseline') initBaseline = true;
|
|
671
|
+
} else if (a.startsWith('--')) {
|
|
672
|
+
console.error(`❌ docs gate: 未知 flag「${a}」。已知 flag:${[...GATE_BARE_FLAGS, ...GATE_PAIRED_FLAGS].join(' ')}(--json 为全局 flag)`);
|
|
673
|
+
process.exit(2);
|
|
674
|
+
} else {
|
|
675
|
+
console.error(`❌ docs gate: 多余位置参数「${a}」。用法:sillyspec docs gate [--init-baseline] [--paths <glob,...>] [--json]`);
|
|
676
|
+
process.exit(2);
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
const { runDocsGate } = await import('./docs-gate.js');
|
|
680
|
+
// specBase:平台模式(specDir 指针)优先,本地回退 <repo>/.sillyspec——基线文件锚定
|
|
681
|
+
// 进度库根(与 .runtime 同级),跨命令稳定。
|
|
682
|
+
const gateSpecBase = specDir || join(dir, '.sillyspec');
|
|
683
|
+
const g = await runDocsGate(
|
|
684
|
+
{ projectRoot: dir, specBase: gateSpecBase, initBaseline },
|
|
685
|
+
cliGatePaths ? { paths: cliGatePaths } : undefined
|
|
686
|
+
);
|
|
687
|
+
if (json) {
|
|
688
|
+
console.log(JSON.stringify(g, null, 2));
|
|
689
|
+
} else {
|
|
690
|
+
console.log(g.message);
|
|
691
|
+
}
|
|
692
|
+
process.exit(g.exitCode);
|
|
632
693
|
} else {
|
|
633
|
-
console.log('用法: sillyspec docs migrate | sillyspec docs check [--paths <glob,...>] [--json] [--suggest] [--
|
|
694
|
+
console.log('用法: sillyspec docs migrate | sillyspec docs check [--paths <glob,...>] [--json] [--suggest] | sillyspec docs gate [--init-baseline] [--json]');
|
|
695
|
+
process.exit(2);
|
|
634
696
|
}
|
|
635
697
|
break;
|
|
636
698
|
}
|
|
@@ -766,7 +828,27 @@ async function main() {
|
|
|
766
828
|
await runCommand([command, ...filteredArgs.slice(1)], doctorEffectiveDir, specDir, { json });
|
|
767
829
|
break;
|
|
768
830
|
}
|
|
769
|
-
case 'scan':
|
|
831
|
+
case 'scan': {
|
|
832
|
+
// scan diff 子命令(D-001@v1:接线唯一入口 index.js,非 command.js 裸 token 静默吞)。
|
|
833
|
+
// 纯只读比较命令:跳过下方共享块的 triggerPullActiveChange(不触发网络 pull),
|
|
834
|
+
// 也不走 scan 主流程(--standard/--deep 分支不动)。解析 --base/--full/--report 后转发 scan-diff。
|
|
835
|
+
if (filteredArgs[1] === 'diff') {
|
|
836
|
+
const { runScanDiff } = await import('./scan-diff.js')
|
|
837
|
+
const diffBaseIdx = filteredArgs.indexOf('--base')
|
|
838
|
+
const diffBase = diffBaseIdx >= 0 && filteredArgs[diffBaseIdx + 1] ? filteredArgs[diffBaseIdx + 1] : null
|
|
839
|
+
// specBase/projectName 与 scan 主流程同口径(scan-profile.js:specBase=进度库根,projectName=仓库根 basename)
|
|
840
|
+
const diffEffectiveDir = specDir ? dir : resolveEffectiveDir(dir)
|
|
841
|
+
process.exitCode = runScanDiff({
|
|
842
|
+
projectRoot: diffEffectiveDir,
|
|
843
|
+
specBase: specDir || join(diffEffectiveDir, '.sillyspec'),
|
|
844
|
+
projectName: basename(diffEffectiveDir),
|
|
845
|
+
base: diffBase,
|
|
846
|
+
full: filteredArgs.includes('--full'),
|
|
847
|
+
report: filteredArgs.includes('--report'),
|
|
848
|
+
})
|
|
849
|
+
break
|
|
850
|
+
}
|
|
851
|
+
}
|
|
770
852
|
case 'status':
|
|
771
853
|
case 'quick':
|
|
772
854
|
case 'explore':
|
|
@@ -774,6 +856,7 @@ async function main() {
|
|
|
774
856
|
case 'plan':
|
|
775
857
|
case 'execute':
|
|
776
858
|
case 'verify':
|
|
859
|
+
case 'auto':
|
|
777
860
|
case 'archive': {
|
|
778
861
|
const { runCommand } = await import('./run.js')
|
|
779
862
|
const stageArgs = [command, ...filteredArgs.slice(1)]
|
package/src/init.js
CHANGED
|
@@ -470,6 +470,14 @@ export async function cmdInit(projectDir, options = {}) {
|
|
|
470
470
|
const version = getVersion();
|
|
471
471
|
const resolvedSpecDir = specDir ? resolve(specDir) : null;
|
|
472
472
|
|
|
473
|
+
// C14c:绿地/棕地引导——必须在 doInstall 之前捕获目录状态(init 会创建 .sillyspec/CLAUDE.md/
|
|
474
|
+
// .gitignore 等,事后检测会误判);棕地(已有非隐藏源码)建议 scan,绿地(空目录)建议 brainstorm。
|
|
475
|
+
let brownfield = false;
|
|
476
|
+
try {
|
|
477
|
+
const entries = readdirSync(projectDir, { withFileTypes: true });
|
|
478
|
+
brownfield = entries.some(e => !e.name.startsWith('.'));
|
|
479
|
+
} catch { brownfield = false; }
|
|
480
|
+
|
|
473
481
|
// ── 交互式模式(--interactive 或 -i)──
|
|
474
482
|
if (interactive && isTTY()) {
|
|
475
483
|
// 欢迎画面
|
|
@@ -603,9 +611,12 @@ export async function cmdInit(projectDir, options = {}) {
|
|
|
603
611
|
const specDisplay = resolvedSpecDir || '.sillyspec';
|
|
604
612
|
console.log(` 📁 规范目录: ${chalk.cyan(specDisplay)}`);
|
|
605
613
|
console.log('');
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
614
|
+
// C14c:绿地/棕地引导区分(README:73 棕地应 scan)——early 捕获的目录状态(init 前),
|
|
615
|
+
// 棕地有代码零上下文进 brainstorm 会迷失,建议先 scan 生成架构文档。
|
|
616
|
+
const nextCmd = brownfield ? '/sillyspec:scan' : '/sillyspec:brainstorm';
|
|
617
|
+
console.log(' 下一步:使用 AI 技能开始工作' + (brownfield ? '(检测到现有代码,建议先扫描生成架构文档)' : ''));
|
|
618
|
+
console.log(` OpenClaw: ${chalk.bold(nextCmd)}`);
|
|
619
|
+
console.log(` Claude Code: ${chalk.bold(nextCmd)}`);
|
|
609
620
|
console.log('');
|
|
610
621
|
console.log(chalk.dim(' 💡 增强能力:sillyspec setup(安装 MCP 工具)'));
|
|
611
622
|
console.log('');
|
package/src/machine-interface.js
CHANGED
|
@@ -113,14 +113,16 @@ export function buildEnvelope({
|
|
|
113
113
|
*/
|
|
114
114
|
export async function runGate(stage, changeName, { cwd, specBase, runtimeRoot, specDriftAnchor, ctx = null } = {}) {
|
|
115
115
|
const specRoot = specBase || resolveSpecDir(cwd);
|
|
116
|
-
|
|
116
|
+
// A4:pm 用 specRoot(而非默认 resolveSpecDir(cwd))——--spec-dir/平台模式下 specBase 是真实
|
|
117
|
+
// specDir,默认解析会指向本地孤儿库,与下方 planContent/changeDir 的 specRoot 两套事实源混拼。
|
|
118
|
+
const pm = new ProgressManager({ specDir: specRoot });
|
|
117
119
|
|
|
118
120
|
try {
|
|
119
121
|
// ── 只读契约守卫(D-002@v1):db 不存在时 fail-closed 返回 exit 2,不触发建库落盘 ──
|
|
120
122
|
// pm.read → _ensureDB → db.init() 在 db 不存在/schema 戳落后时会 _createSchema + _save 落盘
|
|
121
123
|
// (db.js:init),这会让一个声明只读的查询命令在 spec 目录凭空建 sillyspec.db,违反只读契约。
|
|
122
124
|
// db 还没建 = 该变更从未跑过流程,无从核验 → 显式返回「无法核验」而非建库。
|
|
123
|
-
const dbPath = join(
|
|
125
|
+
const dbPath = join(specRoot, '.runtime', 'sillyspec.db');
|
|
124
126
|
if (!existsSync(dbPath)) {
|
|
125
127
|
const envelope = buildEnvelope({
|
|
126
128
|
command: 'gate',
|
|
@@ -356,11 +358,12 @@ export async function runDerive(facet, changeName, { cwd, specBase, runtimeRoot,
|
|
|
356
358
|
}
|
|
357
359
|
|
|
358
360
|
const specRoot = specBase || resolveSpecDir(cwd);
|
|
359
|
-
|
|
361
|
+
// A4:同 runGate——pm 用 specRoot 统一事实源,平台/--spec-dir 模式不读本地孤儿库。
|
|
362
|
+
const pm = new ProgressManager({ specDir: specRoot });
|
|
360
363
|
|
|
361
364
|
try {
|
|
362
365
|
// ── 只读契约守卫(D-002@v1,同 runGate):db 不存在时 fail-closed 返回 exit 2,不建库落盘 ──
|
|
363
|
-
const dbPath = join(
|
|
366
|
+
const dbPath = join(specRoot, '.runtime', 'sillyspec.db');
|
|
364
367
|
if (!existsSync(dbPath)) {
|
|
365
368
|
const envelope = buildEnvelope({
|
|
366
369
|
command: 'derive',
|
|
@@ -275,7 +275,10 @@ export class StageMachine {
|
|
|
275
275
|
}
|
|
276
276
|
|
|
277
277
|
// 找到第一个有 pending/waiting/failed 步骤的 in-progress 阶段
|
|
278
|
+
// C14b:排除 scan(同下方主流程推荐循环)——scan 是 auxiliary,中途未完成时恒处
|
|
279
|
+
// STAGE_ORDER 首位且 in-progress,会把「下一步」劫持为 scan,掩盖主流程真实待办。
|
|
278
280
|
for (const s of STAGE_ORDER) {
|
|
281
|
+
if (s === 'scan') continue
|
|
279
282
|
const sd = data.stages[s];
|
|
280
283
|
if (!sd) continue;
|
|
281
284
|
if (sd.status === 'in-progress' && sd.steps) {
|