sillyspec 3.27.9 → 3.27.12
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 +214 -214
- package/.claude/skills/sillyspec-plan/SKILL.md +156 -156
- package/.claude/skills/sillyspec-quick/SKILL.md +116 -116
- package/README.md +3 -2
- package/package.json +1 -1
- package/src/change-delete.js +181 -0
- package/src/commit-suggest.js +22 -0
- package/src/contract-matrix.js +20 -4
- package/src/decision-distill.js +33 -14
- package/src/docs-check.js +138 -2
- package/src/endpoint-extractor.js +7 -4
- package/src/facade-hint.js +170 -0
- package/src/index.js +259 -22
- package/src/init.js +104 -27
- package/src/module-changelog.js +116 -0
- package/src/progress/change-registry.js +28 -0
- package/src/progress/shared.js +6 -0
- package/src/progress/stage-machine.js +85 -8
- package/src/progress.js +3 -0
- package/src/run/command.js +60 -0
- package/src/run/complete-handlers.js +25 -1
- package/src/run/gates.js +62 -5
- package/src/run/prompt.js +12 -1
- package/src/run/shared.js +81 -23
- package/src/run/stage.js +21 -1
- package/src/spec-sync.js +88 -1
- package/src/stages/archive.js +5 -0
- package/src/stages/execute.js +3 -1
- package/src/sync.js +453 -17
- package/src/verify-postcheck.js +47 -6
- package/src/workflow.js +4 -0
- package/src/worktree-apply.js +62 -4
- package/src/worktree.js +6 -4
- package/templates/{claude-instruction.md → agents-instruction.md} +2 -2
- package/templates/prompts/verify-probes.md +2 -1
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* facade-hint.js — plan 门「透传必经文件」候选探测(advisory)。
|
|
3
|
+
*
|
|
4
|
+
* 坑 plan-facade-files-manual-backfill(2026-08-28 用户实证 ×2):facade 文件(如
|
|
5
|
+
* daemon/service.py——新端点/新模块必须在它里面挂载/转发才对外生效)不进 design §6 /
|
|
6
|
+
* TaskCard allowed_paths,执行期子代理按现实改它 → apply Gate1 拦截 → 人工回补
|
|
7
|
+
* allowed_paths 两轮。本模块在 plan 完成时做静态引用扫描,把候选透传/聚合文件提前
|
|
8
|
+
* 亮给 agent(warning 不阻断):判定是语义,agent 拍板补不补。
|
|
9
|
+
*
|
|
10
|
+
* 两个信号(语言覆盖 py/js/ts/mjs/cjs/jsx/tsx/java 的 import/require 静态文本匹配):
|
|
11
|
+
* - direct:仓内文件 import 了 allowed_paths 里的模块(改被 facade 引用的模块,
|
|
12
|
+
* facade 常需同步改——转发签名/挂载点);
|
|
13
|
+
* - aggregate:仓内文件 import 了该模块同目录下 ≥2 个模块(聚合器/注册表形态,
|
|
14
|
+
* 新增同目录模块几乎必然要在此登记——service.py 挂 router 即此形态)。
|
|
15
|
+
*
|
|
16
|
+
* 启发式 → 恒 advisory:候选只列不改,由 agent 判定补 allowed_paths / design §6,
|
|
17
|
+
* 或确认无关忽略。性能护栏:allowed 条目 > 100 或仓内源码文件 > 5000 时静默跳过。
|
|
18
|
+
*/
|
|
19
|
+
import { existsSync, readdirSync, readFileSync, statSync } from 'fs'
|
|
20
|
+
import { join, dirname, basename, extname, resolve } from 'path'
|
|
21
|
+
|
|
22
|
+
const SOURCE_EXTS = new Set(['.py', '.js', '.ts', '.jsx', '.tsx', '.mjs', '.cjs', '.java'])
|
|
23
|
+
const SKIP_DIRS = new Set([
|
|
24
|
+
'.git', 'node_modules', 'dist', 'build', 'out', '.next', '__pycache__',
|
|
25
|
+
'.venv', 'venv', 'env', '.sillyspec', '.worktrees', '.claude', '.runtime',
|
|
26
|
+
'coverage', 'target', '.idea', '.vscode',
|
|
27
|
+
])
|
|
28
|
+
const MAX_ALLOWED_ENTRIES = 100
|
|
29
|
+
const MAX_REPO_FILES = 5000
|
|
30
|
+
const MAX_FILE_BYTES = 512 * 1024
|
|
31
|
+
|
|
32
|
+
const toPosix = (p) => String(p).replace(/\\/g, '/')
|
|
33
|
+
|
|
34
|
+
/** 递归收集仓内源码文件(posix 相对路径),带总量护栏。 */
|
|
35
|
+
function walkSourceFiles(root, out = []) {
|
|
36
|
+
if (out.length > MAX_REPO_FILES) return out
|
|
37
|
+
let names
|
|
38
|
+
try { names = readdirSync(root) } catch { return out }
|
|
39
|
+
for (const name of names) {
|
|
40
|
+
if (SKIP_DIRS.has(name)) continue
|
|
41
|
+
const abs = join(root, name)
|
|
42
|
+
let st
|
|
43
|
+
try { st = statSync(abs) } catch { continue }
|
|
44
|
+
if (st.isDirectory()) walkSourceFiles(abs, out)
|
|
45
|
+
else if (st.isFile() && SOURCE_EXTS.has(extname(name).toLowerCase()) && st.size <= MAX_FILE_BYTES) {
|
|
46
|
+
out.push(toPosix(join(root, name)))
|
|
47
|
+
if (out.length > MAX_REPO_FILES) return out
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return out
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** 从源码文本提取 import/require 说明符(py dotted + js/ts path-ish)。 */
|
|
54
|
+
export function extractImportSpecifiers(text, ext) {
|
|
55
|
+
const specs = new Set()
|
|
56
|
+
if (ext === '.py') {
|
|
57
|
+
// from X import a, b → 记 pkg 本身 + 逐名拼 'X.a'/'X.b'(前者保 from X.y import sym 的
|
|
58
|
+
// 直接引用形态,后者保 service.py 聚合挂载形态:from daemon.routers import users, orders)
|
|
59
|
+
for (const m of text.matchAll(/\bfrom\s+([.\w]+)\s+import\s+([^\n#]+)/g)) {
|
|
60
|
+
specs.add(m[1])
|
|
61
|
+
const names = m[2].split(',')
|
|
62
|
+
.map(s => s.trim().split(/\s+as\s+/i)[0].trim())
|
|
63
|
+
.filter(n => /^\w+$/.test(n))
|
|
64
|
+
for (const n of names) specs.add(`${m[1]}.${n}`)
|
|
65
|
+
}
|
|
66
|
+
for (const m of text.matchAll(/(?:^|\n)\s*import\s+([\w.]+)/g)) specs.add(m[1])
|
|
67
|
+
} else if (ext === '.java') {
|
|
68
|
+
for (const m of text.matchAll(/\bimport\s+([\w.]+)\s*;/g)) specs.add(m[1])
|
|
69
|
+
} else {
|
|
70
|
+
for (const m of text.matchAll(/(?:from\s+|require\(\s*|import\(\s*)['"]([^'"]+)['"]/g)) specs.add(m[1])
|
|
71
|
+
}
|
|
72
|
+
return [...specs]
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** 说明符是否指向模块名 mod(`m` / `pkg.m` / `./m` / `../d/m` 等尾部命中形态)。 */
|
|
76
|
+
export function specRefsModule(spec, mod) {
|
|
77
|
+
if (!spec || !mod) return false
|
|
78
|
+
if (spec === mod) return true
|
|
79
|
+
return spec.endsWith('.' + mod) || spec.endsWith('/' + mod)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* 探测 allowed_paths 之外的 facade/聚合候选文件。
|
|
84
|
+
*
|
|
85
|
+
* @param {{ cwd: string, allowedPaths: string[] }} opts
|
|
86
|
+
* @returns {{ skipped: boolean, reason: string|null, candidates: Array<{file: string, via: string[]}> }}
|
|
87
|
+
*/
|
|
88
|
+
export function findFacadeCandidates({ cwd, allowedPaths }) {
|
|
89
|
+
const root = cwd || process.cwd()
|
|
90
|
+
const allowed = (allowedPaths || []).map(toPosix).filter(p => !p.includes('*'))
|
|
91
|
+
if (allowed.length === 0) return { skipped: true, reason: 'no-allowed-paths', candidates: [] }
|
|
92
|
+
if (allowed.length > MAX_ALLOWED_ENTRIES) return { skipped: true, reason: 'allowed-too-many', candidates: [] }
|
|
93
|
+
|
|
94
|
+
const files = walkSourceFiles(root)
|
|
95
|
+
if (files.length > MAX_REPO_FILES) return { skipped: true, reason: 'repo-too-large', candidates: [] }
|
|
96
|
+
|
|
97
|
+
const allowedSet = new Set(allowed)
|
|
98
|
+
const isAllowed = (abs) => allowed.some(a => abs === a || abs.endsWith('/' + a) || a.endsWith('/' + abs) || abs.replace(/\.[^.]+$/, '') === a.replace(/\.[^.]+$/, ''))
|
|
99
|
+
|
|
100
|
+
// 目录 → 该目录下模块名索引(聚合信号用:R import 同目录 ≥2 模块 = 聚合器)
|
|
101
|
+
const dirModules = new Map()
|
|
102
|
+
for (const f of files) {
|
|
103
|
+
const d = dirname(f)
|
|
104
|
+
if (!dirModules.has(d)) dirModules.set(d, new Set())
|
|
105
|
+
dirModules.get(d).add(basename(f, extname(f)))
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// 预读一遍源码 import(file → specs),两信号共用
|
|
109
|
+
const fileSpecs = []
|
|
110
|
+
for (const f of files) {
|
|
111
|
+
if (isAllowed(f)) continue
|
|
112
|
+
let text
|
|
113
|
+
try { text = readFileSync(f, 'utf8') } catch { continue }
|
|
114
|
+
fileSpecs.push({ file: f, specs: extractImportSpecifiers(text, extname(f).toLowerCase()) })
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// 逐 allowed 文件探测 direct + aggregate 命中
|
|
118
|
+
const byFile = new Map()
|
|
119
|
+
for (const p of allowed) {
|
|
120
|
+
const mod = basename(p, extname(p))
|
|
121
|
+
// allowed 是相对仓根路径,目录索引键是绝对路径(walk 产物)——resolve 对齐再查
|
|
122
|
+
const dirKey = toPosix(resolve(root, dirname(p)))
|
|
123
|
+
const dirLabel = toPosix(dirname(p))
|
|
124
|
+
const dirModSet = dirModules.get(dirKey) || new Set()
|
|
125
|
+
for (const { file, specs } of fileSpecs) {
|
|
126
|
+
const via = []
|
|
127
|
+
if (specs.some(s => specRefsModule(s, mod))) via.push(`direct import of ${mod}`)
|
|
128
|
+
// aggregate:R 引用 mod 同目录 ≥2 个不同仓内模块(mod 自身可不在——新增模块形态)
|
|
129
|
+
const hitMods = new Set()
|
|
130
|
+
for (const s of specs) {
|
|
131
|
+
for (const b of dirModSet) {
|
|
132
|
+
if (specRefsModule(s, b)) { hitMods.add(b); break }
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
if (hitMods.size >= 2) via.push(`aggregates ≥2 modules of ${dirLabel === '.' ? './' : dirLabel + '/'}`)
|
|
136
|
+
if (via.length === 0) continue
|
|
137
|
+
const existing = byFile.get(file) || []
|
|
138
|
+
for (const v of via) if (!existing.includes(v)) existing.push(v)
|
|
139
|
+
byFile.set(file, existing)
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
const candidates = [...byFile.entries()]
|
|
143
|
+
.map(([file, via]) => ({ file: toPosix(file), via }))
|
|
144
|
+
.sort((a, b) => a.file.localeCompare(b.file))
|
|
145
|
+
return { skipped: false, reason: null, candidates }
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* plan 门消费入口:探测 + 打印(advisory,不阻断)。allowed 集与 apply Gate1 同源
|
|
150
|
+
* (resolveApplyAllowSet 的主仓切片),确保「预检亮出的候选」与「执行期会拦的门」
|
|
151
|
+
* 用同一份清单口径。
|
|
152
|
+
*/
|
|
153
|
+
export function warnFacadeCandidateFiles({ cwd, changeName, allowSet }) {
|
|
154
|
+
try {
|
|
155
|
+
const r = findFacadeCandidates({ cwd, allowedPaths: [...(allowSet || [])] })
|
|
156
|
+
if (r.skipped || r.candidates.length === 0) return r
|
|
157
|
+
console.warn(`\n⚠️ plan 预检:${r.candidates.length} 个 allowed_paths 外的「透传/聚合」候选文件(advisory,不阻断):`)
|
|
158
|
+
for (const c of r.candidates.slice(0, 10)) {
|
|
159
|
+
console.warn(` - ${c.file}(${[...new Set(c.via)].join('; ')})`)
|
|
160
|
+
}
|
|
161
|
+
if (r.candidates.length > 10) console.warn(` …还有 ${r.candidates.length - 10} 个`)
|
|
162
|
+
console.warn(' 若属「透传必经文件」(facade 转发/路由挂载/注册登记——改 allowed 模块必须同步改它才生效):')
|
|
163
|
+
console.warn(' 现在补进对应 TaskCard allowed_paths 或 design §6 清单,避免执行期 apply Gate1 拦截后手工回补;')
|
|
164
|
+
console.warn(' 确认无关则忽略。执行期有据越界另有出口(review.json 声明 changedFiles,apply 有审计放行)。')
|
|
165
|
+
return r
|
|
166
|
+
} catch (e) {
|
|
167
|
+
console.warn(`⚠️ facade 候选探测失败(advisory 跳过): ${e.message}`)
|
|
168
|
+
return { skipped: true, reason: 'error', candidates: [] }
|
|
169
|
+
}
|
|
170
|
+
}
|
package/src/index.js
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, statSync, writeFileSync } from 'fs';
|
|
10
10
|
import { writeAtomicSync } from './fs-atomic.js';
|
|
11
|
-
import { basename, extname, join, resolve } from 'path';
|
|
11
|
+
import { basename, extname, join, resolve, isAbsolute } from 'path';
|
|
12
12
|
import { safeGit, git } from './git-helper.js';
|
|
13
13
|
import { getVersion } from './version.js';
|
|
14
14
|
|
|
@@ -98,7 +98,7 @@ SillySpec CLI — 规范驱动开发工具包
|
|
|
98
98
|
sillyspec commit [--json] 智能提交建议:收集 QUICKLOG/已勾 task/阶段产出语义,生成建议 message(只建议不执行)
|
|
99
99
|
sillyspec verify-probes --change <name> [--init] verify 机械探针(TODO 标记/测试覆盖/API 对账/删除对账);--init 生成 verify-result.md 骨架
|
|
100
100
|
sillyspec module-impact --change <name> 生成 module-impact.md 骨架(文件×模块归属按 module-map 预填 + 未匹配清单)
|
|
101
|
-
sillyspec endpoints extract --change <name> [--task task-NN] [--dir <dir>|--files <a.py,b.js>] 静态扫描路由装饰器生成 endpoints.json
|
|
101
|
+
sillyspec endpoints extract --change <name> [--task task-NN | --all-tasks] [--dir <dir>|--files <a.py,b.js>] 静态扫描路由装饰器生成 endpoints.json
|
|
102
102
|
sillyspec scan-fix-headers [--project <名>] scan 文档补 author/created_at header(幂等)
|
|
103
103
|
sillyspec plan-adopt-waves --change <名> [--dry-run] plan.md Wave 段一键重排为 depends_on 拓扑分组(同步任务总表 W 列,幂等)
|
|
104
104
|
sillyspec workspace add <名> <路径> [--role] [--repo] | remove <名> | status 多项目工作区登记与状态探测
|
|
@@ -118,6 +118,8 @@ SillySpec CLI — 规范驱动开发工具包
|
|
|
118
118
|
生成 Windows 安全 TaskCard 骨架(LF 行尾 + frontmatter 闭合 +
|
|
119
119
|
硬校验 9 字段齐全,标题自动取自 plan.md,Edit 填占位符即可)
|
|
120
120
|
sillyspec change-rename <旧变更名> <新变更名>
|
|
121
|
+
sillyspec change-delete <变更名> [--confirm] 删除变更(DB status='deleted' 与归档语义分离 +
|
|
122
|
+
移目录 + 清 worktree + 推平台墓碑;默认 dry-run)
|
|
121
123
|
sillyspec knowledge <search --query "..." --limit N
|
|
122
124
|
| inspect --id "..."
|
|
123
125
|
| validate | refresh
|
|
@@ -127,6 +129,8 @@ SillySpec CLI — 规范驱动开发工具包
|
|
|
127
129
|
| sync [--change <name>] | sync-docs [--change <name>]
|
|
128
130
|
| status | pointer [--cleanup]
|
|
129
131
|
| approve <change> | reject <change> [--reason <r>]>
|
|
132
|
+
sillyspec pull --spec [--force] 拉取服务器 spec 快照到本地 .sillyspec(打包时刻
|
|
133
|
+
快照非实时、无自动同步;进度六表拉取用 platform pull)
|
|
130
134
|
sillyspec docs migrate
|
|
131
135
|
sillyspec dashboard [--port <N>] [--no-open]
|
|
132
136
|
|
|
@@ -743,6 +747,20 @@ async function main() {
|
|
|
743
747
|
}
|
|
744
748
|
console.log(`📦 待提交变更(${cc.changedCount} 个文件${cc.untrackedCount > 0 ? `,其中未跟踪 ${cc.untrackedCount} 个` : ''};上方 stat 仅列已跟踪文件的改动)`);
|
|
745
749
|
if (cc.stat) console.log(cc.stat.split('\n').map(l => ' ' + l).join('\n'));
|
|
750
|
+
// 暂存区快照(坑 same-main-staging-pollution,2026-08-28 实证:同 main 双会话共享暂存区,
|
|
751
|
+
// A 的 commit 扫入 B 暂存的文件):git commit 只含暂存内容——提交前把快照亮出来供核对。
|
|
752
|
+
{
|
|
753
|
+
const { collectStagedArea } = await import('./commit-suggest.js');
|
|
754
|
+
const staged = collectStagedArea({ cwd: dir });
|
|
755
|
+
if (staged.available && staged.staged.length > 0) {
|
|
756
|
+
console.log(`\n📌 暂存区快照(git diff --cached --name-only,你的下一次 git commit 将恰好含这 ${staged.staged.length} 个文件):`);
|
|
757
|
+
for (const f of staged.staged.slice(0, 15)) console.log(` - ${f}`);
|
|
758
|
+
if (staged.staged.length > 15) console.log(` … 共 ${staged.staged.length} 个`);
|
|
759
|
+
console.log(` ⚠️ 同 main 并行会话共享同一暂存区——快照里非本会话的文件请先 git restore --staged <file> 剔除,否则会被你的 commit 扫入`);
|
|
760
|
+
} else if (staged.available) {
|
|
761
|
+
console.log(`\n📌 暂存区为空(git commit 前需先 git add;用 pathspec 级 add 防扫入无关文件)`);
|
|
762
|
+
}
|
|
763
|
+
}
|
|
746
764
|
if (cc.quickEntries.length > 0) {
|
|
747
765
|
console.log(`\n📝 QUICKLOG(上次提交 ${cc.lastCommitTime || '(无)'} 之后 ${cc.quickEntries.length} 条):`);
|
|
748
766
|
for (const q of cc.quickEntries) {
|
|
@@ -892,15 +910,19 @@ async function main() {
|
|
|
892
910
|
// 此前靠 agent 手扫装饰器手写(易漏 endpoint)。CLI 复用 endpoint-extractor 全套提取器
|
|
893
911
|
// (FastAPI @router.* / Express router.* / Spring @*Mapping)静态扫描生成,
|
|
894
912
|
// verify 探针 5(verifyApiParity)直接消费该产物。
|
|
913
|
+
// --all-tasks(坑 probe5-single-task-artifact-scope,2026-08-28 实证:单 task 产物对账
|
|
914
|
+
// 产生大量假 missing):聚合模式——逐 task 卡提取,各自落 contract-artifacts/<change>/<task>/,
|
|
915
|
+
// 与 verifyApiParity 的聚合读侧(contract-artifacts/<change>/*/endpoints.json 全量并集)对齐。
|
|
895
916
|
const epSub = filteredArgs[1];
|
|
896
917
|
if (epSub !== 'extract') {
|
|
897
|
-
console.error('用法: sillyspec endpoints extract --change <name> [--task task-NN] [--dir <backendRoot> | --files <a.py,b.js...>] [--spec-dir <path>]\n 静态扫描后端路由装饰器生成 endpoints.json(FastAPI/Express/Spring);verify 探针 5
|
|
918
|
+
console.error('用法: sillyspec endpoints extract --change <name> [--task task-NN | --all-tasks] [--dir <backendRoot> | --files <a.py,b.js...>] [--spec-dir <path>]\n 静态扫描后端路由装饰器生成 endpoints.json(FastAPI/Express/Spring);verify 探针 5 消费。\n --all-tasks:聚合逐 task 卡 allowed_paths 各自提取(对账口径与探针 5 聚合读侧一致,推荐)。\n execute worktree 模式:allowed_paths 自动按 worktree 根解析,产物恒落主仓 spec(坑 endpoints-extract-worktree-pitfalls)');
|
|
898
919
|
process.exit(2);
|
|
899
920
|
}
|
|
900
921
|
const epChangeIdx = args.indexOf('--change');
|
|
901
922
|
const epChange = epChangeIdx >= 0 && args[epChangeIdx + 1] ? args[epChangeIdx + 1] : null;
|
|
902
923
|
const epTaskIdx = args.indexOf('--task');
|
|
903
924
|
const epTask = epTaskIdx >= 0 && args[epTaskIdx + 1] ? args[epTaskIdx + 1] : null;
|
|
925
|
+
const epAllTasks = args.includes('--all-tasks');
|
|
904
926
|
const epDirIdx = args.indexOf('--dir');
|
|
905
927
|
const epDir = epDirIdx >= 0 && args[epDirIdx + 1] ? args[epDirIdx + 1] : null;
|
|
906
928
|
const epFilesIdx = args.indexOf('--files');
|
|
@@ -909,17 +931,51 @@ async function main() {
|
|
|
909
931
|
console.error('❌ 缺 --change <变更名>');
|
|
910
932
|
process.exit(2);
|
|
911
933
|
}
|
|
934
|
+
if (epAllTasks && (epTask || epDir || epFilesRaw)) {
|
|
935
|
+
console.error('❌ --all-tasks 与 --task/--dir/--files 互斥(聚合模式逐 task 卡自动取 allowed_paths)');
|
|
936
|
+
process.exit(2);
|
|
937
|
+
}
|
|
912
938
|
assertSafeChangeName(epChange, '--change 变更名');
|
|
913
939
|
const { extractFastApiEndpoints, extractExpressEndpoints, extractSpringEndpoints, scanBackendEndpoints } = await import('./endpoint-extractor.js');
|
|
914
940
|
const endpoints = [];
|
|
915
|
-
|
|
941
|
+
let epSpecBase = resolvePlatformSpecDir(dir, specDir) || join(dir, '.sillyspec');
|
|
942
|
+
// 坑 endpoints-extract-worktree-pitfalls ②(2026-08-29 实测):CLI 在 execute worktree 内跑时
|
|
943
|
+
// spec 命中副本(worktree 内 checkout 出来的 .sillyspec)→ 产物落 worktree 自己的
|
|
944
|
+
// contract-artifacts/,主仓 verify 探针 5 读不到。复用 detectWorktreeSpecDrift 判据锚回
|
|
945
|
+
// 主仓 spec:产物落点恒主仓,扫描面(--dir/--files 绝对路径)不受影响。
|
|
946
|
+
const epDrift = (await import('./run/shared.js')).detectWorktreeSpecDrift(epSpecBase);
|
|
947
|
+
if (epDrift) {
|
|
948
|
+
console.log(`⚠️ 已自动锚定主仓 spec:${epDrift.mainSpecBase}(endpoints 产物落主仓;当前 cwd 命中 worktree 副本 ${epDrift.changeName},流程继续)`);
|
|
949
|
+
epSpecBase = epDrift.mainSpecBase;
|
|
950
|
+
}
|
|
916
951
|
const epChangeDir = join(epSpecBase, 'changes', epChange);
|
|
917
952
|
if (!existsSync(epChangeDir)) {
|
|
918
953
|
console.error(`❌ 变更目录不存在: ${epChangeDir}`);
|
|
919
954
|
process.exit(1);
|
|
920
955
|
}
|
|
956
|
+
// 坑 endpoints-extract-worktree-pitfalls ①(2026-08-29 实测 13 task 全 0 端点):
|
|
957
|
+
// execute worktree 模式下变更代码只在 worktree 里,allowed_paths 按主仓(cwd)相对
|
|
958
|
+
// 解析 0 命中。活跃 worktree meta 存在时相对路径优先按 worktree 根解析——worktree =
|
|
959
|
+
// baseline 检出 + 本变更改动,扫描结果即交付态;不在 worktree 的文件回落主仓(显式
|
|
960
|
+
// --files 绝对路径原样通过)。WorktreeManager 从 worktree 内跑也会经 git-common-dir
|
|
961
|
+
// 锚定主仓 meta(构造器注释),meta 读取失败按无 worktree 处理不阻断。
|
|
962
|
+
let epWorktreeRoot = null;
|
|
963
|
+
try {
|
|
964
|
+
const { WorktreeManager } = await import('./worktree.js');
|
|
965
|
+
const epWtMeta = new WorktreeManager(dir).getMeta(epChange);
|
|
966
|
+
if (epWtMeta?.worktreePath && existsSync(epWtMeta.worktreePath)) epWorktreeRoot = epWtMeta.worktreePath;
|
|
967
|
+
} catch { /* meta 读取失败按无 worktree 处理 */ }
|
|
968
|
+
const resolveScoped = (p) => {
|
|
969
|
+
if (isAbsolute(p)) return p;
|
|
970
|
+
if (epWorktreeRoot) {
|
|
971
|
+
const inWt = join(epWorktreeRoot, p);
|
|
972
|
+
if (existsSync(inWt)) return inWt;
|
|
973
|
+
}
|
|
974
|
+
return resolve(p);
|
|
975
|
+
};
|
|
976
|
+
if (epWorktreeRoot) console.log(`ℹ️ 变更有活跃 worktree,扫描面优先按其根解析:${epWorktreeRoot}`);
|
|
921
977
|
const collectFile = (f) => {
|
|
922
|
-
const abs =
|
|
978
|
+
const abs = resolveScoped(f)
|
|
923
979
|
if (!existsSync(abs)) return
|
|
924
980
|
const ext = extname(abs).toLowerCase()
|
|
925
981
|
try {
|
|
@@ -928,10 +984,56 @@ async function main() {
|
|
|
928
984
|
else if (ext === '.java') endpoints.push(...extractSpringEndpoints(abs))
|
|
929
985
|
} catch { /* 单文件解析失败不阻断整体 */ }
|
|
930
986
|
}
|
|
987
|
+
// 单 task 产物写盘(taskName 归属目录);返回提取数
|
|
988
|
+
const epRuntimeRoot = (await import('./run/shared.js')).resolveRuntimeRoot({ specRoot: specDir }, epSpecBase);
|
|
989
|
+
const writeArtifact = (taskName, eps) => {
|
|
990
|
+
const artifact = {
|
|
991
|
+
generated_at: new Date().toISOString().slice(0, 19).replace('T', ' '),
|
|
992
|
+
generator: 'sillyspec endpoints extract',
|
|
993
|
+
endpoints: eps.map(e => ({ method: e.method, path: e.path, source: e.source })),
|
|
994
|
+
}
|
|
995
|
+
const epOutDir = join(epRuntimeRoot, 'contract-artifacts', epChange, taskName);
|
|
996
|
+
mkdirSync(epOutDir, { recursive: true });
|
|
997
|
+
const epOutPath = join(epOutDir, 'endpoints.json');
|
|
998
|
+
writeFileSync(epOutPath, JSON.stringify(artifact, null, 2) + '\n');
|
|
999
|
+
return epOutPath;
|
|
1000
|
+
}
|
|
931
1001
|
if (epDir) {
|
|
932
|
-
endpoints.push(...scanBackendEndpoints(resolve(epDir)))
|
|
1002
|
+
endpoints.push(...scanBackendEndpoints(resolve(epDir)));
|
|
933
1003
|
} else if (epFilesRaw) {
|
|
934
1004
|
for (const f of epFilesRaw.split(',').map(s => s.trim()).filter(Boolean)) collectFile(f)
|
|
1005
|
+
} else if (epAllTasks) {
|
|
1006
|
+
// 聚合模式:逐 task 卡 allowed_paths 提取,各自落产物目录(对齐 verifyApiParity 聚合读侧)
|
|
1007
|
+
const { parseAllowedPaths } = await import('./stages/plan-postcheck.js')
|
|
1008
|
+
const tasksDir = join(epChangeDir, 'tasks')
|
|
1009
|
+
const taskCards = existsSync(tasksDir)
|
|
1010
|
+
? readdirSync(tasksDir).filter(f => /^task-[\w.-]+\.md$/.test(f)).sort()
|
|
1011
|
+
: []
|
|
1012
|
+
if (taskCards.length === 0) {
|
|
1013
|
+
console.error(`❌ --all-tasks 无 task 卡(${tasksDir} 不存在或无 task-*.md)——先跑 plan 生成 TaskCard`);
|
|
1014
|
+
process.exit(1);
|
|
1015
|
+
}
|
|
1016
|
+
let total = 0
|
|
1017
|
+
for (const card of taskCards) {
|
|
1018
|
+
const taskName = card.replace(/\.md$/, '')
|
|
1019
|
+
const eps = []
|
|
1020
|
+
const perFile = (f) => {
|
|
1021
|
+
const abs = resolveScoped(f)
|
|
1022
|
+
if (!existsSync(abs)) return
|
|
1023
|
+
const ext = extname(abs).toLowerCase()
|
|
1024
|
+
try {
|
|
1025
|
+
if (ext === '.py') eps.push(...extractFastApiEndpoints(abs))
|
|
1026
|
+
else if (ext === '.js' || ext === '.ts') eps.push(...extractExpressEndpoints(abs))
|
|
1027
|
+
else if (ext === '.java') eps.push(...extractSpringEndpoints(abs))
|
|
1028
|
+
} catch { /* 单文件解析失败不阻断整体 */ }
|
|
1029
|
+
}
|
|
1030
|
+
for (const p of parseAllowedPaths(readFileSync(join(tasksDir, card), 'utf8'))) perFile(p)
|
|
1031
|
+
const outPath = writeArtifact(taskName, eps)
|
|
1032
|
+
total += eps.length
|
|
1033
|
+
console.log(`ℹ️ ${taskName}: ${eps.length} 个端点 → ${outPath}`)
|
|
1034
|
+
}
|
|
1035
|
+
console.log(`✅ 聚合提取完成:${taskCards.length} 个 task 卡共 ${total} 个端点(探针 5 对账时全量并集消费)`);
|
|
1036
|
+
break;
|
|
935
1037
|
} else {
|
|
936
1038
|
// 默认扫描面:task 卡 allowed_paths ∪ design §6 清单里的具体源码文件
|
|
937
1039
|
const { parseFileChangeListDetailed } = await import('./change-list.js')
|
|
@@ -951,28 +1053,19 @@ async function main() {
|
|
|
951
1053
|
}
|
|
952
1054
|
let scanned = 0
|
|
953
1055
|
for (const p of candidates) {
|
|
954
|
-
const abs =
|
|
1056
|
+
const abs = resolveScoped(p)
|
|
955
1057
|
if (!existsSync(abs)) continue
|
|
956
1058
|
scanned++
|
|
957
|
-
collectFile(
|
|
1059
|
+
collectFile(abs)
|
|
958
1060
|
}
|
|
959
1061
|
if (candidates.size === 0) {
|
|
960
|
-
console.error('❌ 无扫描面:--task 卡无 allowed_paths 且 design.md 无具体文件清单——显式传 --dir <backendRoot> 或 --files <a.py,b.js
|
|
1062
|
+
console.error('❌ 无扫描面:--task 卡无 allowed_paths 且 design.md 无具体文件清单——显式传 --dir <backendRoot> 或 --files <a.py,b.js>,或用 --all-tasks 聚合逐卡提取');
|
|
961
1063
|
process.exit(1);
|
|
962
1064
|
}
|
|
963
1065
|
console.log(`ℹ️ 扫描 ${scanned}/${candidates.size} 个清单文件(不存在的跳过)`);
|
|
964
1066
|
}
|
|
965
|
-
const
|
|
966
|
-
|
|
967
|
-
generator: 'sillyspec endpoints extract',
|
|
968
|
-
endpoints: endpoints.map(e => ({ method: e.method, path: e.path, source: e.source })),
|
|
969
|
-
}
|
|
970
|
-
const epRuntimeRoot = (await import('./run/shared.js')).resolveRuntimeRoot({ specRoot: specDir }, epSpecBase);
|
|
971
|
-
const epOutDir = join(epRuntimeRoot, 'contract-artifacts', epChange, epTask || 'scan');
|
|
972
|
-
mkdirSync(epOutDir, { recursive: true });
|
|
973
|
-
const epOutPath = join(epOutDir, 'endpoints.json');
|
|
974
|
-
writeFileSync(epOutPath, JSON.stringify(artifact, null, 2) + '\n');
|
|
975
|
-
console.log(`✅ 提取 ${endpoints.length} 个端点 → ${epOutPath}`);
|
|
1067
|
+
const outPath = writeArtifact(epTask || 'scan', endpoints);
|
|
1068
|
+
console.log(`✅ 提取 ${endpoints.length} 个端点 → ${outPath}`);
|
|
976
1069
|
console.log(` verify 阶段探针 5(API 契约对账)自动消费该产物;FastAPI/Express/Spring 装饰器均支持。`);
|
|
977
1070
|
break;
|
|
978
1071
|
}
|
|
@@ -1146,7 +1239,7 @@ async function main() {
|
|
|
1146
1239
|
docsCheckFlags.push(a);
|
|
1147
1240
|
}
|
|
1148
1241
|
}
|
|
1149
|
-
const { runDocsCheck, readDocsCheckConfig, DocsCheckConfigError, applyFixes } = await import('./docs-check.js');
|
|
1242
|
+
const { runDocsCheck, readDocsCheckConfig, DocsCheckConfigError, applyFixes, runChangeNameAdvisory } = await import('./docs-check.js');
|
|
1150
1243
|
try {
|
|
1151
1244
|
// 配置优先级:--paths > local.yaml docs-check.paths > 缺省 docs/**/*.md
|
|
1152
1245
|
const cfg = readDocsCheckConfig(dir);
|
|
@@ -1177,10 +1270,20 @@ async function main() {
|
|
|
1177
1270
|
}
|
|
1178
1271
|
}
|
|
1179
1272
|
const fixResult = fixActive ? applyFixes(dir, fixes, dryRun ? { dryRun: true } : {}) : null;
|
|
1273
|
+
// 变更名提名 advisory(2026-08-31 变更关联审计 P2):铁律同决策规则族——只 warn,
|
|
1274
|
+
// 不进 invalid、不影响下方 exit code 与 docs gate。降级容错:advisory 自身异常
|
|
1275
|
+
// 不拖垮主检查(try-catch 吞并提示)。
|
|
1276
|
+
let changeNameReport = null;
|
|
1277
|
+
try {
|
|
1278
|
+
changeNameReport = runChangeNameAdvisory({ projectRoot: dir });
|
|
1279
|
+
} catch (e) {
|
|
1280
|
+
console.warn(`⚠️ 变更名提名检查执行失败(advisory 降级跳过):${e.message}`);
|
|
1281
|
+
}
|
|
1180
1282
|
if (json) {
|
|
1181
1283
|
// --fix 与 --json 组合保持 stdout 纯 JSON 可解析(constraints):修复统计作 result.fixReport
|
|
1182
1284
|
// 附加后整体 stringify,主体仍是 runDocsCheck 返回(invalid[].fix 是 task-01 设计内增量)
|
|
1183
1285
|
if (fixResult) result.fixReport = { applied: fixResult.applied, skipped: fixResult.skipped.length, dryRun };
|
|
1286
|
+
if (changeNameReport) result.changeNameReport = changeNameReport;
|
|
1184
1287
|
console.log(JSON.stringify(result, null, 2));
|
|
1185
1288
|
} else {
|
|
1186
1289
|
for (const w of result.warnings) console.warn(`⚠️ ${w}`);
|
|
@@ -1219,6 +1322,16 @@ async function main() {
|
|
|
1219
1322
|
console.error(`\n修复指引:行号漂移 → 更新文档行号到当前源码;文件删改名 → 更新引用路径;`);
|
|
1220
1323
|
console.error(`关键词缺失但行号正确 → 确认符号是否改名,改文档 token 或行号。`);
|
|
1221
1324
|
}
|
|
1325
|
+
// 变更名提名 advisory 段(主结果之后、exit 之前;不参与 ok/invalid 与 exit code)。
|
|
1326
|
+
// 零输出原则(docs-check-fix 契约:无新 flag 时 stdout 逐字节一致;决策规则族同款
|
|
1327
|
+
// 「无信号零输出」):仅 findings 非空才输出——悬空即信号,无悬空不打扰。
|
|
1328
|
+
if (changeNameReport && changeNameReport.findings.length > 0) {
|
|
1329
|
+
console.warn(`⚠️ 变更名提名悬空 ${changeNameReport.findings.length}/${changeNameReport.mentions} 处(advisory 不阻断、不进 docs gate):`);
|
|
1330
|
+
for (const f of changeNameReport.findings) console.warn(` - ${f.message}`);
|
|
1331
|
+
if (changeNameReport.exempted.length > 0) {
|
|
1332
|
+
console.log(`ℹ️ 另有 ${changeNameReport.exempted.length} 处经 known_failures change-name.* 键豁免`);
|
|
1333
|
+
}
|
|
1334
|
+
}
|
|
1222
1335
|
}
|
|
1223
1336
|
// exit code(design §5.2 行为矩阵逐字对齐):全绿 → 0(--fix 无操作);--fix 后全部
|
|
1224
1337
|
// fixable 修复完且无 needs-manual 残留 → 0;仍有 needs-manual 或写回 skipped → 1;
|
|
@@ -1719,6 +1832,20 @@ SillySpec worktree — git worktree 隔离管理
|
|
|
1719
1832
|
console.log(` git add --pathspec-from-file="${result.pathspecFile}"`);
|
|
1720
1833
|
}
|
|
1721
1834
|
console.log(` (完整清单 ${result.commitPathspec.length} 个文件已存 ${result.pathspecFile},可 --pathspec-from-file 复用)`);
|
|
1835
|
+
// 他者暂存差集(坑 same-main-staging-pollution):暂存区已有非本变更文件时,
|
|
1836
|
+
// 普通 git commit 会把它们一并扫入——提交前亮出来。pathspec 级 commit
|
|
1837
|
+
// (git commit -m <msg> -- <paths>)只提交指定路径、不动他者暂存,双出口都给。
|
|
1838
|
+
try {
|
|
1839
|
+
const { collectStagedArea } = await import('./commit-suggest.js');
|
|
1840
|
+
const stagedNow = collectStagedArea({ cwd: dir, ownFiles: result.commitPathspec });
|
|
1841
|
+
if (stagedNow.available && stagedNow.foreign.length > 0) {
|
|
1842
|
+
console.warn(`\n⚠️ 暂存区已有 ${stagedNow.foreign.length} 个非本变更文件(很可能为他者并行会话暂存,普通 git commit 会扫入):`);
|
|
1843
|
+
for (const f of stagedNow.foreign.slice(0, 10)) console.warn(` - ${f}`);
|
|
1844
|
+
if (stagedNow.foreign.length > 10) console.warn(` … 共 ${stagedNow.foreign.length} 个`);
|
|
1845
|
+
console.warn(` 推荐用 pathspec 级 commit(不动他者暂存):git commit -m "<msg>" --pathspec-from-file="${result.pathspecFile}"`);
|
|
1846
|
+
console.warn(` 或先剔除:git restore --staged -- <文件>(工作区内容不丢,对方可重新 add)`);
|
|
1847
|
+
}
|
|
1848
|
+
} catch { /* 暂存区快照失败不影响 apply 结果展示 */ }
|
|
1722
1849
|
}
|
|
1723
1850
|
}
|
|
1724
1851
|
if (result.warnings && result.warnings.length > 0) {
|
|
@@ -2069,6 +2196,9 @@ SillySpec platform — SillyHub 平台同步
|
|
|
2069
2196
|
sillyspec platform pointer [--cleanup]
|
|
2070
2197
|
sillyspec platform approve <change-name>
|
|
2071
2198
|
sillyspec platform reject <change-name> [--reason <reason>]
|
|
2199
|
+
|
|
2200
|
+
注: platform pull 拉的是进度六表(多机进度同步);拉取 spec 文件整树快照请用
|
|
2201
|
+
sillyspec pull --spec(X2,打包时刻快照非实时)。
|
|
2072
2202
|
`);
|
|
2073
2203
|
break;
|
|
2074
2204
|
}
|
|
@@ -2431,6 +2561,53 @@ checkbox 行;depends_on 自动反填行内注解 "(depends_on: task-01,02)";
|
|
|
2431
2561
|
pm.renameChange(dir, oldName, newName);
|
|
2432
2562
|
break;
|
|
2433
2563
|
}
|
|
2564
|
+
case 'change-delete': {
|
|
2565
|
+
// 2026-08-30 用户反馈①:一等删除路径——此前删变更靠 git rm + 借道幽灵清理,
|
|
2566
|
+
// 且幽灵清理把删除行写成 archived,DB 无法区分归档与删除。两段式(默认 dry-run)。
|
|
2567
|
+
// 变更名:位置参数(对齐 change-rename)或 --change(对齐 gate/derive)二选一。
|
|
2568
|
+
let delName = filteredArgs[1];
|
|
2569
|
+
if (!delName || delName.startsWith('-')) {
|
|
2570
|
+
const idx = filteredArgs.indexOf('--change');
|
|
2571
|
+
if (idx >= 0 && filteredArgs[idx + 1]) delName = filteredArgs[idx + 1];
|
|
2572
|
+
}
|
|
2573
|
+
if (!delName || delName.startsWith('-')) {
|
|
2574
|
+
console.error('❌ 用法: sillyspec change-delete <变更名> [--confirm](或 --change <变更名>)');
|
|
2575
|
+
process.exitCode = 2;
|
|
2576
|
+
break;
|
|
2577
|
+
}
|
|
2578
|
+
const delConfirm = filteredArgs.includes('--confirm');
|
|
2579
|
+
const { deleteChange } = await import('./change-delete.js');
|
|
2580
|
+
const r = await deleteChange({
|
|
2581
|
+
cwd: dir,
|
|
2582
|
+
specDir: resolvePlatformSpecDir(dir, specDir),
|
|
2583
|
+
changeName: delName,
|
|
2584
|
+
confirm: delConfirm,
|
|
2585
|
+
});
|
|
2586
|
+
if (json) {
|
|
2587
|
+
console.log(JSON.stringify(r, null, 2));
|
|
2588
|
+
} else if (!r.ok) {
|
|
2589
|
+
console.error(`❌ ${r.reason}`);
|
|
2590
|
+
} else if (r.action === 'dry_run') {
|
|
2591
|
+
console.log(`🗑️ 变更 ${delName} 待删除(dry-run,未执行):`);
|
|
2592
|
+
if (r.db_row) console.log(` DB 行:status=${r.db_row.status}${r.already_deleted ? '(已删除,本次仅清残留)' : ` → deleted(与 archived 语义分离,审计可直接区分)`}`);
|
|
2593
|
+
else console.log(' DB 行:无(孤儿目录,仅删目录)');
|
|
2594
|
+
if (r.dir) console.log(` 目录:${r.dir.path}(${r.dir.files} 个文件,git tracked 文件可从历史回溯)`);
|
|
2595
|
+
else console.log(' 目录:不存在(仅 DB 行翻 deleted——取代 git rm 后借道幽灵清理的老路径)');
|
|
2596
|
+
if (r.worktree) {
|
|
2597
|
+
console.log(` worktree:${r.worktree.mode}${r.worktree.unappliedChanges > 0
|
|
2598
|
+
? `(⚠️ ${r.worktree.unappliedChanges} 个未 apply 变更——删除时保留 worktree 不丢代码,确认废弃后手动 cleanup)`
|
|
2599
|
+
: '(将一并清理)'}`);
|
|
2600
|
+
}
|
|
2601
|
+
console.log(`\n加 --confirm 执行删除(DB status='deleted' + 移除目录 + 清理 worktree + 暂存 git 删除 + 推平台墓碑)。`);
|
|
2602
|
+
} else {
|
|
2603
|
+
console.log(`🗑️ 变更 ${delName} 已删除:`);
|
|
2604
|
+
if (r.db_updated) console.log(` ✅ DB status='deleted'(与 archived 语义分离,审计可直接区分)`);
|
|
2605
|
+
if (r.dir_removed) console.log(` ✅ 目录已移除(${r.files_removed} 个文件,git 历史可回溯)`);
|
|
2606
|
+
for (const w of r.warnings || []) console.log(` ⚠️ ${w}`);
|
|
2607
|
+
}
|
|
2608
|
+
process.exitCode = r.ok ? 0 : 1;
|
|
2609
|
+
break;
|
|
2610
|
+
}
|
|
2434
2611
|
case 'workflow': {
|
|
2435
2612
|
const wfSub = filteredArgs[1];
|
|
2436
2613
|
if (!wfSub || wfSub === 'help' || wfSub === '--help') {
|
|
@@ -2863,8 +3040,68 @@ SillySpec config — local.yaml 配置键速查
|
|
|
2863
3040
|
}
|
|
2864
3041
|
break;
|
|
2865
3042
|
}
|
|
3043
|
+
case 'pull': {
|
|
3044
|
+
// X2(task-14 / design §7.1 §7.4 / FR-07 / FR-08):拉取服务器 spec 整树快照。
|
|
3045
|
+
// 注意与 platform pull(进度六表 DB import,多机进度同步)语义正交——本命令操作
|
|
3046
|
+
// .sillyspec 文件树;命名 pull --spec 显式区分,防误用。
|
|
3047
|
+
const pullSpecArgs = filteredArgs.slice(1);
|
|
3048
|
+
const pullWantsHelp = pullSpecArgs.length === 0
|
|
3049
|
+
|| pullSpecArgs.includes('help') || pullSpecArgs.includes('--help') || pullSpecArgs.includes('-h');
|
|
3050
|
+
const pullWantsSpec = pullSpecArgs.includes('--spec');
|
|
3051
|
+
const printPullSpecHelp = () => console.log(`
|
|
3052
|
+
SillySpec pull — 拉取服务器 spec 快照到本地(X2 / FR-07)
|
|
3053
|
+
|
|
3054
|
+
用法:
|
|
3055
|
+
sillyspec pull --spec [--force]
|
|
3056
|
+
|
|
3057
|
+
说明:
|
|
3058
|
+
从 SillyHub 平台拉取当前工作区的 .sillyspec 整树快照(tar 解压到 specDir,
|
|
3059
|
+
含 tar 顶层 PLATFORM-BUNDLE.json 快照元数据,可离线辨认快照新旧)。
|
|
3060
|
+
覆盖语义:specDir 为空 → 直接解压;非空 → 必须加 --force 整树替换
|
|
3061
|
+
(先删除本地整棵 .sillyspec 再解包——.runtime(进度 DB)等本地文件会一并丢失;
|
|
3062
|
+
--force 保留连接凭据 local.yaml(服务端 bundle 恒排除它),覆盖后无需重新 connect;
|
|
3063
|
+
崩溃安全:覆盖前凭据先备份为 .sillyspec.local.yaml.bak,成功后自动清理,
|
|
3064
|
+
中途失败可从该备份文件恢复)。
|
|
3065
|
+
|
|
3066
|
+
⚠️ 快照语义:拉取的是服务器「打包时刻」的快照,非实时镜像——
|
|
3067
|
+
本命令不自动同步、不在会话中自动刷新;拉取后本地的改动仍需正常推送。
|
|
3068
|
+
daemon 模式下任务/会话开始时会按 spec_version 自动取新,无需手动跑本命令。
|
|
3069
|
+
|
|
3070
|
+
拉取进度六表(多机进度同步)请用:sillyspec platform pull [--change <name>]
|
|
3071
|
+
`);
|
|
3072
|
+
if (pullWantsHelp || !pullWantsSpec) {
|
|
3073
|
+
printPullSpecHelp();
|
|
3074
|
+
if (!pullWantsHelp) {
|
|
3075
|
+
console.error('❌ 缺少 --spec(本命令拉取 spec 整树快照;进度六表拉取用 sillyspec platform pull)');
|
|
3076
|
+
process.exit(1);
|
|
3077
|
+
}
|
|
3078
|
+
break;
|
|
3079
|
+
}
|
|
3080
|
+
|
|
3081
|
+
const pullSpecForce = pullSpecArgs.includes('--force');
|
|
3082
|
+
const syncModule = await import('./sync.js');
|
|
3083
|
+
// 未连接明确提示不崩(对齐 platform pull :2289 先例)
|
|
3084
|
+
const smForSpecPull = new syncModule.SyncManager(dir);
|
|
3085
|
+
if (!smForSpecPull._getPlatform()) {
|
|
3086
|
+
console.error('❌ 未连接平台,请先 sillyspec platform connect');
|
|
3087
|
+
process.exit(1);
|
|
3088
|
+
}
|
|
3089
|
+
const specR = await syncModule.pullSpecBundle(dir, { force: pullSpecForce, specDir });
|
|
3090
|
+
if (specR.ok && specR.pulled) {
|
|
3091
|
+
console.log(`✅ 已拉取 spec 快照 → ${specR.specDir}${specR.specVersion ? `(平台 spec_version: ${specR.specVersion})` : ''}`);
|
|
3092
|
+
console.log(' 快照语义:打包时刻快照非实时,无自动同步;本地后续改动仍需正常推送。');
|
|
3093
|
+
} else if (/非空/.test(specR.reason || '')) {
|
|
3094
|
+
console.error(`❌ ${specR.reason}`);
|
|
3095
|
+
console.error(' 确认整树替换请加 --force(先删除本地 .sillyspec 整树再解包,进度 DB 等本地文件一并丢失;连接凭据 local.yaml 保留,无需重新 connect)');
|
|
3096
|
+
process.exit(1);
|
|
3097
|
+
} else {
|
|
3098
|
+
console.error(`❌ 拉取 spec 快照失败: ${specR.reason || '未知'}`);
|
|
3099
|
+
process.exit(1);
|
|
3100
|
+
}
|
|
3101
|
+
break;
|
|
3102
|
+
}
|
|
2866
3103
|
default: {
|
|
2867
|
-
const topCommands = ['init', 'setup', 'run', 'progress', 'worktree', 'dispatch', 'agent-log', 'local', 'workflow', 'gate', 'derive', 'backfill-reviews', 'register-stage-review', 'modules', 'change-rename', 'knowledge', 'platform', 'scan', 'brainstorm', 'plan', 'execute', 'verify', 'archive', 'quick', 'explore', 'status', 'doctor', 'auto', 'runtime'];
|
|
3104
|
+
const topCommands = ['init', 'setup', 'run', 'progress', 'worktree', 'dispatch', 'agent-log', 'local', 'workflow', 'gate', 'derive', 'backfill-reviews', 'register-stage-review', 'modules', 'change-rename', 'change-delete', 'knowledge', 'platform', 'pull', 'scan', 'brainstorm', 'plan', 'execute', 'verify', 'archive', 'quick', 'explore', 'status', 'doctor', 'auto', 'runtime'];
|
|
2868
3105
|
const suggestion = didYouMean(command, topCommands);
|
|
2869
3106
|
console.error(`❌ 未知命令: ${command}`);
|
|
2870
3107
|
if (command === '--status') {
|