sillyspec 3.28.8 → 3.28.10
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/full.json +1 -0
- package/.claude/skills/sillyspec-execute/SKILL.md +222 -224
- package/.claude/skills/sillyspec-plan/SKILL.md +156 -156
- package/.claude/skills/sillyspec-quick/SKILL.md +118 -117
- package/AGENTS.md +3 -2
- package/README.md +35 -11
- package/SKILL.md +24 -10
- package/SillySpec-/350/203/275/345/212/233/344/272/256/347/202/271/345/205/250/346/231/257-2026-09-14.pptx +0 -0
- package/package.json +1 -1
- package/src/change-list.js +73 -20
- package/src/change-risk-profile.js +20 -0
- package/src/config-schema.js +62 -0
- package/src/contract-matrix.js +84 -5
- package/src/cross-repo-reconcile.js +134 -0
- package/src/db.js +11 -3
- package/src/decision-distill.js +15 -3
- package/src/design-facts.js +89 -20
- package/src/docs-gate.js +105 -6
- package/src/doctor-diagnostics.js +224 -2
- package/src/friction-ledger.js +171 -0
- package/src/hooks/worktree-guard.js +13 -1
- package/src/index.js +206 -18
- package/src/knowledge-classify.js +382 -0
- package/src/knowledge-hits.js +74 -0
- package/src/knowledge-stats.js +180 -0
- package/src/probe7-anchor-check.js +69 -0
- package/src/progress/change-registry.js +183 -8
- package/src/progress/shared.js +3 -2
- package/src/progress.js +100 -8
- package/src/quick-gate-profile.js +251 -0
- package/src/quicklog.js +164 -18
- package/src/review-dispatch.js +2 -1
- package/src/run/command.js +131 -13
- package/src/run/complete-handlers.js +511 -21
- package/src/run/complete.js +123 -23
- package/src/run/concurrent-detect.js +98 -0
- package/src/run/gate-snapshot.js +110 -2
- package/src/run/gates.js +141 -16
- package/src/run/prompt.js +221 -11
- package/src/run/quick-audit.js +111 -13
- package/src/run/shared.js +147 -11
- package/src/run/stage.js +6 -0
- package/src/scan-diff.js +61 -30
- package/src/scan-postcheck.js +74 -0
- package/src/scan-refresh.js +477 -0
- package/src/scan-staleness.js +25 -13
- package/src/scope-audit.js +456 -56
- package/src/stage-contract.js +185 -2
- package/src/stage-review.js +23 -7
- package/src/stages/brainstorm.js +6 -0
- package/src/stages/cmd-existence.js +9 -3
- package/src/stages/execute.js +85 -3
- package/src/stages/knowledge.js +76 -6
- package/src/stages/plan-postcheck.js +32 -5
- package/src/stages/quick.js +1 -1
- package/src/stages/verify.js +1 -0
- package/src/sync-noise.js +170 -6
- package/src/sync.js +141 -9
- package/src/task-review.js +366 -46
- package/src/verify-postcheck.js +212 -55
- package/src/verify-probes.js +727 -16
- package/src/worktree-apply.js +685 -48
- package/src/worktree-deps.js +124 -3
- package/src/worktree.js +391 -15
- package/templates/agents-instruction.md +2 -2
- package/templates/prompts/testcase-design.md +2 -1
- package/templates/prompts/verify-probes.md +4 -1
- package/.patch16-tmp.mjs +0 -169
- package/sa-rename-probe/src/new.js +0 -4
package/src/worktree-deps.js
CHANGED
|
@@ -13,8 +13,8 @@
|
|
|
13
13
|
* node 校验与 java 校验各说各话。新生态补一行表项即可;表外项目落 generic → n/a 不阻断。
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
|
-
import { existsSync, readFileSync, realpathSync, lstatSync, readdirSync, unlinkSync } from 'fs';
|
|
17
|
-
import { join, isAbsolute, relative, resolve as resolvePath, sep as pathSep } from 'path';
|
|
16
|
+
import { existsSync, readFileSync, realpathSync, lstatSync, readdirSync, readlinkSync, unlinkSync } from 'fs';
|
|
17
|
+
import { join, dirname, isAbsolute, relative, resolve as resolvePath, sep as pathSep } from 'path';
|
|
18
18
|
import { fileURLToPath } from 'url';
|
|
19
19
|
import { execFileSync } from 'child_process';
|
|
20
20
|
import { createHash } from 'crypto';
|
|
@@ -387,6 +387,86 @@ function isSafeModulePath(p) {
|
|
|
387
387
|
return !segs.includes('..');
|
|
388
388
|
}
|
|
389
389
|
|
|
390
|
+
/**
|
|
391
|
+
* resolved 路径是否严格位于 root 目录之下(不含 root 自身)。win32 大小写不敏感比较由
|
|
392
|
+
* path.win32.relative 自带(与 detectEditableInstallEscape 的越界判定同口径)。
|
|
393
|
+
*/
|
|
394
|
+
function isUnderDir(child, root) {
|
|
395
|
+
if (!child || !root) return false;
|
|
396
|
+
const rel = relative(resolvePath(root), resolvePath(child));
|
|
397
|
+
return rel !== '' && !rel.startsWith('..') && !isAbsolute(rel);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/**
|
|
401
|
+
* ql-20260915-003 修复①(坑 junction-foreign-ownership,2026-09-15 用户实证:主仓
|
|
402
|
+
* frontend/node_modules 内部发现 junction 指向 session-export worktree 的 .pnpm——跨会话
|
|
403
|
+
* node_modules 污染,apply 后被迫 --force 重装)。
|
|
404
|
+
*
|
|
405
|
+
* 归属判定:链接方向 wt/node_modules → main 是 SillySpec 的正确设计(worktree 共享主仓
|
|
406
|
+
* 依赖);反向——main node_modules 内出现指向 .sillyspec/.runtime/worktrees/ 之下目标的
|
|
407
|
+
* junction/symlink——是 install 兜底/包管理器穿透写坏留下的污染,任何合法流程都不依赖它,
|
|
408
|
+
* 一律视为外来(foreign)。pnpm 自建的包链接(node_modules/<pkg> → 同 node_modules 内
|
|
409
|
+
* .pnpm/...)与全局 store 链接目标都不在 worktrees 区之下,零误伤。
|
|
410
|
+
*
|
|
411
|
+
* 扫描口径:main 的 node_modules(根 + 各子模块 node_modules)**顶层目录项**——单层
|
|
412
|
+
* readdir+lstat,不递归 .pnpm 内部(顶层已覆盖包链接;递归大仓 node_modules 的性能与
|
|
413
|
+
* 误删面都不可接受)。子模块清单:submodules 直传相对路径列表,或给 specBase 自动读
|
|
414
|
+
* local.yaml modules 块。
|
|
415
|
+
*
|
|
416
|
+
* 删除安全:junction/symlink 删除不跟随目标(win32 cmd /c rmdir / POSIX unlinkSync——与
|
|
417
|
+
* worktree.js unlinkNodeModulesLinks :82 同款安全删法);单个清理失败 fail-open 记入
|
|
418
|
+
* failed 不阻断(清扫是治理动作,不拦主流程)。
|
|
419
|
+
*
|
|
420
|
+
* @param {string} mainDir 主仓根目录(node_modules 所在)
|
|
421
|
+
* @param {{ worktreesRoot?: string, specBase?: string|null, submodules?: string[]|null, dryRun?: boolean, onlyUnder?: string|null }} opts
|
|
422
|
+
* - worktreesRoot 外来边界:resolved target 落在其下才算外来(缺省直接空手而归)
|
|
423
|
+
* - submodules 为 null 且给了 specBase 时,自动读 local.yaml modules 块补子模块清单
|
|
424
|
+
* - dryRun=true 只报清单零写盘
|
|
425
|
+
* - onlyUnder 进一步收窄:只认 target 落在该目录之下的(install 兜底前只清指向本 worktree 的)
|
|
426
|
+
* @returns {{ foreign: Array<{link: string, target: string}>, removed: Array<{link: string, target: string}>, failed: Array<{link: string, error: string}> }}
|
|
427
|
+
* dryRun 时 foreign=将清清单、removed/failed 恒空;非 dryRun 时 removed=实际已清、failed=清理失败
|
|
428
|
+
*/
|
|
429
|
+
export function sweepForeignNodeModulesJunctions(mainDir, { worktreesRoot, specBase = null, submodules = null, dryRun = false, onlyUnder = null } = {}) {
|
|
430
|
+
const result = { foreign: [], removed: [], failed: [] };
|
|
431
|
+
if (!mainDir || !worktreesRoot || !existsSync(mainDir)) return result;
|
|
432
|
+
let mods = submodules;
|
|
433
|
+
if (mods === null && specBase) mods = extractModulePaths(readLocalYaml(specBase, null));
|
|
434
|
+
const nmDirs = [join(mainDir, 'node_modules')];
|
|
435
|
+
for (const sp of Array.isArray(mods) ? mods : []) {
|
|
436
|
+
if (!isSafeModulePath(sp)) continue;
|
|
437
|
+
nmDirs.push(join(mainDir, sp, 'node_modules'));
|
|
438
|
+
}
|
|
439
|
+
for (const nmDir of nmDirs) {
|
|
440
|
+
let names;
|
|
441
|
+
try { names = readdirSync(nmDir); } catch { continue; } // 目录不存在/不可读 → 该层跳过
|
|
442
|
+
for (const nm of names) {
|
|
443
|
+
const linkPath = join(nmDir, nm);
|
|
444
|
+
let st;
|
|
445
|
+
try { st = lstatSync(linkPath); } catch { continue; }
|
|
446
|
+
if (!st.isSymbolicLink()) continue; // junction(reparse point)与 symlink 的 lstat 均报 isSymbolicLink
|
|
447
|
+
let target;
|
|
448
|
+
try { target = readlinkSync(linkPath); } catch { continue; }
|
|
449
|
+
// 相对 target(pnpm 自建包链接常态)相对链接所在目录解析
|
|
450
|
+
const resolved = resolvePath(dirname(linkPath), target);
|
|
451
|
+
if (!isUnderDir(resolved, worktreesRoot)) continue;
|
|
452
|
+
if (onlyUnder && !isUnderDir(resolved, onlyUnder)) continue;
|
|
453
|
+
result.foreign.push({ link: linkPath, target: resolved });
|
|
454
|
+
if (dryRun) continue;
|
|
455
|
+
try {
|
|
456
|
+
if (process.platform === 'win32') {
|
|
457
|
+
execFileSync('cmd.exe', ['/c', 'rmdir', linkPath], { stdio: ['pipe', 'pipe', 'pipe'] });
|
|
458
|
+
} else {
|
|
459
|
+
unlinkSync(linkPath);
|
|
460
|
+
}
|
|
461
|
+
result.removed.push({ link: linkPath, target: resolved });
|
|
462
|
+
} catch (e) {
|
|
463
|
+
result.failed.push({ link: linkPath, error: e.message });
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
return result;
|
|
468
|
+
}
|
|
469
|
+
|
|
390
470
|
/**
|
|
391
471
|
* 对单个子模块目录 tryLink main 的 node_modules → wt 的 node_modules(modules 子模块专用)。
|
|
392
472
|
* 仅走 link 快路径(不 install——子模块 install 慢且易失败;lockfile 不一致时交给用户 pnpm install)。
|
|
@@ -464,6 +544,24 @@ export function provisionDeps(worktreePath, mainCwd, opts = {}) {
|
|
|
464
544
|
}
|
|
465
545
|
if (!linked) {
|
|
466
546
|
// 兜底:install
|
|
547
|
+
// ql-20260915-003 修复①(坑 junction-foreign-ownership)防再犯:install 在 worktree cwd
|
|
548
|
+
// 执行,若 main node_modules 内残留指向**本次 worktree** 的外来 junction,包管理器会经
|
|
549
|
+
// 反向链接穿透写坏主仓 node_modules(2026-09-15 实证的污染源头)——先清再装。
|
|
550
|
+
// 只清指向本 worktree 的(onlyUnder),全量治理交给 create/cleanup 挂点
|
|
551
|
+
// (worktree.js _sweepMainForeignJunctionLinks)。
|
|
552
|
+
if (mainCwd && existsSync(join(mainCwd, 'node_modules'))) {
|
|
553
|
+
try {
|
|
554
|
+
const preSweep = sweepForeignNodeModulesJunctions(mainCwd, {
|
|
555
|
+
worktreesRoot: resolvePath(mainCwd, '.sillyspec', '.runtime', 'worktrees'),
|
|
556
|
+
submodules: extractModulePaths(yamlText),
|
|
557
|
+
onlyUnder: worktreePath,
|
|
558
|
+
});
|
|
559
|
+
if (preSweep.removed.length > 0) {
|
|
560
|
+
console.warn(`⚠️ install 前清理:主仓 node_modules 内检测到指向本 worktree 的外来 junction(坑 junction-foreign-ownership),已移除 ${preSweep.removed.length} 个`);
|
|
561
|
+
for (const r of preSweep.removed) console.warn(` ${r.link} → ${r.target}`);
|
|
562
|
+
}
|
|
563
|
+
} catch { /* 清扫自身异常 fail-open,交 install 自然暴露问题 */ }
|
|
564
|
+
}
|
|
467
565
|
const installResult = tryInstall(installCmd, worktreePath, timeout);
|
|
468
566
|
result = installResult.ok
|
|
469
567
|
? { depsStatus: 'installed', depsMethod: 'install', depsSource: 'install', depsLockHash: wtHash }
|
|
@@ -518,10 +616,33 @@ export function provisionDeps(worktreePath, mainCwd, opts = {}) {
|
|
|
518
616
|
// generic monorepo(如 multi-agent-platform)worktree 子模块 frontend/sillyhub-daemon 无 node_modules,
|
|
519
617
|
// 跑 pnpm test 失败。读 local.yaml modules 块,对 nodejs 子模块 tryLink main 的 node_modules。
|
|
520
618
|
const modulePaths = extractModulePaths(yamlText);
|
|
619
|
+
// 注册仓根集合(坑 deps-sibling-repo-false-reject,2026-09-15 EHS 生产实证:modules 里登记的
|
|
620
|
+
// `../sub-grid-security` 是跨仓测试命令的合法配置,被「path 越界拒绝 link」当攻击面误报——该
|
|
621
|
+
// 目录本来就不在 worktree 内、无 main/worktree 双份语义,node_modules link 天然不适用,正确
|
|
622
|
+
// 动作是分类跳过+准确理由。未注册的越界路径维持 fail-closed 拒绝)。轻量解析(不引
|
|
623
|
+
// plan-postcheck 依赖边,条目形状 <key>: <path> 与 parseRepoRegistry 对齐)。
|
|
624
|
+
const registeredRepoRoots = (() => {
|
|
625
|
+
const roots = [];
|
|
626
|
+
const lines = String(yamlText || '').replace(/\r\n?/g, '\n').split('\n');
|
|
627
|
+
const start = lines.findIndex(l => /^repos:\s*(?:#.*)?$/.test(l));
|
|
628
|
+
if (start === -1) return roots;
|
|
629
|
+
for (let i = start + 1; i < lines.length; i++) {
|
|
630
|
+
if (lines[i].length > 0 && !/\s/.test(lines[i][0]) && lines[i].trim() !== '' && !lines[i].startsWith('#')) break;
|
|
631
|
+
const m = lines[i].match(/^\s+([A-Za-z0-9_.\-]+):\s*(\S+)/);
|
|
632
|
+
if (m && !m[2].startsWith('#')) roots.push(m[2].replace(/^['"]|['"]$/g, ''));
|
|
633
|
+
}
|
|
634
|
+
return roots;
|
|
635
|
+
})();
|
|
521
636
|
const moduleResults = [];
|
|
522
637
|
for (const mp of modulePaths) {
|
|
523
638
|
if (!isSafeModulePath(mp)) {
|
|
524
|
-
|
|
639
|
+
const resolvedMp = resolvePath(worktreePath, mp);
|
|
640
|
+
const hitRepo = registeredRepoRoots.some(rRoot => {
|
|
641
|
+
try { return resolvePath(mainCwd || worktreePath, rRoot) === resolvedMp } catch { return false }
|
|
642
|
+
});
|
|
643
|
+
moduleResults.push({ path: mp, status: 'skipped', reason: hitRepo
|
|
644
|
+
? '跨仓注册仓路径(repos 已注册)——不在 worktree 内,node_modules link 不适用;其测试命令经 modules.<name>.test 由 verify 直接在该仓执行'
|
|
645
|
+
: 'path 越界(绝对路径或 .. 段),拒绝 link' });
|
|
525
646
|
continue;
|
|
526
647
|
}
|
|
527
648
|
const wtDir = join(worktreePath, mp);
|