sillyspec 3.29.5 → 3.31.0
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-auto/SKILL.md +1 -1
- package/.claude/skills/sillyspec-brainstorm/SKILL.md +2 -2
- package/.claude/skills/sillyspec-commit/SKILL.md +1 -1
- package/.claude/skills/sillyspec-flow/SKILL.md +57 -0
- package/.claude/skills/sillyspec-quick/SKILL.md +7 -105
- package/AGENTS.md +38 -20
- package/SKILL.md +9 -3
- package/assets/command-cards/flow.md +32 -0
- package/assets/command-cards/run-brainstorm.md +1 -1
- package/assets/command-cards/run-quick.md +8 -25
- package/package.json +2 -1
- package/packages/dashboard/server/executor.js +2 -1
- package/packages/dashboard/server/parser.js +7 -7
- package/r7-test-env/README-R7-TEST.md +93 -0
- package/r7-test-env/demo-project/lib/calc.js +6 -0
- package/r7-test-env/demo-project/package.json +1 -0
- package/r7-test-env/demo-project/test.js +10 -0
- package/r7-test-env/prompt-R7-SMOKE.md +41 -0
- package/r7-test-env/sillyspec-r7.cmd +3 -0
- package/round4/account.mjs +18 -5
- package/round4/b1-analyze-task08.mjs +40 -0
- package/round4/b1-b1-target-validation.md +32 -0
- package/round4/b3a-prompt-audit-result.md +66 -0
- package/round4/b3a-prompt-audit.mjs +89 -0
- package/round4/optimization-plan.md +165 -0
- package/round4/r4-final-report.html +347 -0
- package/round4/r5-scenario-matrix.md +46 -0
- package/src/change-list.js +9 -0
- package/src/classify-change.js +6 -0
- package/src/command-cards.js +2 -1
- package/src/commit-guard.js +2 -0
- package/src/config-schema.js +64 -12
- package/src/db.js +35 -4
- package/src/decision-distill.js +9 -3
- package/src/docs-check.js +1 -1
- package/src/doctor-diagnostics.js +44 -2
- package/src/flow-draft.js +748 -0
- package/src/flow-parity.js +236 -0
- package/src/flow-review.js +184 -0
- package/src/flow.js +1492 -0
- package/src/foreign-declared.js +17 -1
- package/src/fr-index.js +396 -22
- package/src/fs-atomic.js +5 -1
- package/src/git-helper.js +2 -2
- package/src/greenfield-bootstrap.js +98 -0
- package/src/handoff.js +120 -7
- package/src/hunk-attribution.js +191 -0
- package/src/index.js +570 -16
- package/src/knowledge-classify.js +47 -1
- package/src/knowledge-digest.js +162 -0
- package/src/knowledge-quicklog.js +246 -0
- package/src/machine-draft.js +128 -0
- package/src/machine-interface.js +107 -5
- package/src/mcp-server.js +1 -0
- package/src/module-resolve.js +3 -1
- package/src/preview-progress.js +160 -0
- package/src/progress/change-registry.js +11 -3
- package/src/progress/shared.js +3 -2
- package/src/progress.js +56 -13
- package/src/quicklog.js +44 -6
- package/src/redomain.js +147 -0
- package/src/review-material-pack.js +204 -1
- package/src/run/archive-distill.js +4 -2
- package/src/run/bg-sync.js +67 -0
- package/src/run/command.js +123 -10
- package/src/run/complete-handlers.js +327 -72
- package/src/run/complete.js +350 -71
- package/src/run/gate-snapshot-ledger.js +213 -0
- package/src/run/gate-snapshot.js +250 -34
- package/src/run/gates.js +370 -233
- package/src/run/green-cache.js +123 -0
- package/src/run/junction-rm.js +70 -0
- package/src/run/next.js +14 -0
- package/src/run/prompt.js +185 -55
- package/src/run/quick-audit.js +185 -12
- package/src/run/shared.js +120 -3
- package/src/run/stage.js +182 -216
- package/src/run/test-ledger.js +264 -0
- package/src/run/verify-quality-scan.js +84 -7
- package/src/scope-audit.js +6 -1
- package/src/sentinel-assertions.js +114 -0
- package/src/setup.js +3 -3
- package/src/spec-sync.js +41 -1
- package/src/stage-contract.js +26 -8
- package/src/stage-review-checklist.js +2 -0
- package/src/stage-review.js +7 -2
- package/src/stage-templates.js +157 -0
- package/src/stages/archive.js +2 -0
- package/src/stages/brainstorm.js +24 -106
- package/src/stages/execute.js +221 -84
- package/src/stages/knowledge.js +74 -1
- package/src/stages/plan-postcheck.js +352 -1
- package/src/stages/plan.js +23 -1
- package/src/stages/quick.js +10 -2
- package/src/stages/verify.js +4 -2
- package/src/sync.js +81 -23
- package/src/task-done.js +181 -0
- package/src/task-review.js +8 -55
- package/src/test-bindings.js +549 -0
- package/src/timeline.js +218 -0
- package/src/ui-visual.js +175 -0
- package/src/verify-draft.js +214 -0
- package/src/verify-postcheck.js +860 -158
- package/src/verify-probes.js +59 -3
- package/src/watcher.js +1031 -0
- package/src/worktree-apply.js +44 -4
- package/src/worktree-deps.js +22 -8
- package/src/worktree-policy.js +42 -0
- package/src/worktree.js +102 -15
- package/templates/agents-instruction.md +37 -18
package/src/worktree-apply.js
CHANGED
|
@@ -16,7 +16,7 @@ import { existsSync, unlinkSync, writeFileSync, mkdtempSync, rmSync, readdirSync
|
|
|
16
16
|
import { join, resolve } from 'path';
|
|
17
17
|
import { tmpdir } from 'os';
|
|
18
18
|
import { createHash } from 'crypto';
|
|
19
|
-
import { WorktreeManager } from './worktree.js';
|
|
19
|
+
import { WorktreeManager, detectDeliverableContained } from './worktree.js';
|
|
20
20
|
import { getCrossWorktreeMeta, cleanupCrossWorktrees } from './worktree-cross.js';
|
|
21
21
|
import { parseFileChangeList, parseFileChangeListDetailed, pathMatches } from './change-list.js';
|
|
22
22
|
import { parseAllowedPaths, parseRepo } from './stages/plan-postcheck.js';
|
|
@@ -25,6 +25,7 @@ import { resolveLatestExecuteRunId, resolveLatestExecuteRunIdWithTasks, readRevi
|
|
|
25
25
|
import { collectActiveQuickGuardFiles } from './quicklog.js';
|
|
26
26
|
import { appendWriteAudit, WRITE_AUDIT_FILE_CAP } from './write-audit.js';
|
|
27
27
|
import { detectCommittedDrift, formatCommittedDriftWarning } from './run/concurrent-detect.js';
|
|
28
|
+
import { readWorktreePolicy } from './worktree-policy.js';
|
|
28
29
|
|
|
29
30
|
const CHANGES_REL = '.sillyspec/changes';
|
|
30
31
|
|
|
@@ -1254,6 +1255,21 @@ function applyCrossRepoWorktrees(changeName, projectRoot, ctx, { checkOnly = fal
|
|
|
1254
1255
|
|
|
1255
1256
|
export function applyWorktree(changeName, { cwd, checkOnly = false, merge = false, base = 'merge-base', ctx = null, skipOverlap = false, stashDirty = false, force = false, autoApply = false } = {}) {
|
|
1256
1257
|
const projectRoot = cwd || process.cwd();
|
|
1258
|
+
// ── worktree.policy 选择记忆(R16 减负批次,2026-09-24)──
|
|
1259
|
+
// 显式 flag > policy > fail-closed 缺省;policy 触发的 force/skip 走同一条留痕路径
|
|
1260
|
+
//(overlapForced/overlapSkipped),不开新权限面——只消「同一冲突每次重拦重问」的摩擦轮。
|
|
1261
|
+
let policyOverlapForced = false;
|
|
1262
|
+
try {
|
|
1263
|
+
const policy = readWorktreePolicy(projectRoot);
|
|
1264
|
+
if (policy.applyOverlap === 'force' && !force && !autoApply) {
|
|
1265
|
+
force = true;
|
|
1266
|
+
policyOverlapForced = true;
|
|
1267
|
+
}
|
|
1268
|
+
if (policy.stashDirty && !stashDirty) {
|
|
1269
|
+
stashDirty = true;
|
|
1270
|
+
console.log(`📦 worktree.policy.stash_dirty=true:主仓在途改动将自动 stash(等效常备 --stash-dirty,local.yaml 选择记忆)`);
|
|
1271
|
+
}
|
|
1272
|
+
} catch { /* policy 读失败 → 全缺省 fail-closed,行为与无 policy 完全一致 */ }
|
|
1257
1273
|
const wm = new WorktreeManager({ cwd: projectRoot });
|
|
1258
1274
|
const meta = wm.getMeta(changeName);
|
|
1259
1275
|
const result = {
|
|
@@ -1571,10 +1587,11 @@ export function applyWorktree(changeName, { cwd, checkOnly = false, merge = fals
|
|
|
1571
1587
|
const files = [...new Set(guardCheck.overlaps.map(o => o.file))];
|
|
1572
1588
|
const pairLines = guardCheck.overlaps.map(o => ` [${o.sessionId}] ${o.file}`);
|
|
1573
1589
|
if (force) {
|
|
1574
|
-
// 显式解锁:放行并留痕(审计可见——自动路径永不带 force,overlapForced 只可能来自人工 --force
|
|
1575
|
-
|
|
1590
|
+
// 显式解锁:放行并留痕(审计可见——自动路径永不带 force,overlapForced 只可能来自人工 --force
|
|
1591
|
+
// 或 worktree.policy.apply_overlap=force 的选择记忆[R16 减负批次],两种来源均留痕)
|
|
1592
|
+
result.overlapForced = { sessions, files, ...(policyOverlapForced ? { via: 'worktree.policy.apply_overlap=force' } : {}) };
|
|
1576
1593
|
result.warnings = (result.warnings || []).concat([
|
|
1577
|
-
|
|
1594
|
+
`${policyOverlapForced ? 'worktree.policy.apply_overlap=force 选择记忆放行(local.yaml)' : '--force 越过 guard 相交预检'}:${guardCheck.overlaps.length} 个会话×文件对与活跃 quick 会话声明重叠——` +
|
|
1578
1595
|
pairLines.join(';') + `(对方会话在途改动可能被覆盖,overlapForced 留痕)`
|
|
1579
1596
|
]);
|
|
1580
1597
|
} else if (autoApply) {
|
|
@@ -1874,6 +1891,29 @@ export function applyWorktree(changeName, { cwd, checkOnly = false, merge = fals
|
|
|
1874
1891
|
|
|
1875
1892
|
// 5b hashMismatch 计算已前移到 step3.5(Grill P0),见上
|
|
1876
1893
|
|
|
1894
|
+
// ── 合并感知包含清理(ql-20260921-010 修复,ql-20260921-009② 的晚置重接线)──
|
|
1895
|
+
// 4.5/4.5b 合并管线跑完之后、patch 生成/checkOnly 返回之前:对 changedFiles 残余面逐文件
|
|
1896
|
+
// 只读三方包含测试(merge(base, ours=主仓现状, theirs=worktree) clean 且===主仓现状 ⟺
|
|
1897
|
+
// 交付 delta 已全在主仓——差异纯属并行增量)。命中者剔出 changedFiles(面清:归档/清理门
|
|
1898
|
+
// 的「未 apply」判定不再误拦三方合并落地件),审计入 result.mergeContainedFiles。
|
|
1899
|
+
// 早于合并步做(009② 原形态)会抢占 EXCLUDE-DIRTY/MISMATCH 管线——mergedDirtyFiles 审计
|
|
1900
|
+
// 与合并写回被短路(apply-dirty-threeway 主仓红实证),故必须晚置。
|
|
1901
|
+
try {
|
|
1902
|
+
if (changedFiles.length > 0) {
|
|
1903
|
+
const containedFiles = detectDeliverableContained(
|
|
1904
|
+
{ projectRoot, worktreePath, baseRef: deliverableBase }, changedFiles);
|
|
1905
|
+
if (containedFiles.length > 0) {
|
|
1906
|
+
const containedSet = new Set(containedFiles);
|
|
1907
|
+
changedFiles = changedFiles.filter(f => !containedSet.has(f));
|
|
1908
|
+
result.mergeContainedFiles = containedFiles;
|
|
1909
|
+
result.warnings = result.warnings || [];
|
|
1910
|
+
result.warnings.push(
|
|
1911
|
+
containedFiles.length + ' 个文件交付已包含于主仓当前内容(差异为并行会话增量,合并感知判定不计入未 apply 面):' +
|
|
1912
|
+
containedFiles.slice(0, 5).join(', ') + (containedFiles.length > 5 ? ' 等' : ''));
|
|
1913
|
+
}
|
|
1914
|
+
}
|
|
1915
|
+
} catch { /* 包含判定异常 → 保守保留(宁误拦不误放) */ }
|
|
1916
|
+
|
|
1877
1917
|
// --- 6. checkOnly 模式:到此返回 ---
|
|
1878
1918
|
if (checkOnly) {
|
|
1879
1919
|
result.ok = true;
|
package/src/worktree-deps.js
CHANGED
|
@@ -74,8 +74,22 @@ const ECOSYSTEMS = [
|
|
|
74
74
|
marker: null,
|
|
75
75
|
detect: (d) => existsSync(join(d, 'pyproject.toml')) || existsSync(join(d, 'requirements.txt')),
|
|
76
76
|
// uv 优先(现代 Python 工具链,pyproject/uv.lock 项目走 uv sync 建 .venv + 装依赖,
|
|
77
|
-
// 与 execute worktree 环境预告一致);纯 requirements.txt(无 pyproject)回退 pip
|
|
78
|
-
|
|
77
|
+
// 与 execute worktree 环境预告一致);纯 requirements.txt(无 pyproject)回退 pip。
|
|
78
|
+
// extras 探测(R14/R15 对撞实证 2026-09-24):裸 uv sync 不装 [project.optional-dependencies]
|
|
79
|
+
// extras,worktree / verify deps 门重建的 .venv 缺 dev extras → `uv run pytest` 回落全局
|
|
80
|
+
// 环境报批量 ModuleNotFoundError(R14-OS 244 个,每次 6 轮自愈)。声明了 optional-dependencies
|
|
81
|
+
// 的仓补 --all-extras;[dependency-groups] 的 dev 组 uv sync 缺省已含,不加 flag。
|
|
82
|
+
// 白名单(INSTALL_BINARY_WHITELIST)按 binary 首 token 判定,--all-extras 无元字符天然放行。
|
|
83
|
+
install: (d) => {
|
|
84
|
+
if (existsSync(join(d, 'uv.lock')) || existsSync(join(d, 'pyproject.toml'))) {
|
|
85
|
+
try {
|
|
86
|
+
const py = readFileSync(join(d, 'pyproject.toml'), 'utf8')
|
|
87
|
+
if (/^\s*\[project\.optional-dependencies\]/m.test(py)) return 'uv sync --all-extras'
|
|
88
|
+
} catch { /* pyproject 读不到(纯 uv.lock 残缺 checkout)按裸 uv sync */ }
|
|
89
|
+
return 'uv sync'
|
|
90
|
+
}
|
|
91
|
+
return 'pip install -r requirements.txt'
|
|
92
|
+
},
|
|
79
93
|
},
|
|
80
94
|
{
|
|
81
95
|
type: 'go',
|
|
@@ -229,7 +243,7 @@ function tryLink(mainNodeModules, linkPath) {
|
|
|
229
243
|
try {
|
|
230
244
|
if (lstatSync(linkPath).isSymbolicLink()) {
|
|
231
245
|
if (process.platform === 'win32') {
|
|
232
|
-
execFileSync('cmd.exe', ['/c', 'rmdir', linkPath], { stdio: ['pipe', 'pipe', 'pipe'] });
|
|
246
|
+
execFileSync('cmd.exe', ['/c', 'rmdir', linkPath], { stdio: ['pipe', 'pipe', 'pipe'], windowsHide: true });
|
|
233
247
|
} else {
|
|
234
248
|
unlinkSync(linkPath);
|
|
235
249
|
}
|
|
@@ -239,9 +253,9 @@ function tryLink(mainNodeModules, linkPath) {
|
|
|
239
253
|
if (process.platform === 'win32') {
|
|
240
254
|
// execFileSync 数组形式不经 shell:POSIX 双引号内 `/$() 会执行、cmd.exe 双引号内 %VAR% 仍展开,
|
|
241
255
|
// linkPath 含 local.yaml 模块 path,属项目内可配置值(安全收敛,与 git-helper 同范式)
|
|
242
|
-
execFileSync('cmd.exe', ['/c', 'mklink', '/J', linkPath, mainNodeModules], { stdio: ['pipe', 'pipe', 'pipe'] });
|
|
256
|
+
execFileSync('cmd.exe', ['/c', 'mklink', '/J', linkPath, mainNodeModules], { stdio: ['pipe', 'pipe', 'pipe'], windowsHide: true });
|
|
243
257
|
} else {
|
|
244
|
-
execFileSync('ln', ['-s', mainNodeModules, linkPath], { stdio: ['pipe', 'pipe', 'pipe'] });
|
|
258
|
+
execFileSync('ln', ['-s', mainNodeModules, linkPath], { stdio: ['pipe', 'pipe', 'pipe'], windowsHide: true });
|
|
245
259
|
}
|
|
246
260
|
// 创建后实物复核(坑 provision-silent-fake-installed 第②层,2026-08-21 实证:doctor 报
|
|
247
261
|
// re-provisioned 成功但 junction 实际没建)——mklink/ln 退出码 0 不等于链接落盘(cmd.exe
|
|
@@ -325,9 +339,9 @@ function tryInstall(cmd, cwd, timeout) {
|
|
|
325
339
|
if (process.platform === 'win32') {
|
|
326
340
|
// Windows 包管理器是 .cmd 垫片,无 shell 的 spawn 无法解析——经 cmd.exe /c 传参;
|
|
327
341
|
// 元字符(含 %)已拦,残余风险与 tryLink 的 mklink 同款
|
|
328
|
-
execFileSync('cmd.exe', ['/c', ...argv], { cwd: curCwd, timeout, stdio: ['pipe', 'pipe', 'pipe'] });
|
|
342
|
+
execFileSync('cmd.exe', ['/c', ...argv], { cwd: curCwd, timeout, stdio: ['pipe', 'pipe', 'pipe'], windowsHide: true });
|
|
329
343
|
} else {
|
|
330
|
-
execFileSync(argv[0], argv.slice(1), { cwd: curCwd, timeout, stdio: ['pipe', 'pipe', 'pipe'] });
|
|
344
|
+
execFileSync(argv[0], argv.slice(1), { cwd: curCwd, timeout, stdio: ['pipe', 'pipe', 'pipe'], windowsHide: true });
|
|
331
345
|
}
|
|
332
346
|
} catch (e) {
|
|
333
347
|
const raw = e.killed ? `timeout after ${timeout}ms` : ((e.stderr && e.stderr.toString()) || e.message);
|
|
@@ -454,7 +468,7 @@ export function sweepForeignNodeModulesJunctions(mainDir, { worktreesRoot, specB
|
|
|
454
468
|
if (dryRun) continue;
|
|
455
469
|
try {
|
|
456
470
|
if (process.platform === 'win32') {
|
|
457
|
-
execFileSync('cmd.exe', ['/c', 'rmdir', linkPath], { stdio: ['pipe', 'pipe', 'pipe'] });
|
|
471
|
+
execFileSync('cmd.exe', ['/c', 'rmdir', linkPath], { stdio: ['pipe', 'pipe', 'pipe'], windowsHide: true });
|
|
458
472
|
} else {
|
|
459
473
|
unlinkSync(linkPath);
|
|
460
474
|
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* worktree-policy.js — worktree 守卫选择记忆(R16 减负批次,2026-09-24)
|
|
3
|
+
*
|
|
4
|
+
* 背景:worktree/apply 三处守卫(分支冲突三选一 / guard 相交三态 / 主仓脏树 stash)此前
|
|
5
|
+
* 全是 fail-closed 抛错 + 文字菜单,选择只活在当次 flag 里不落任何配置——同一冲突下次
|
|
6
|
+
* 原样重拦重问(对撞 R15 实证:无人值守跑批时这类重拦是纯摩擦轮)。
|
|
7
|
+
*
|
|
8
|
+
* 本模块读 local.yaml `worktree.policy` 段(js-yaml,best-effort 绝不抛):
|
|
9
|
+
* adopt_branch: true → 分支已存在冲突时自动按「收编既有分支」处置(等效常备 --adopt-branch)
|
|
10
|
+
* apply_overlap: force | skip | manual(缺省 manual = 既有 fail-closed)
|
|
11
|
+
* → guard 相交拦截的无人值守处置(force=放行留痕 / skip=软跳过)
|
|
12
|
+
* stash_dirty: true → 主仓脏树时自动 stash(等效常备 --stash-dirty)
|
|
13
|
+
*
|
|
14
|
+
* 优先级铁律:显式 flag > policy > fail-closed 缺省。policy 只消「重复选择」,不开新权限面
|
|
15
|
+
* ——force/skip 经 policy 触发时同样落 overlapForced/overlapSkipped 留痕(审计可见)。
|
|
16
|
+
*/
|
|
17
|
+
import { readFileSync } from 'fs'
|
|
18
|
+
import { join } from 'path'
|
|
19
|
+
import jsYaml from 'js-yaml'
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* 读 worktree.policy 段。
|
|
23
|
+
* @param {string} repoRoot - 主仓根(local.yaml 在 <repoRoot>/.sillyspec/local.yaml)
|
|
24
|
+
* @returns {{ adoptBranch: boolean, applyOverlap: 'manual'|'force'|'skip', stashDirty: boolean }}
|
|
25
|
+
* 文件缺/解析失败/段缺 → 全缺省(adoptBranch=false, applyOverlap='manual', stashDirty=false)。
|
|
26
|
+
*/
|
|
27
|
+
export function readWorktreePolicy(repoRoot) {
|
|
28
|
+
const fallback = { adoptBranch: false, applyOverlap: 'manual', stashDirty: false }
|
|
29
|
+
try {
|
|
30
|
+
const text = readFileSync(join(repoRoot, '.sillyspec', 'local.yaml'), 'utf8')
|
|
31
|
+
const doc = jsYaml.load(text) || {}
|
|
32
|
+
const policy = doc && doc.worktree && doc.worktree.policy
|
|
33
|
+
if (!policy || typeof policy !== 'object') return fallback
|
|
34
|
+
return {
|
|
35
|
+
adoptBranch: policy.adopt_branch === true,
|
|
36
|
+
applyOverlap: policy.apply_overlap === 'force' || policy.apply_overlap === 'skip' ? policy.apply_overlap : 'manual',
|
|
37
|
+
stashDirty: policy.stash_dirty === true,
|
|
38
|
+
}
|
|
39
|
+
} catch {
|
|
40
|
+
return fallback
|
|
41
|
+
}
|
|
42
|
+
}
|
package/src/worktree.js
CHANGED
|
@@ -9,8 +9,9 @@
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
import { execFileSync } from 'child_process';
|
|
12
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync, rmSync, readdirSync, statSync, lstatSync, readlinkSync, unlinkSync, copyFileSync } from 'fs';
|
|
12
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync, rmSync, readdirSync, statSync, lstatSync, readlinkSync, unlinkSync, copyFileSync, mkdtempSync } from 'fs';
|
|
13
13
|
import { join, resolve, dirname, relative, isAbsolute, basename } from 'path';
|
|
14
|
+
import { tmpdir } from 'os';
|
|
14
15
|
import { createHash } from 'crypto';
|
|
15
16
|
import { provisionDeps, checkDepsFreshness, detectEditableInstallEscape, sweepForeignNodeModulesJunctions } from './worktree-deps.js';
|
|
16
17
|
import { resolveRuntimeRoot } from './run/shared.js';
|
|
@@ -20,6 +21,7 @@ import { git, gitQuiet, unquoteGitPath } from './git-helper.js';
|
|
|
20
21
|
// foreign-declared 零环独立模块(仅依赖 change-list/git-helper/stages/plan-postcheck,
|
|
21
22
|
// 链上无回边)——contract-matrix/verify-probes/task-review 等处已有同款 import 先例
|
|
22
23
|
import { splitOwnVsForeignDiffFiles } from './foreign-declared.js';
|
|
24
|
+
import { readWorktreePolicy } from './worktree-policy.js';
|
|
23
25
|
|
|
24
26
|
// meta.json 会被 hook 进程与其它 CLI 进程并发读取(worktree-guard / getMeta / create 幽灵判定),
|
|
25
27
|
// 必须原子写:半截 JSON 会让 getMeta 返回 null → 触发幽灵 worktree 强删(可能丢 gitignored 改动)。
|
|
@@ -47,6 +49,62 @@ function _chunkPathsPrivate(paths, maxChars = 8000) {
|
|
|
47
49
|
return batches
|
|
48
50
|
}
|
|
49
51
|
|
|
52
|
+
/**
|
|
53
|
+
* 交付包含判定(合并感知「未 apply」口径,ql-20260921-009 ②,2026-09-21 batch2 归档实证)。
|
|
54
|
+
*
|
|
55
|
+
* 语义:worktree 交付文件与主仓**当前工作区**内容不等时,做只读三方合并测试——
|
|
56
|
+
* base=交付锚点(baselineCommit||baseHash)版本、ours=主仓工作区内容、theirs=worktree
|
|
57
|
+
* 交付内容;clean 合并且结果 === 主仓工作区内容 ⟺ worktree 的全部改动已被主仓包含
|
|
58
|
+
* (差异全是并行会话在主仓的增量)→ 该文件视为已 apply。实证形态:交付文件经
|
|
59
|
+
* EXCLUDE-DIRTY/MISMATCH 三方合并落地后天然含并行增量,字节等值口径的归档/清理门
|
|
60
|
+
* 恒误拦「未 apply」(分支非 merge 落地时可达性短路也不命中),人工 grep 自证后才
|
|
61
|
+
* 走 --skip-apply/--force 逃生口。
|
|
62
|
+
*
|
|
63
|
+
* 与 worktree-apply.js mergeDirtyOverlapThreeWay 的差异:**只读不写回**(判定专用,
|
|
64
|
+
* 无备份/暂存面);冲突/取不到 base/异常 fail-safe 不剔(宁误拦不误放)。放本文件
|
|
65
|
+
* 导出供 worktree-apply.js 消费(那边 import 本文件是既有方向,反向会成环——
|
|
66
|
+
* _lsTreeBlobs 注释同款约束)。成本:仅对「内容不等」的残余交付文件逐个一次
|
|
67
|
+
* git merge-file(正常收尾路径 0 个文件触发),生命周期边界点一次性开销。
|
|
68
|
+
*
|
|
69
|
+
* @param {{ projectRoot: string, worktreePath: string, baseRef: string }} ctx
|
|
70
|
+
* @param {string[]} files 待判文件(仓库根相对路径;不可读/base 缺席者自动跳过)
|
|
71
|
+
* @returns {string[]} 判定为「已包含于主仓当前内容」的文件子集(files 原字面)
|
|
72
|
+
*/
|
|
73
|
+
export function detectDeliverableContained({ projectRoot, worktreePath, baseRef }, files) {
|
|
74
|
+
const contained = []
|
|
75
|
+
if (!Array.isArray(files) || files.length === 0 || !baseRef) return contained
|
|
76
|
+
let tmpDir = null
|
|
77
|
+
try {
|
|
78
|
+
for (const f of files) {
|
|
79
|
+
if (!f || typeof f !== 'string') continue
|
|
80
|
+
try {
|
|
81
|
+
const wtRaw = readFileSync(join(worktreePath, f))
|
|
82
|
+
const mainRaw = readFileSync(join(projectRoot, f))
|
|
83
|
+
if (wtRaw.equals(mainRaw)) { contained.push(f); continue } // 快路径:内容相等天然已含
|
|
84
|
+
const baseRaw = git(projectRoot, ['show', `${baseRef}:${f}`], { encoding: 'buffer', timeout: 15000 })
|
|
85
|
+
if (!tmpDir) tmpDir = mkdtempSync(join(tmpdir(), 'sillyspec-contain-'))
|
|
86
|
+
const p = (tag) => join(tmpDir, `${tag}-${Math.random().toString(36).slice(2, 8)}`)
|
|
87
|
+
const baseP = p('base'); const oursP = p('ours'); const theirsP = p('theirs')
|
|
88
|
+
writeFileSync(baseP, baseRaw); writeFileSync(oursP, mainRaw); writeFileSync(theirsP, wtRaw)
|
|
89
|
+
// merge-file <current=ours(主仓现状)> <base> <other=theirs(worktree 交付)> -p:
|
|
90
|
+
// stdout=合并结果;exit 0=clean。结果===主仓现状 ⟺ 交付 delta 已全在主仓。
|
|
91
|
+
let outBuf, code = 0
|
|
92
|
+
try {
|
|
93
|
+
outBuf = git(projectRoot, ['merge-file', '-p', oursP, baseP, theirsP], { encoding: 'buffer', timeout: 15000 })
|
|
94
|
+
} catch (e) {
|
|
95
|
+
code = (typeof e.status === 'number') ? e.status : -1
|
|
96
|
+
outBuf = (e.stdout instanceof Buffer) ? e.stdout : Buffer.alloc(0)
|
|
97
|
+
if (code < 0) continue // merge-file 不可用 → 保守不剔
|
|
98
|
+
}
|
|
99
|
+
if (code === 0 && outBuf.equals(mainRaw)) contained.push(f)
|
|
100
|
+
} catch { /* 该文件判定不可得 → 保守不剔 */ }
|
|
101
|
+
}
|
|
102
|
+
} finally {
|
|
103
|
+
if (tmpDir) { try { rmSync(tmpDir, { recursive: true, force: true }) } catch {} }
|
|
104
|
+
}
|
|
105
|
+
return contained
|
|
106
|
+
}
|
|
107
|
+
|
|
50
108
|
// ── supplyFiles 生成物供给(坑③ / FR-03 / D-003@v1)──
|
|
51
109
|
// .gitignore 生成物(如 src/build-id.ts)不在 git 树也不进 untracked overlay
|
|
52
110
|
// (ls-files --others --exclude-standard 尊重 .gitignore),worktree 缺失致构建炸
|
|
@@ -163,7 +221,7 @@ export function unlinkNodeModulesLinks(worktreePath, meta = null, details = null
|
|
|
163
221
|
try {
|
|
164
222
|
if (process.platform === 'win32') {
|
|
165
223
|
// Windows rmdir 删 junction(reparse point)不跟随目标;execFileSync 数组形式不经 shell
|
|
166
|
-
execFileSync('cmd.exe', ['/c', 'rmdir', nm]);
|
|
224
|
+
execFileSync('cmd.exe', ['/c', 'rmdir', nm], { windowsHide: true });
|
|
167
225
|
} else {
|
|
168
226
|
unlinkSync(nm);
|
|
169
227
|
}
|
|
@@ -695,17 +753,28 @@ export class WorktreeManager {
|
|
|
695
753
|
// 2. 检查分支是否已存在(坑 worktree-user-branch-conflict,2026-08-24 用户反馈五期①:
|
|
696
754
|
// 「用户要求在指定分支上做」时既有同名分支让 execute 直接死锁,旧报错只说 Run cleanup
|
|
697
755
|
// first、修复建议无条件推荐删分支——用户自建分支被引导误删。现给决策菜单;--adopt-branch
|
|
698
|
-
// 显式收编:检出该分支为 worktree 工作分支,分支现状作 baseline
|
|
756
|
+
// 显式收编:检出该分支为 worktree 工作分支,分支现状作 baseline,仅后续新改动计入交付。
|
|
757
|
+
// worktree.policy.adopt_branch(R16 减负批次 2026-09-24):选择记忆——显式 flag 缺席时
|
|
758
|
+
// policy=true 等效常备 --adopt-branch(adoption 留痕同款),消「同一冲突每次重拦重问」。
|
|
699
759
|
const branchExists = !!gitQuiet(this.cwd, ['rev-parse', '--verify', `refs/heads/${branch}`]);
|
|
700
760
|
let adoptedBranch = false;
|
|
761
|
+
let adoptViaPolicy = false;
|
|
701
762
|
if (branchExists && !adoptBranch) {
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
763
|
+
const policy = readWorktreePolicy(this.cwd);
|
|
764
|
+
if (policy.adoptBranch) {
|
|
765
|
+
adoptBranch = true;
|
|
766
|
+
adoptViaPolicy = true;
|
|
767
|
+
console.log(`📌 worktree.policy.adopt_branch=true:分支 ${branch} 已存在,自动收编(等效 --adopt-branch,local.yaml 选择记忆)`);
|
|
768
|
+
} else {
|
|
769
|
+
throw new Error(
|
|
770
|
+
`分支已存在:${branch}(worktree create 需新建同名分支,直接冲突)。按实际情况三选一:\n` +
|
|
771
|
+
` ① 本变更的遗留分支(上次 execute 残留、内容已落地或作废):确认后 git branch -D ${branch} 再重跑;\n` +
|
|
772
|
+
` ② 该分支是你有意让本变更在其上做的(用户指定分支):sillyspec run execute --change ${name} --adopt-branch\n` +
|
|
773
|
+
` —— 收编为 worktree 工作分支,分支既有内容计入 baseline(仅后续新改动算交付 diff);\n` +
|
|
774
|
+
` (每次都选②可在 local.yaml 配 worktree.policy.adopt_branch: true 记忆该选择)\n` +
|
|
775
|
+
` ③ 换变更名重跑。`
|
|
776
|
+
);
|
|
777
|
+
}
|
|
709
778
|
}
|
|
710
779
|
|
|
711
780
|
// 3. 解析 base 分支
|
|
@@ -865,12 +934,13 @@ export class WorktreeManager {
|
|
|
865
934
|
// 5.9 实供清单(纯记录面,doctor/审计可读;空供给省略字段——存量 meta 缺省兼容)
|
|
866
935
|
...(suppliedFiles.length > 0 ? { supplyFiles: suppliedFiles } : {}),
|
|
867
936
|
...(adoptedBranch ? { adoptedBranch: true } : {}),
|
|
937
|
+
...(adoptViaPolicy ? { adoptViaPolicy: true } : {}),
|
|
868
938
|
};
|
|
869
939
|
|
|
870
940
|
const metaPath = join(worktreePath, META_FILE);
|
|
871
941
|
writeMetaAtomic(metaPath, meta);
|
|
872
942
|
|
|
873
|
-
return { branch, worktreePath, baseHash, mode: meta.mode, syncDiagnostic, ...(adoptedBranch ? { adoptedBranch: true } : {}) };
|
|
943
|
+
return { branch, worktreePath, baseHash, mode: meta.mode, syncDiagnostic, ...(adoptedBranch ? { adoptedBranch: true } : {}), ...(adoptViaPolicy ? { adoptViaPolicy: true } : {}) };
|
|
874
944
|
}
|
|
875
945
|
|
|
876
946
|
/**
|
|
@@ -1957,7 +2027,7 @@ export class WorktreeManager {
|
|
|
1957
2027
|
}
|
|
1958
2028
|
|
|
1959
2029
|
// 2) 候选集中尚未落到主工作区 HEAD 的子集
|
|
1960
|
-
const pending = this._changesAlreadyOnMain(worktreePath, tracked, untracked);
|
|
2030
|
+
const pending = this._changesAlreadyOnMain(worktreePath, tracked, untracked, diffBase);
|
|
1961
2031
|
// 3) main 工作区副本降噪层(坑 unapplied-false-positive-workspace-copy,2026-08-21 实证:
|
|
1962
2032
|
// apply 后 main 工作区有逐字节一致副本但未 commit → HEAD-only 判定报「未落仓」虚警拦
|
|
1963
2033
|
// doctor)。原注释「不查 main 工作区未提交副本(防误删)」的保护意图是内容未落地——
|
|
@@ -1995,9 +2065,9 @@ export class WorktreeManager {
|
|
|
1995
2065
|
* @param {string[]} untrackedFiles
|
|
1996
2066
|
* @returns {string[]} 尚未落到 main HEAD 的文件
|
|
1997
2067
|
*/
|
|
1998
|
-
_changesAlreadyOnMain(worktreePath, trackedFiles, untrackedFiles) {
|
|
2068
|
+
_changesAlreadyOnMain(worktreePath, trackedFiles, untrackedFiles, diffBase = null) {
|
|
1999
2069
|
const mainHead = git(this.cwd, ['rev-parse', 'HEAD']); // 失败即抛 → 外层 catch fail-safe
|
|
2000
|
-
|
|
2070
|
+
let pending = [];
|
|
2001
2071
|
|
|
2002
2072
|
if (trackedFiles.length > 0) {
|
|
2003
2073
|
const diverged = (gitQuiet(worktreePath,
|
|
@@ -2024,7 +2094,24 @@ export class WorktreeManager {
|
|
|
2024
2094
|
}
|
|
2025
2095
|
}
|
|
2026
2096
|
|
|
2027
|
-
|
|
2097
|
+
pending = [...new Set(pending)];
|
|
2098
|
+
|
|
2099
|
+
// 合并感知包含剔除(ql-20260921-009 ②):pending 文件(≠主仓 HEAD)与主仓**当前工作区**
|
|
2100
|
+
// 内容逐个只读三方合并测试——worktree delta 已全在主仓(差异=并行增量,如三方合并落地后
|
|
2101
|
+
// 形态)→ 视为已落地剔除。字节等值口径对该形态恒误拦(2026-09-21 batch2 归档实证),
|
|
2102
|
+
// 分支非 merge 落地时上方可达性短路也不命中。冲突/异常 fail-safe 不剔。
|
|
2103
|
+
if (diffBase && pending.length > 0) {
|
|
2104
|
+
const contained = detectDeliverableContained(
|
|
2105
|
+
{ projectRoot: this.cwd, worktreePath, baseRef: diffBase },
|
|
2106
|
+
pending,
|
|
2107
|
+
);
|
|
2108
|
+
if (contained.length > 0) {
|
|
2109
|
+
const containedSet = new Set(contained);
|
|
2110
|
+
pending = pending.filter(f => !containedSet.has(f));
|
|
2111
|
+
}
|
|
2112
|
+
}
|
|
2113
|
+
|
|
2114
|
+
return pending;
|
|
2028
2115
|
}
|
|
2029
2116
|
|
|
2030
2117
|
/**
|
|
@@ -1,24 +1,43 @@
|
|
|
1
1
|
# Agent 指引
|
|
2
2
|
|
|
3
3
|
## 项目说明
|
|
4
|
-
本项目使用 **SillySpec** 管理变更,采用文档驱动开发。SillySpec 是给 Agent 调用的 CLI
|
|
4
|
+
本项目使用 **SillySpec** 管理变更,采用文档驱动开发。SillySpec 是给 Agent 调用的 CLI 流程控制器:你通过 CLI 告诉它"我在哪",它告诉你"下一步做什么";你执行步骤,它校验产出、推进状态,人类只在关键决策点介入审批。要考虑多 agent 同时操作代码,代码随时可能变化。所有变更以稳定、可用、可维护为目标,按生产级标准处理。
|
|
5
|
+
|
|
6
|
+
## 选道(动手前先对号入座)
|
|
7
|
+
| 需求形态 | 走法 |
|
|
8
|
+
|---|---|
|
|
9
|
+
| 需求已含决策(改什么、成功标准说得清) | **轻量变更(默认快道)**:`flow start` → 直接干活 → `flow done`,全程 2 次协议调用 |
|
|
10
|
+
| 需求不清晰 / 需要方案探索 | 头脑风暴预段:`run brainstorm --change <名>` → 完成后 `flow start --change <名>` 收编续跑(产物自动接管,不重复起草;design 以头脑风暴版为准) |
|
|
11
|
+
| 大改动:跨模块取舍 / 需要 Wave 计划编排 / 多阶段治理 / 设计期人机对抗 | 完整流程:`run brainstorm → plan → execute → verify → archive` 五阶段(每阶段一次渲染 + 一次 --done 收口;local.yaml 开 stage.burst 时一次下发全部步骤说明书;verify 用 `verify-probes --init --draft` 机器预填) |
|
|
12
|
+
| 代码已先写好(倒推收尾) | 不回头补 brainstorm/plan 装样子:`flow start --change <名> --input "<已做改动的描述+成功标准>"` → `flow done`,实测门+测试绑定+patch 留档一步收口 |
|
|
13
|
+
|
|
14
|
+
选道看流程形态需求,不看技术关键词——风险面由收口评审按证据(承诺词/diff 原语/盲维作答)判定。轻量→完整转道是用户决策:须征得用户同意并带 `--upgrade-thick`(同意门留痕,无 flag 拒跑);轻量变更实测失败自动升厚。
|
|
15
|
+
|
|
16
|
+
## 轻量变更(默认快道)
|
|
17
|
+
`sillyspec flow start --change <名> --input "<需求>"` → 改代码、写测试(治理工件 CLI 机器起草,不用你写)→ `sillyspec flow done --change <名>`。
|
|
18
|
+
|
|
19
|
+
`--input` 过门格式(清晰度门按此提取,缺「成功标准」条目会被拦下):
|
|
20
|
+
先写动机/背景;随后独立一行只写「成功标准:」;再每行一条「- <可验证标准>」。
|
|
21
|
+
|
|
22
|
+
start 之后:按提示填 design.md 四节 AGENT 槽(做法/接口契约/边界并发四问/风险,每节至少一行,写「不适用:<理由>」也算答——空槽 flow done 拒收);干活时逐条勾 tasks.md 的 task-NN(勾选是收口哨兵证据面);收口前交付代码先用显式 pathspec 提交(patch 冻结面=baseline..HEAD 提交面,未提交的代码不进审计件)。
|
|
23
|
+
|
|
24
|
+
## 常用命令速查
|
|
25
|
+
- 看进度:`sillyspec progress show`;轻量变更恢复:重跑 `sillyspec flow start --change <名>`(输出恢复简报)或 `sillyspec flow status --change <名>`;完整流程恢复:`run <stage> --change <名>` 续跑。不直接 commit 半成品
|
|
26
|
+
- 跨会话交接:`sillyspec handoff --change <名>`(生成交接块,新会话零背景可续)
|
|
27
|
+
- 变更列表 / 状态:`sillyspec status`
|
|
28
|
+
- 自检修复:`sillyspec doctor`
|
|
29
|
+
- 知识库:`sillyspec knowledge search "<关键词>"`(命中知识 CLI 会自动注入 prompt,勿自行重复检索)
|
|
5
30
|
|
|
6
31
|
## 核心规则
|
|
7
32
|
1. **禁止绕过本文件规则和 SillySpec 流程**。所有变更走 sillyspec 流程,不裸改裸提交。
|
|
8
|
-
2. **改代码前必须先说明依据**——依据的文档路径(design.md / 模块文档 / file-lifecycle.md
|
|
9
|
-
3.
|
|
10
|
-
4.
|
|
11
|
-
5.
|
|
12
|
-
6.
|
|
13
|
-
7.
|
|
14
|
-
8.
|
|
15
|
-
9.
|
|
16
|
-
10.
|
|
17
|
-
11.
|
|
18
|
-
12.
|
|
19
|
-
13. **代码必须兼容 Windows / Linux / macOS**(路径 / 换行 / 并发都要顾)。
|
|
20
|
-
14. **任务记录隔离**:永不重置 / reset / 清零已存在的 change;多个活跃 change 各自 `--change <名>` 隔离不重叠;quick 同一 QUICKLOG 按 ql-ID 条目追加,不冲突。
|
|
21
|
-
15. **quicklog 结构化落盘**:末步 `--done` 用四参数 `--req/--cause/--solution/--result`(CLI 合成结构化 output 并自动提取标题/四段分行);文件括注用 `--file-notes "path::注 || path2"`。骨架由 CLI 接管,`--done` 后按需核对即可,勿手拼模板。
|
|
22
|
-
16. **代码可能随时在修改**(多 agent 并行),Edit 前重跑 + 查最新态;破坏性 git op 前先备份。
|
|
23
|
-
17. **不奉承用户**,禁止"你说得对"类话术,直接给结论、依据、方案。
|
|
24
|
-
18. **禁止目录级 git add / git add -A**(多会话共享仓):.sillyspec/ 等目录里有并行会话的进行中文件与已提交文档,目录级暂存会夹带他者改动甚至误删已提交文件(2026-09-10 实证)。提交一律用显式 pathspec(git add -- 文件1 文件2),提交前 git status 核对暂存面只含本会话文件。
|
|
33
|
+
2. **改代码前必须先说明依据**——依据的文档路径(design.md / 模块文档 / file-lifecycle.md(如有))或现有代码依据,无依据不改。
|
|
34
|
+
3. **执行顺序**:文档 → 读代码 → 写测试 → 写实现 → 跑测试 → 验收 → 更新文档。
|
|
35
|
+
4. **实证核验再收口**:触及 `src`/`test` 的改动,CLI 会在收口时亲自实测(实测失败阻断收口;纯 doc/配置自动跳过)。测试面按变更动态推断(本变更测试 ∪ FR 关联回归 ∪ import 依赖,runner 自项目结构推断——无需配置);lint 走 `.sillyspec/local.yaml` 的 `commands.lint`。以落盘文件与测试结果为准,不信口头"已完成"。
|
|
36
|
+
5. **非测试逻辑本身有误时,禁止改测试来"通过"**——修逻辑,不修测试。
|
|
37
|
+
6. **git hook 拦截提交时禁止跳过**(如 `.husky/pre-push`),修复问题后再提交。
|
|
38
|
+
7. **代码必须兼容 Windows / Linux / macOS**(路径 / 换行 / 并发都要顾)。
|
|
39
|
+
8. **任务记录隔离**:永不重置 / reset / 清零已存在的 change;多个活跃 change 各自 `--change <名>` 隔离不重叠。
|
|
40
|
+
9. **代码可能随时在修改**(多 agent 并行),Edit 前重跑 + 查最新态;破坏性 git op 前先备份。
|
|
41
|
+
10. **会话启动先立身份**:`export SILLYSPEC_SESSION_ID=<唯一标识>`(如 agent 名+任务名)——多会话并行时 change 所有权判定(apply / cleanup / 归档对他会话活跃变更的接管拒绝)依赖此标识;缺省降级为 `anon@<主机名>` 机器级标识(只拦他机,同机并行不设防)。部分 harness 的 shell 状态不持久(env 随命令丢失),此时接管类命令每条显式带 flag:`sillyspec worktree apply <change> --session <唯一标识>`(`--session` 优先级高于 env)。
|
|
42
|
+
11. **禁止目录级 git add / git add -A**(多会话共享仓):目录级暂存会夹带并行会话的进行中文件甚至误删已提交文件。提交一律用显式 pathspec,且 **add 与 commit 用同一份清单**:`git commit -m "..." -- 文件1 文件2`——带 pathspec 的 commit 只提交这些路径,他侧已 staged 的条目既不被带走、也原样留给对方;裸 `git commit` 提交的是整个共享暂存区,他会话 add 过的文件会被一并扫入。核对暂存面固定用 `git diff --cached --name-only` 全量读取,禁用过滤式核对(过滤正是致盲原因);核对与提交分开两条命令执行,不得链在同一命令里(链行没有拦截点)。安装了 commit-guard hook 的仓会对超声明面的 staged 文件警告(只警告不阻断)。
|
|
43
|
+
12. **不奉承用户**,禁止"你说得对"类话术,直接给结论、依据、方案。
|