sillyspec 3.26.15 → 3.27.2
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-explore/SKILL.md +10 -3
- package/.claude/skills/sillyspec-verify/SKILL.md +2 -0
- package/README.md +2 -2
- package/package.json +1 -1
- package/packages/dashboard/server/index.js +22 -6
- package/src/agent-session-log.js +763 -0
- package/src/change-list.js +22 -2
- package/src/change-risk-profile.js +21 -6
- package/src/contract-matrix.js +91 -7
- package/src/db.js +14 -3
- package/src/docs-check.js +598 -570
- package/src/doctor-diagnostics.js +53 -1
- package/src/endpoint-extractor.js +21 -2
- package/src/fs-atomic.js +4 -2
- package/src/git-helper.js +73 -5
- package/src/hooks/claude-pre-tool-use.cjs +10 -5
- package/src/hooks/worktree-guard.js +22 -3
- package/src/index.js +59 -7
- package/src/module-impact.js +21 -1
- package/src/module-resolve.js +211 -0
- package/src/modules.js +82 -2
- package/src/progress/step-store.js +12 -8
- package/src/progress.js +6 -2
- package/src/quicklog.js +11 -3
- package/src/run/command.js +40 -0
- package/src/run/complete-handlers.js +17 -2
- package/src/run/complete.js +81 -8
- package/src/run/gates.js +83 -14
- package/src/run/prompt.js +16 -2
- package/src/run/quick-audit.js +22 -2
- package/src/run/shared.js +70 -19
- package/src/run/stage.js +60 -3
- package/src/stage-contract.js +14 -1
- package/src/stages/archive.js +3 -1
- package/src/stages/execute.js +171 -18
- package/src/stages/explore.js +11 -2
- package/src/stages/plan-postcheck.js +3 -1
- package/src/stages/plan.js +3 -0
- package/src/stages/quick.js +4 -3
- package/src/stages/verify.js +9 -2
- package/src/sync.js +98 -3
- package/src/task-review.js +55 -7
- package/src/taskcard.js +52 -22
- package/src/verify-postcheck.js +223 -6
- package/src/worktree-apply.js +28 -10
- package/src/worktree-deps.js +71 -5
- package/src/worktree.js +70 -6
package/src/worktree-deps.js
CHANGED
|
@@ -8,8 +8,8 @@
|
|
|
8
8
|
* 见 change 2026-06-28-worktree-deps-provision / D-005@v1, D-007@v1。
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
import { existsSync, readFileSync, realpathSync } from 'fs';
|
|
12
|
-
import { join, isAbsolute, resolve as resolvePath, sep as pathSep } from 'path';
|
|
11
|
+
import { existsSync, readFileSync, realpathSync, lstatSync } from 'fs';
|
|
12
|
+
import { join, isAbsolute, relative, resolve as resolvePath, sep as pathSep } from 'path';
|
|
13
13
|
import { execFileSync } from 'child_process';
|
|
14
14
|
import { createHash } from 'crypto';
|
|
15
15
|
|
|
@@ -137,10 +137,22 @@ function tryLink(mainNodeModules, linkPath) {
|
|
|
137
137
|
// execFileSync 数组形式不经 shell:POSIX 双引号内 `/$() 会执行、cmd.exe 双引号内 %VAR% 仍展开,
|
|
138
138
|
// linkPath 含 local.yaml 模块 path,属项目内可配置值(安全收敛,与 git-helper 同范式)
|
|
139
139
|
execFileSync('cmd.exe', ['/c', 'mklink', '/J', linkPath, mainNodeModules], { stdio: ['pipe', 'pipe', 'pipe'] });
|
|
140
|
-
|
|
140
|
+
} else {
|
|
141
|
+
execFileSync('ln', ['-s', mainNodeModules, linkPath], { stdio: ['pipe', 'pipe', 'pipe'] });
|
|
141
142
|
}
|
|
142
|
-
|
|
143
|
-
|
|
143
|
+
// 创建后实物复核(坑 provision-silent-fake-installed 第②层,2026-08-21 实证:doctor 报
|
|
144
|
+
// re-provisioned 成功但 junction 实际没建)——mklink/ln 退出码 0 不等于链接落盘(cmd.exe
|
|
145
|
+
// 垫片链存在静默失败面),lstat+existsSync 复核不存在即判失败,交由调用方走 install 兜底
|
|
146
|
+
//(junction(reparse point)与 symlink 的 lstat 均报 isSymbolicLink=true)
|
|
147
|
+
try {
|
|
148
|
+
const st = lstatSync(linkPath);
|
|
149
|
+
if (!st.isSymbolicLink() || !existsSync(linkPath)) {
|
|
150
|
+
return { ok: false, error: 'link 创建后复核失败:lstat 非 link 或路径不可达' };
|
|
151
|
+
}
|
|
152
|
+
} catch (e) {
|
|
153
|
+
return { ok: false, error: `link 创建后复核失败(lstat ${e.message})` };
|
|
154
|
+
}
|
|
155
|
+
return { ok: true, method: process.platform === 'win32' ? 'junction' : 'symlink' };
|
|
144
156
|
} catch (e) {
|
|
145
157
|
return { ok: false, error: e.message };
|
|
146
158
|
}
|
|
@@ -344,6 +356,48 @@ export function provisionDeps(worktreePath, mainCwd, opts = {}) {
|
|
|
344
356
|
? { depsStatus: 'installed', depsMethod: 'install', depsSource: 'install', depsLockHash: wtHash }
|
|
345
357
|
: { depsStatus: 'failed', depsMethod: null, depsSource: null, depsLockHash: wtHash, depsError: installResult.error };
|
|
346
358
|
}
|
|
359
|
+
// ── 结果后验证(坑 provision-silent-fake-installed,2026-08-21 Windows 实证;
|
|
360
|
+
// 坑 provision-monorepo-subpackage-fake-installed,2026-08-22 复发:pnpm monorepo
|
|
361
|
+
// workspace 根 package.json 的 dependencies 常为空(依赖在 frontend/daemon 子包),
|
|
362
|
+
// 只查根会把 installed 状态整体跳过——doctor 标 installed 但子包 node_modules 全缺,
|
|
363
|
+
// Wave 1 测试才挂)。分层校验:
|
|
364
|
+
// linked:根 junction 实存硬校验(link 声称成功必留痕);
|
|
365
|
+
// installed:校验目标集 = 根 + local.yaml modules 块声明的含 package.json 子模块,
|
|
366
|
+
// 其中【声明了依赖】的目录逐一要求 node_modules 存在(pnpm/npm/yarn workspace
|
|
367
|
+
// 安装都会给子包建 node_modules;无依赖声明的空壳目录合法地不建,不校验)。
|
|
368
|
+
// python/.venv、jvm 本地仓库本就不适用(整体仅 nodejs)。
|
|
369
|
+
if (projectType === 'nodejs') {
|
|
370
|
+
const pkgHasDeps = (dir) => {
|
|
371
|
+
try {
|
|
372
|
+
const pkg = JSON.parse(readFileSync(join(dir, 'package.json'), 'utf8'))
|
|
373
|
+
return ['dependencies', 'devDependencies', 'optionalDependencies', 'peerDependencies']
|
|
374
|
+
.some(k => pkg[k] && Object.keys(pkg[k]).length > 0)
|
|
375
|
+
} catch { return false }
|
|
376
|
+
}
|
|
377
|
+
// 目标集:根 + modules 块内含 package.json 的安全子模块(monorepo 子包)
|
|
378
|
+
const verifyDirs = [worktreePath]
|
|
379
|
+
for (const mp of extractModulePaths(yamlText)) {
|
|
380
|
+
if (!isSafeModulePath(mp)) continue
|
|
381
|
+
const sub = join(worktreePath, mp)
|
|
382
|
+
if (existsSync(join(sub, 'package.json'))) verifyDirs.push(sub)
|
|
383
|
+
}
|
|
384
|
+
const depsDeclaredDirs = verifyDirs.filter(pkgHasDeps)
|
|
385
|
+
const needVerify = result.depsStatus === 'linked'
|
|
386
|
+
|| (result.depsStatus === 'installed' && depsDeclaredDirs.length > 0)
|
|
387
|
+
if (needVerify) {
|
|
388
|
+
// linked 只验根(junction 语义);installed 验全部声明依赖的目标目录
|
|
389
|
+
const targets = result.depsStatus === 'linked' ? [worktreePath] : depsDeclaredDirs
|
|
390
|
+
const missing = targets.filter(dir => !existsSync(join(dir, 'node_modules')))
|
|
391
|
+
if (missing.length > 0) {
|
|
392
|
+
const missingRel = missing.map(dir => relative(worktreePath, dir) || '(根)')
|
|
393
|
+
result = {
|
|
394
|
+
depsStatus: 'failed', depsMethod: null, depsSource: null, depsLockHash: wtHash,
|
|
395
|
+
depsError: `${result.depsStatus} 后验证失败:${missingRel.join('、')} 的 node_modules 不存在(报成功但实际未落盘——Windows 静默失败面:cmd.exe 垫片解析/杀毒拦截/frozen-lockfile 快退)。` +
|
|
396
|
+
`手动兜底:进 worktree 手动跑 ${installCmd}(子包随 workspace 一起装),或对 lockfile 一致的目录用 junction:New-Item -ItemType Junction -Path "<目标>/node_modules" -Target "<主仓对应目录>/node_modules",然后重试 doctor --fix`,
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
}
|
|
347
401
|
}
|
|
348
402
|
result.depsCheckedAt = depsCheckedAt;
|
|
349
403
|
|
|
@@ -372,6 +426,18 @@ export function provisionDeps(worktreePath, mainCwd, opts = {}) {
|
|
|
372
426
|
result.depsMethod = linkedModules[0].method;
|
|
373
427
|
result.depsSource = 'main-checkout';
|
|
374
428
|
}
|
|
429
|
+
// 子模块 link 后验证(坑 modules-submodule-link-verify,2026-08-23 实证:Windows 下漏链
|
|
430
|
+
// frontend node_modules——linkOneDir 报 linked 但 junction 实际未建或 main 侧本就没有)。
|
|
431
|
+
// linked 状态的子模块逐一核验 wt/<mp>/node_modules 实存:缺失降 failed 并给兜底指引
|
|
432
|
+
//(不静默放行——跑测试时才发现就晚了)。
|
|
433
|
+
for (const m of moduleResults) {
|
|
434
|
+
if (m.status !== 'linked') continue
|
|
435
|
+
const nmPath = join(worktreePath, m.path, 'node_modules')
|
|
436
|
+
if (!existsSync(nmPath)) {
|
|
437
|
+
m.status = 'failed'
|
|
438
|
+
m.error = `linked 后验证失败:${m.path}/node_modules 不存在(报成功但实际未落盘)。兜底:New-Item -ItemType Junction -Path "<wt>/${m.path}/node_modules" -Target "<主仓>/${m.path}/node_modules"`
|
|
439
|
+
}
|
|
440
|
+
}
|
|
375
441
|
const failedModules = moduleResults.filter(m => m.status === 'failed');
|
|
376
442
|
if (failedModules.length > 0) {
|
|
377
443
|
const fmsg = failedModules.map(m => `${m.path}: ${m.error}`).join('; ');
|
package/src/worktree.js
CHANGED
|
@@ -1003,7 +1003,7 @@ export class WorktreeManager {
|
|
|
1003
1003
|
* @param {string} wtPath - worktree 根目录(meta.worktreePath)
|
|
1004
1004
|
* @returns {{ ok: boolean, msg: string }}
|
|
1005
1005
|
*/
|
|
1006
|
-
_doctorReprovision(name, wtPath) {
|
|
1006
|
+
_doctorReprovision(name, wtPath, opts = {}) {
|
|
1007
1007
|
try {
|
|
1008
1008
|
const meta = this.getMeta(name) || {};
|
|
1009
1009
|
const isInPlace = meta.mode === 'in-place-fallback';
|
|
@@ -1033,9 +1033,14 @@ export class WorktreeManager {
|
|
|
1033
1033
|
}
|
|
1034
1034
|
}
|
|
1035
1035
|
}
|
|
1036
|
+
// 触发分流(坑 doctor-reprovision-junction-missing,2026-08-22 实证:junction 未建的
|
|
1037
|
+
// missing/failed 场景被 force install 强制重装——install 静默失败面大且慢;lockfile 一致时
|
|
1038
|
+
// 重建 junction 才是对症动作。opts.relinkFirst(missing/failed 触发)→ 非 force 跑:解链后
|
|
1039
|
+
// node_modules 不存在,tryLink 直接重建;lockfile 漂移自动落 install。stale/main-drift 维持
|
|
1040
|
+
// force 重装(依赖确实变了)。
|
|
1036
1041
|
const deps = provisionDeps(wtPath, this.cwd, {
|
|
1037
1042
|
specBase: join(this.cwd, '.sillyspec'),
|
|
1038
|
-
force:
|
|
1043
|
+
force: !opts.relinkFirst,
|
|
1039
1044
|
}) || {};
|
|
1040
1045
|
const metaPath = join(this.getWorktreePath(name), META_FILE);
|
|
1041
1046
|
// 成功重供时清掉旧 depsError(provisionDeps 成功结果不含该键,{...meta,...deps} 会残留旧值)
|
|
@@ -1153,7 +1158,29 @@ export class WorktreeManager {
|
|
|
1153
1158
|
// 放宽原 909 in-place-fallback 守卫:in-place 也跑(main-drift/stale/failed 对它同样有意义,
|
|
1154
1159
|
// 真 in-place 的 wtPath===主仓→wtHash===mainHash→不会误报 main-drift,仅 failed/stale 有意义)。
|
|
1155
1160
|
// fix 时 _doctorReprovision 自行判断是否解链(in-place 不解链,仅 install)。
|
|
1161
|
+
// 归档态前置闸(坑 doctor-reprovision-archived-change,2026-08-21 实证:已归档变更的
|
|
1162
|
+
// worktree 被 doctor 当活跃任务 re-provision——给死目录装依赖)。归档判定(changes/archive/
|
|
1163
|
+
// 实体 或 进度库 status='archived')命中 → 跳过供给,fix 时走 cleanup(其内建
|
|
1164
|
+
// hasUnappliedChanges 护栏仍在,未落仓内容照样拒删)。
|
|
1156
1165
|
if (meta && meta.worktreePath && existsSync(meta.worktreePath)) {
|
|
1166
|
+
const isArchivedChange = existsSync(join(this.cwd, '.sillyspec', 'changes', 'archive', name))
|
|
1167
|
+
|| this._isChangeArchivedInDb(name);
|
|
1168
|
+
if (isArchivedChange) {
|
|
1169
|
+
issues.push({ type: 'worktree-archived-change', name, fixable: meta.mode !== 'native-worktree',
|
|
1170
|
+
detail: `变更 ${name} 已归档但 worktree 残留——不供给依赖(死目录),建议清理(cleanup 内建未落仓护栏)` });
|
|
1171
|
+
if (fix && meta.mode !== 'native-worktree') {
|
|
1172
|
+
try {
|
|
1173
|
+
const r = this.cleanup(name);
|
|
1174
|
+
if (r.result === 'cleaned' || r.result === 'force-cleaned') {
|
|
1175
|
+
fixed.push(`cleaned archived-change worktree: ${name}`);
|
|
1176
|
+
} else if (r.result === 'blocked') {
|
|
1177
|
+
unfixable.push(`cleanup blocked (archived change 有未落仓内容): ${name} — 先 apply/commit 再清`);
|
|
1178
|
+
} else {
|
|
1179
|
+
unfixable.push(`cleanup skipped (archived change): ${name}`);
|
|
1180
|
+
}
|
|
1181
|
+
} catch { unfixable.push(`cleanup failed (archived change): ${name}`); }
|
|
1182
|
+
}
|
|
1183
|
+
} else {
|
|
1157
1184
|
const wtPath = meta.worktreePath;
|
|
1158
1185
|
const fresh = checkDepsFreshness(meta, wtPath, this.cwd);
|
|
1159
1186
|
const issueTypeByStatus = {
|
|
@@ -1166,10 +1193,13 @@ export class WorktreeManager {
|
|
|
1166
1193
|
if (issueType) {
|
|
1167
1194
|
issues.push({ type: issueType, name, detail: fresh.detail, fixable: true });
|
|
1168
1195
|
if (fix) {
|
|
1169
|
-
|
|
1196
|
+
// relinkFirst:missing/failed 多为 junction 丢失/上次供给失败——link 优先重建;
|
|
1197
|
+
// stale/main-drift 是依赖真变了——force 重装
|
|
1198
|
+
const r = this._doctorReprovision(name, wtPath, { relinkFirst: issueType === 'deps-missing' || issueType === 'deps-failed' });
|
|
1170
1199
|
(r.ok ? fixed : unfixable).push(r.msg);
|
|
1171
1200
|
}
|
|
1172
1201
|
}
|
|
1202
|
+
}
|
|
1173
1203
|
}
|
|
1174
1204
|
|
|
1175
1205
|
// meta 存在但 worktree 目录不存在
|
|
@@ -1311,10 +1341,24 @@ export class WorktreeManager {
|
|
|
1311
1341
|
|
|
1312
1342
|
// 2) 候选集中尚未落到主工作区 HEAD 的子集
|
|
1313
1343
|
const pending = this._changesAlreadyOnMain(worktreePath, tracked, untracked);
|
|
1314
|
-
|
|
1315
|
-
|
|
1344
|
+
// 3) main 工作区副本降噪层(坑 unapplied-false-positive-workspace-copy,2026-08-21 实证:
|
|
1345
|
+
// apply 后 main 工作区有逐字节一致副本但未 commit → HEAD-only 判定报「未落仓」虚警拦
|
|
1346
|
+
// doctor)。原注释「不查 main 工作区未提交副本(防误删)」的保护意图是内容未落地——
|
|
1347
|
+
// 逐字节一致时内容确已落地(删 worktree 无损),仅逐字节不同的才保守保留。
|
|
1348
|
+
const pendingAfterWs = pending.filter(f => {
|
|
1349
|
+
try {
|
|
1350
|
+
const mainCopy = join(this.cwd, f);
|
|
1351
|
+
if (!existsSync(mainCopy)) return true; // main 工作区无副本 → 仍算未落仓
|
|
1352
|
+
const wtCopy = join(worktreePath, f);
|
|
1353
|
+
if (!existsSync(wtCopy)) return true; // worktree 侧是删除态、main 侧却在 → 内容分叉,保留
|
|
1354
|
+
const a = readFileSync(wtCopy); const b = readFileSync(mainCopy);
|
|
1355
|
+
return !(a.length === b.length && a.equals(b)); // 逐字节一致 → 已落地(剔除)
|
|
1356
|
+
} catch { return true }
|
|
1357
|
+
})
|
|
1358
|
+
if (pendingAfterWs.length === 0) {
|
|
1359
|
+
return { hasChanges: false, changedFiles: [], reason: 'all changes already on main (HEAD or identical workspace copy)' };
|
|
1316
1360
|
}
|
|
1317
|
-
return { hasChanges: true, changedFiles:
|
|
1361
|
+
return { hasChanges: true, changedFiles: pendingAfterWs };
|
|
1318
1362
|
} catch (e) {
|
|
1319
1363
|
// 检测失败时保守处理:视为有变更,保留 worktree
|
|
1320
1364
|
return { hasChanges: true, changedFiles: [], reason: `check failed: ${e.message}` };
|
|
@@ -1385,6 +1429,26 @@ export class WorktreeManager {
|
|
|
1385
1429
|
return map;
|
|
1386
1430
|
}
|
|
1387
1431
|
|
|
1432
|
+
/**
|
|
1433
|
+
* DB 归档态探测(坑 doctor-reprovision-archived-change):changes 表 status='archived'。
|
|
1434
|
+
* 只读直查(node:sqlite)——doctor 是同步方法,惰性动态 import 不可用;DB 不存在/无行/
|
|
1435
|
+
* 读失败 → false(保守退回普通 deps 供给路径)。
|
|
1436
|
+
* @param {string} name
|
|
1437
|
+
* @returns {boolean}
|
|
1438
|
+
*/
|
|
1439
|
+
_isChangeArchivedInDb(name) {
|
|
1440
|
+
try {
|
|
1441
|
+
const dbPath = join(this.cwd, '.sillyspec', '.runtime', 'sillyspec.db');
|
|
1442
|
+
if (!existsSync(dbPath)) return false;
|
|
1443
|
+
const { openDatabase } = require('./db-engine.js');
|
|
1444
|
+
const db = openDatabase(dbPath, { readOnly: true });
|
|
1445
|
+
try {
|
|
1446
|
+
const row = db.prepare('SELECT status FROM changes WHERE name = ?').get(name);
|
|
1447
|
+
return row?.status === 'archived';
|
|
1448
|
+
} finally { try { db.close() } catch {} }
|
|
1449
|
+
} catch { return false }
|
|
1450
|
+
}
|
|
1451
|
+
|
|
1388
1452
|
/**
|
|
1389
1453
|
* 从 worktree 路径反推 changeName
|
|
1390
1454
|
* @private
|