sillyspec 3.29.3 → 3.29.5
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/asset-audit-2026-09-20/README.md +42 -0
- package/asset-audit-2026-09-20/asset-audit.cjs +253 -0
- package/asset-audit-2026-09-20/extract-candidates.cjs +37 -0
- package/asset-audit-2026-09-20/platform-archive.csv +294 -0
- package/asset-audit-2026-09-20/platform-fr.csv +12 -0
- package/asset-audit-2026-09-20/platform-gotchas.csv +6 -0
- package/asset-audit-2026-09-20/platform-knownissues.csv +23 -0
- package/asset-audit-2026-09-20/platform-modules.csv +222 -0
- package/asset-audit-2026-09-20/platform-quicklog.csv +1076 -0
- package/asset-audit-2026-09-20/platform-troubleshooting-candidates.csv +88 -0
- package/asset-audit-2026-09-20/platform-troubleshooting.csv +1 -0
- package/asset-audit-2026-09-20/sillyspec-archive.csv +108 -0
- package/asset-audit-2026-09-20/sillyspec-fr.csv +51 -0
- package/asset-audit-2026-09-20/sillyspec-gotchas.csv +4 -0
- package/asset-audit-2026-09-20/sillyspec-knownissues.csv +34 -0
- package/asset-audit-2026-09-20/sillyspec-modules.csv +30 -0
- package/asset-audit-2026-09-20/sillyspec-quicklog.csv +460 -0
- package/asset-audit-2026-09-20/sillyspec-troubleshooting.csv +69 -0
- package/asset-audit-2026-09-20/summary.json +241 -0
- package/assets/command-cards/run-archive.md +26 -0
- package/assets/command-cards/run-brainstorm.md +33 -0
- package/assets/command-cards/run-execute.md +29 -0
- package/assets/command-cards/run-plan.md +27 -0
- package/assets/command-cards/run-quick.md +34 -0
- package/assets/command-cards/run-verify.md +29 -0
- package/assets/command-cards/status.md +27 -0
- package/package.json +1 -1
- package/round4/account.mjs +85 -0
- package/round4/brief-autocompact.md +34 -0
- package/round4/brief-session-replay.md +50 -0
- package/round4/deep-read-synthesis.md +38 -0
- package/round4/extract-full.mjs +38 -0
- package/round4/extract-timeline.mjs +43 -0
- package/round4/find-sessions.mjs +13 -0
- package/round4/full-r4-l.md +4707 -0
- package/round4/full-r4-o-l.md +4300 -0
- package/round4/full-r4-os.md +1681 -0
- package/round4/full-r4-sf.md +4255 -0
- package/round4/full-r4-sq.md +1734 -0
- package/round4/gate-audit-relay-receipt.md +30 -0
- package/round4/gate-value-audit.md +52 -0
- package/round4/prereg.md +67 -0
- package/round4/prompt-R4-L.md +63 -0
- package/round4/prompt-R4-O-L.md +59 -0
- package/round4/prompt-R4-O-S.md +56 -0
- package/round4/prompt-R4-S-F.md +60 -0
- package/round4/prompt-R4-S-Q.md +66 -0
- package/round4/r4-os-timeline.txt +297 -0
- package/round4/r4-sf-timeline.txt +738 -0
- package/round4/r4-sq-timeline.txt +322 -0
- package/round4/r4l-timeline.txt +793 -0
- package/round4/r4ol-timeline.txt +729 -0
- package/round4/r4sf-timeline.txt +852 -0
- package/round4/result-S.md +171 -0
- package/round4/result-final.md +46 -0
- package/round4/result-quality.md +63 -0
- package/src/command-cards.js +73 -0
- package/src/contract-matrix.js +26 -5
- package/src/cross-repo-reconcile.js +232 -46
- package/src/diagnostic-codes.js +2 -1
- package/src/fr-index.js +58 -2
- package/src/index.js +35 -8
- package/src/init.js +25 -8
- package/src/machine-interface.js +88 -3
- package/src/module-impact.js +51 -22
- package/src/run/bg-sync.js +231 -0
- package/src/run/command.js +4 -0
- package/src/run/complete-handlers.js +244 -6
- package/src/run/complete.js +6 -3
- package/src/run/gate-snapshot.js +10 -4
- package/src/run/gates.js +124 -42
- package/src/run/prompt.js +1 -1
- package/src/run/quick-audit.js +28 -2
- package/src/run/shared.js +35 -1
- package/src/scope-audit.js +303 -20
- package/src/stages/execute.js +2 -2
- package/src/stages/plan-postcheck.js +33 -19
- package/src/stages/quick.js +2 -0
- package/src/sync.js +12 -0
- package/src/taskcard-frontmatter.js +57 -0
- package/src/verify-postcheck.js +68 -6
- package/src/verify-probes.js +45 -23
package/src/init.js
CHANGED
|
@@ -5,6 +5,7 @@ import { checkbox, confirm, input } from '@inquirer/prompts';
|
|
|
5
5
|
import { ProgressManager } from './progress.js';
|
|
6
6
|
import chalk from 'chalk';
|
|
7
7
|
import { getVersion } from './version.js';
|
|
8
|
+
import { injectCommandCards } from './command-cards.js';
|
|
8
9
|
import { gitQuiet } from './git-helper.js';
|
|
9
10
|
import { renderExample } from './config-schema.js';
|
|
10
11
|
// 向后兼容:getVersion 已抽到轻量 version.js(避免 index.js 为 --version 静态加载 init.js 的 inquirer 税),
|
|
@@ -76,10 +77,11 @@ function copyDirSync(src, dst) {
|
|
|
76
77
|
|
|
77
78
|
|
|
78
79
|
|
|
79
|
-
const VALID_TOOLS = ['claude', 'cursor', 'openclaw', 'codex', 'gemini', 'opencode'];
|
|
80
|
+
const VALID_TOOLS = ['claude', 'zcode', 'cursor', 'openclaw', 'codex', 'gemini', 'opencode'];
|
|
80
81
|
|
|
81
82
|
const TOOL_LABELS = {
|
|
82
83
|
claude: 'Claude Code',
|
|
84
|
+
zcode: 'ZCode',
|
|
83
85
|
cursor: 'Cursor',
|
|
84
86
|
openclaw: 'OpenClaw',
|
|
85
87
|
codex: 'OpenAI Codex (通过 AGENTS.md)',
|
|
@@ -288,14 +290,17 @@ function isTTY() {
|
|
|
288
290
|
|
|
289
291
|
// ── 核心安装逻辑 ──
|
|
290
292
|
|
|
291
|
-
function doInstall(projectDir, tools, subprojects = [], specDir = null, options = {}) {
|
|
293
|
+
async function doInstall(projectDir, tools, subprojects = [], specDir = null, options = {}) {
|
|
292
294
|
// specDir: 规范目录(默认 projectDir/.sillyspec)
|
|
293
295
|
// projectDir: 源码项目根目录(用于工具检测、指令注入、.gitignore)
|
|
294
296
|
// options.noSkills: 跳过 skills 复制段(--no-skills;指令注入不受影响)
|
|
295
297
|
// options.platformMode: 平台模式(cmdInit 收到 platformOpts 非空)——项目内 .sillyspec/
|
|
296
298
|
// 通常只有 local.yaml(平台 init lease 写、含用户手调 mcp 段),任何清理都会丢失,整体跳过
|
|
297
|
-
const { noSkills = false, platformMode = false } = options;
|
|
299
|
+
const { noSkills = false, platformMode = false, forceCards = false } = options;
|
|
298
300
|
const spec = specDir || join(projectDir, '.sillyspec');
|
|
301
|
+
// 命令卡工具面(2026-09-21-flow-command-cards):仅 zcode/claude 有落点(D-003);
|
|
302
|
+
// noSkills/platformMode 与 skills 同门跳过(Grill P2-4)
|
|
303
|
+
const cardTools = noSkills || platformMode ? [] : tools.filter((t) => t === 'zcode' || t === 'claude');
|
|
299
304
|
|
|
300
305
|
// 外部 specDir 时清理旧版本残留的 cwd/.sillyspec/(防止源码污染)。
|
|
301
306
|
// ⚠️ 必须保护真实资产:若本地 .sillyspec 含 changes/(非空)、projects/(非空)
|
|
@@ -443,11 +448,23 @@ function doInstall(projectDir, tools, subprojects = [], specDir = null, options
|
|
|
443
448
|
}
|
|
444
449
|
}
|
|
445
450
|
|
|
446
|
-
// 注入 AGENTS.md 完整指引(claude/codex 共用:版本感知幂等三态四分支 + 旧 ## SillySpec
|
|
447
|
-
|
|
451
|
+
// 注入 AGENTS.md 完整指引(claude/codex/zcode 共用:版本感知幂等三态四分支 + 旧 ## SillySpec 段迁移;
|
|
452
|
+
// zcode 2026-09-21-flow-command-cards FR-05 入面——AGENTS.md 是跨工具通用标准内容源)
|
|
453
|
+
if (tools.includes('claude') || tools.includes('codex') || tools.includes('zcode')) {
|
|
448
454
|
injectAgentsInstructions(projectDir);
|
|
449
455
|
}
|
|
450
456
|
|
|
457
|
+
// 流程命令卡注入(2026-09-21-flow-command-cards:zcode→.zcode/commands/sillyspec/、claude→.claude/commands/sillyspec/;
|
|
458
|
+
// 尾部锚行三态幂等。与 skills 复制同门:noSkills/platformMode 跳过(工具目录零污染语义,Grill P2-4);
|
|
459
|
+
// 挂点在 noSkills return 之前——卡与 AGENTS.md 同批,早于 skills 复制不影响)
|
|
460
|
+
if (cardTools.length > 0) {
|
|
461
|
+
const cards = await injectCommandCards(projectDir, { tools: cardTools, force: forceCards, version: getVersion() });
|
|
462
|
+
for (const w of cards.warnings) console.warn(` ⚠️ [命令卡] ${w}`);
|
|
463
|
+
if (cards.written.length + cards.updated.length > 0) {
|
|
464
|
+
console.log(chalk.green(` ✓ 流程命令卡已${cards.written.length ? '生成' : '更新'} ${cards.written.length + cards.updated.length} 张 → ${cardTools.map((t) => (t === 'zcode' ? '.zcode' : '.claude') + '/commands/sillyspec/').join(' + ')}`));
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
|
|
451
468
|
// 注入 CLAUDE.md 指针(claude 专属:@AGENTS.md 导入薄文件,须在 AGENTS.md 之后)
|
|
452
469
|
if (tools.includes('claude')) {
|
|
453
470
|
injectClaudePointer(projectDir);
|
|
@@ -546,7 +563,7 @@ function showSummary(version, tools, specDir) {
|
|
|
546
563
|
// ── 主命令 ──
|
|
547
564
|
|
|
548
565
|
export async function cmdInit(projectDir, options = {}) {
|
|
549
|
-
const { tool, tools: toolsOpt, interactive, specDir, noSkills = false, platformOpts = null } = options;
|
|
566
|
+
const { tool, tools: toolsOpt, interactive, specDir, noSkills = false, forceCards = false, platformOpts = null } = options;
|
|
550
567
|
const version = getVersion();
|
|
551
568
|
const resolvedSpecDir = specDir ? resolve(specDir) : null;
|
|
552
569
|
|
|
@@ -654,7 +671,7 @@ export async function cmdInit(projectDir, options = {}) {
|
|
|
654
671
|
}
|
|
655
672
|
|
|
656
673
|
console.log('');
|
|
657
|
-
await doInstall(projectDir, selectedTools, subprojects, resolvedSpecDir, { noSkills, platformMode: platformOpts != null });
|
|
674
|
+
await doInstall(projectDir, selectedTools, subprojects, resolvedSpecDir, { noSkills, forceCards, platformMode: platformOpts != null });
|
|
658
675
|
writeInitPlatformPointer(projectDir, resolvedSpecDir, platformOpts);
|
|
659
676
|
showSummary(version, selectedTools, resolvedSpecDir);
|
|
660
677
|
return;
|
|
@@ -682,7 +699,7 @@ export async function cmdInit(projectDir, options = {}) {
|
|
|
682
699
|
tools = detectTools(projectDir);
|
|
683
700
|
}
|
|
684
701
|
|
|
685
|
-
await doInstall(projectDir, tools, [], resolvedSpecDir, { noSkills, platformMode: platformOpts != null });
|
|
702
|
+
await doInstall(projectDir, tools, [], resolvedSpecDir, { noSkills, forceCards, platformMode: platformOpts != null });
|
|
686
703
|
writeInitPlatformPointer(projectDir, resolvedSpecDir, platformOpts);
|
|
687
704
|
|
|
688
705
|
console.log('');
|
package/src/machine-interface.js
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
* 仅依赖 Node 18+ 原生 API,零新增外部依赖。
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
|
-
import { existsSync, readFileSync } from 'fs';
|
|
18
|
+
import { existsSync, readFileSync, mkdirSync, writeFileSync } from 'fs';
|
|
19
19
|
import { join } from 'path';
|
|
20
20
|
import { ProgressManager, resolveSpecDir } from './progress.js';
|
|
21
21
|
import { resolveRuntimeRoot } from './run/shared.js';
|
|
@@ -25,7 +25,6 @@ import { runVerifyTestCheck } from './verify-postcheck.js';
|
|
|
25
25
|
import { checkCode } from './diagnostic-codes.js';
|
|
26
26
|
|
|
27
27
|
// ============ 退出码常量(D-004@v1)============
|
|
28
|
-
|
|
29
28
|
export const EXIT_OK = 0; // 核验通过(可含 warnings)
|
|
30
29
|
export const EXIT_BLOCKED = 1; // 事实性阻断(JSON 含 errors)
|
|
31
30
|
export const EXIT_UNKNOWN = 2; // 无法核验(用法错/变更不存在/环境错/内部异常)
|
|
@@ -295,8 +294,9 @@ export async function runGate(stage, changeName, { cwd, specBase, runtimeRoot, s
|
|
|
295
294
|
vtWarnings.push(`⚠️ verify-test SKIPPED — gate 未核验测试(${vt.reason || 'local.yaml 未配置 commands.test 或显式无测试'})。本次 gate 结论不含测试客观核验,driver 不应据 exit 0 判定测试通过;integration-critical 变更应在 verify 阶段降级 FAIL`);
|
|
296
295
|
}
|
|
297
296
|
// known_failures 豁免披露:本次 PASS 含豁免,driver 应提示人工复核清单是否过宽
|
|
297
|
+
// (quick-E,2026-09-20 报告问题 E:提示原不带清单位置,复核要先翻一遍才找到——带路径)
|
|
298
298
|
if (vt.exemptedCount > 0) {
|
|
299
|
-
vtWarnings.push(`⚠️ verify-test PASS 含 ${vt.exemptedCount} 个 known_failures 豁免(${vt.reason || ''});driver
|
|
299
|
+
vtWarnings.push(`⚠️ verify-test PASS 含 ${vt.exemptedCount} 个 known_failures 豁免(${vt.reason || ''});driver 应提示人工复核豁免清单是否过宽(避免误豁免本变更引入的真实失败)——清单位置:${join(specRoot, 'local.yaml')} 的 known_failures 键`);
|
|
300
300
|
}
|
|
301
301
|
// 全量 fallback 明示:跑了全量 commands.test 但非变更范围子集,失败可能含未变更
|
|
302
302
|
// 模块的预存错误——driver 不应据 exit 0 判定本次变更范围已客观测试(见 3.24 verify 坑1)。
|
|
@@ -318,6 +318,64 @@ export async function runGate(stage, changeName, { cwd, specBase, runtimeRoot, s
|
|
|
318
318
|
fallbackReason: vt.fallbackReason ?? null,
|
|
319
319
|
},
|
|
320
320
|
});
|
|
321
|
+
|
|
322
|
+
// ── d2. verify-lint(parity 补齐,2026-09-21 R4-S-F 实证)──
|
|
323
|
+
// gate verify 原先只查 test 不查 lint,而 verify --done 的 lint 隔离快照门会拦——
|
|
324
|
+
// gate PASS / --done FAIL 两套口径并存,误导 agent 诊断 16 分钟(对撞会话 db 实证)。
|
|
325
|
+
// 本检查与 --done 同引擎(runVerifyLintCheck + triageLintOwnership 归属降档);口径差异
|
|
326
|
+
// 永久明示:gate 在工作树直跑 lint,--done 在隔离快照(HEAD+归属文件 overlay)跑——
|
|
327
|
+
// 不一致时以 --done 为准。lint 未配置/不可跑 → informational,不阻断只读查询。
|
|
328
|
+
try {
|
|
329
|
+
const { runVerifyLintCheck, triageLintOwnership, resolveVerifyChangedFiles } = await import('./verify-postcheck.js');
|
|
330
|
+
const lc = runVerifyLintCheck({ cwd, specBase: specRoot });
|
|
331
|
+
if (lc.status === 'skipped') {
|
|
332
|
+
checks.push({
|
|
333
|
+
id: 'verify-lint',
|
|
334
|
+
code: checkCode('verify-lint'),
|
|
335
|
+
ok: true,
|
|
336
|
+
informational: true,
|
|
337
|
+
warnings: [`verify-lint SKIPPED — ${lc.reason || 'local.yaml 未配置 commands.lint'};本次 gate 结论不含 lint 客观核验`],
|
|
338
|
+
data: { status: lc.status },
|
|
339
|
+
});
|
|
340
|
+
} else {
|
|
341
|
+
const lcWarnings = [
|
|
342
|
+
'口径说明:gate verify 的 lint 在工作树直跑;verify --done 的 lint 在隔离快照(HEAD+归属文件 overlay)跑——两口径不一致时以 --done 为准。',
|
|
343
|
+
];
|
|
344
|
+
let ownership = null;
|
|
345
|
+
let lintOk = lc.status !== 'failed';
|
|
346
|
+
if (lc.status === 'failed') {
|
|
347
|
+
const changed = resolveVerifyChangedFiles(cwd, changeName, null, { specBase: specRoot }) || [];
|
|
348
|
+
ownership = triageLintOwnership({ failureFiles: lc.failureFiles || [], changeFiles: changed });
|
|
349
|
+
if (ownership.verdict === 'pre-existing') {
|
|
350
|
+
lintOk = true; // 存量债不拦只读查询(与 --done 的 advisory 降档同口径)
|
|
351
|
+
lcWarnings.push('lint 失败文件与本变更零交集(HEAD 存量债)→ 本 check 降档放行;--done 同口径 advisory,建议顺手清偿解锁全仓 lint 门。');
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
checks.push({
|
|
355
|
+
id: 'verify-lint',
|
|
356
|
+
code: checkCode('verify-lint'),
|
|
357
|
+
ok: lintOk,
|
|
358
|
+
errors: !lintOk ? [`lint 失败(工作树口径): ${lc.reason || ''}`] : [],
|
|
359
|
+
warnings: lcWarnings,
|
|
360
|
+
data: {
|
|
361
|
+
status: lc.status,
|
|
362
|
+
exitCode: lc.exitCode,
|
|
363
|
+
durationMs: lc.durationMs,
|
|
364
|
+
ownership: ownership ? ownership.verdict : null,
|
|
365
|
+
overlap: ownership ? ownership.overlap : [],
|
|
366
|
+
},
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
} catch (e) {
|
|
370
|
+
checks.push({
|
|
371
|
+
id: 'verify-lint',
|
|
372
|
+
code: checkCode('verify-lint'),
|
|
373
|
+
ok: true,
|
|
374
|
+
informational: true,
|
|
375
|
+
warnings: [`verify-lint 检查装配失败(fail-open): ${e && e.message ? e.message : e}`],
|
|
376
|
+
data: { status: 'error' },
|
|
377
|
+
});
|
|
378
|
+
}
|
|
321
379
|
}
|
|
322
380
|
|
|
323
381
|
// ── 综合结论:所有非 informational check 均 ok ──
|
|
@@ -667,3 +725,30 @@ export function runStatusOverview({ cwd, specBase } = {}) {
|
|
|
667
725
|
return { envelope, exitCode: EXIT_UNKNOWN };
|
|
668
726
|
}
|
|
669
727
|
}
|
|
728
|
+
|
|
729
|
+
// ============ gate 失败明细稳定路径落盘(quick-D,2026-09-20 报告问题 D)============
|
|
730
|
+
|
|
731
|
+
/**
|
|
732
|
+
* `sillyspec gate <stage> --change <name>` 的失败信封落盘(稳定路径,每次覆盖)。
|
|
733
|
+
* 背景:gate 失败明细只在 stdout——宿主把长命令转后台截断输出后 ❌ 段落实际取不回
|
|
734
|
+
* (2026-09-20 实证:agent 靠推断绕 3 轮)。runGate 本体保持只读契约(D-002@v1,不写
|
|
735
|
+
* sillyspec.db),落盘由本函数承担(CLI 层调用,fail-soft)——审计附件非状态。
|
|
736
|
+
* @param {{ specBase: string, stage: string, changeName: string, envelope: object }} opts
|
|
737
|
+
* @returns {string|null} 成功返回落盘路径;失败返回 null
|
|
738
|
+
*/
|
|
739
|
+
export function writeGateResultArtifact({ specBase, stage, changeName, envelope }) {
|
|
740
|
+
try {
|
|
741
|
+
const runsDir = join(specBase, '.runtime', 'verify-runs')
|
|
742
|
+
const resultPath = join(runsDir, `gate-${stage}-${changeName}.json`)
|
|
743
|
+
mkdirSync(runsDir, { recursive: true })
|
|
744
|
+
writeFileSync(resultPath, JSON.stringify({
|
|
745
|
+
written_at: new Date().toISOString(),
|
|
746
|
+
stage,
|
|
747
|
+
change: changeName,
|
|
748
|
+
envelope,
|
|
749
|
+
}, null, 2) + '\n')
|
|
750
|
+
return resultPath
|
|
751
|
+
} catch {
|
|
752
|
+
return null // fail-soft:落盘是审计附件,失败只缺附件不误门
|
|
753
|
+
}
|
|
754
|
+
}
|
package/src/module-impact.js
CHANGED
|
@@ -48,7 +48,15 @@ export function parseModuleMapPaths(yamlText) {
|
|
|
48
48
|
}
|
|
49
49
|
if (inPaths && currentModule) {
|
|
50
50
|
const item = line.match(/^ - (.+)$/)
|
|
51
|
-
if (item)
|
|
51
|
+
if (item) {
|
|
52
|
+
// 引号剥离(坑 map-quoted-path-deadmatch,2026-09-21 R4-L 实证):docs/<p>/modules/
|
|
53
|
+
// _module-map.yaml 存在带引号条目("frontend/src/app/page.tsx")——只 trim 不剥引号时
|
|
54
|
+
// classifyFile 的 === / startsWith 恒失配,引号条目全是死路径(frontend map 790 条
|
|
55
|
+
// 几乎全带引号=整图失效)。仅首尾成对引号才剥(路径内部引号不动)。
|
|
56
|
+
const raw = item[1].trim()
|
|
57
|
+
const unquoted = raw.replace(/^(['"])([\s\S]*)\1$/, '$2')
|
|
58
|
+
map.get(currentModule).push(unquoted.replace(/\\/g, '/'))
|
|
59
|
+
}
|
|
52
60
|
}
|
|
53
61
|
}
|
|
54
62
|
return map
|
|
@@ -89,18 +97,33 @@ export function syncModuleDocSidecars({ cwd, changeName, note, specDir = null })
|
|
|
89
97
|
// 复用 generateModuleImpactSkeleton 的 map 发现 + 归类(diff 来源)
|
|
90
98
|
const skel = generateModuleImpactSkeleton({ cwd, changeName, specDir })
|
|
91
99
|
if (!skel) return { synced: [], unmatched: [], skipped: [], reason: '无 module-map 或无 diff 可归类' }
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
100
|
+
// 结构化归类直用(classified 含 project 归属;multimap-first-found-mismatch 同坑修复面)——
|
|
101
|
+
// 不再反解 markdown(正则脆且丢 project 维度)。classified 缺席时兜底反解 + 首个 modules 目录。
|
|
102
|
+
let mods = []
|
|
103
|
+
if (Array.isArray(skel.classified) && skel.classified.length >= 0) {
|
|
104
|
+
const seen = new Set()
|
|
105
|
+
for (const r of skel.classified) {
|
|
106
|
+
const key = r.project + '/' + r.module
|
|
107
|
+
if (seen.has(key)) continue
|
|
108
|
+
seen.add(key)
|
|
109
|
+
mods.push({ project: r.project, module: r.module, modulesDir: join(specBase, 'docs', r.project, 'modules') })
|
|
110
|
+
}
|
|
111
|
+
} else {
|
|
112
|
+
const modulesDir = findModulesDir(specBase)
|
|
113
|
+
if (!modulesDir) return { synced: [], unmatched: [], skipped: [], reason: 'modules 目录不存在' }
|
|
114
|
+
mods = [...new Set([...skel.markdown.matchAll(/^\| (\S+) \| `([^`]+)`/gm)].map(m => m[1]))]
|
|
115
|
+
.map(mod => ({ project: null, module: mod, modulesDir }))
|
|
116
|
+
}
|
|
117
|
+
const unmatched = Array.isArray(skel.unmatchedFiles)
|
|
118
|
+
? skel.unmatchedFiles
|
|
119
|
+
: [...skel.markdown.matchAll(/^- `([^`]+)` /gm)].map(m => m[1])
|
|
97
120
|
const synced = []
|
|
98
121
|
const skipped = []
|
|
99
|
-
for (const mod of
|
|
122
|
+
for (const { project: proj, module: mod, modulesDir } of mods) {
|
|
100
123
|
const sidecarPath = join(modulesDir, `${mod}.changelog.md`)
|
|
101
124
|
let sidecar = ''
|
|
102
125
|
try { sidecar = readFileSync(sidecarPath, 'utf8') } catch { sidecar = '' }
|
|
103
|
-
if (sidecar.includes(`- ${changeName} |`)) { skipped.push(mod); continue }
|
|
126
|
+
if (sidecar.includes(`- ${changeName} |`)) { skipped.push(proj ? `${proj}/${mod}` : mod); continue }
|
|
104
127
|
const NL = String.fromCharCode(10)
|
|
105
128
|
const line = `- ${changeName} | ${note || '(见该变更 quicklog/归档)'}`
|
|
106
129
|
const base = sidecar
|
|
@@ -114,7 +137,7 @@ export function syncModuleDocSidecars({ cwd, changeName, note, specDir = null })
|
|
|
114
137
|
const stamped = card.replace(/^updated_at:.*$/m, `updated_at: ${new Date().toISOString().slice(0, 19) + '+08:00'}`)
|
|
115
138
|
if (stamped !== card) writeFileSync(cardPath, stamped)
|
|
116
139
|
} catch { /* 卡不存在——sidecar 照写,卡内容留给 agent */ }
|
|
117
|
-
synced.push(mod)
|
|
140
|
+
synced.push(proj ? `${proj}/${mod}` : mod)
|
|
118
141
|
}
|
|
119
142
|
return { synced, unmatched, skipped }
|
|
120
143
|
}
|
|
@@ -133,22 +156,22 @@ function findModulesDir(specBase) {
|
|
|
133
156
|
export function generateModuleImpactSkeleton({ cwd, changeName, specDir = null, sourceFiles = null, origin = null }) {
|
|
134
157
|
const specBase = resolveSpecDir(cwd, { specDir })
|
|
135
158
|
|
|
136
|
-
//
|
|
137
|
-
|
|
159
|
+
// 多项目 map 联合归类(坑 multimap-first-found-mismatch,2026-09-21 R4-L 实证):docs/ 下
|
|
160
|
+
// 各项目可各带 _module-map.yaml(对撞仓 5 个),原「取首个命中」让 daemon/frontend 文件对
|
|
161
|
+
// 按目录序排前的 backend map 全失配——24 文件全未匹配、sidecar 也写错项目目录。联合口径:
|
|
162
|
+
// 逐项目 classify、目录序首个命中 wins(单项目时联合退化为一图,行为不变)。
|
|
163
|
+
const moduleMaps = []
|
|
138
164
|
try {
|
|
139
165
|
for (const p of readdirSync(join(specBase, 'docs'), { withFileTypes: true })) {
|
|
140
166
|
if (!p.isDirectory()) continue
|
|
141
167
|
const candidate = join(specBase, 'docs', p.name, 'modules', '_module-map.yaml')
|
|
142
168
|
if (existsSync(candidate)) {
|
|
143
|
-
|
|
144
|
-
|
|
169
|
+
const pm = parseModuleMapPaths(readFileSync(candidate, 'utf8'))
|
|
170
|
+
if (pm.size > 0) moduleMaps.push({ project: p.name, modulePaths: pm })
|
|
145
171
|
}
|
|
146
172
|
}
|
|
147
173
|
} catch { /* 无 docs/ */ }
|
|
148
|
-
if (
|
|
149
|
-
|
|
150
|
-
const modulePaths = parseModuleMapPaths(readFileSync(moduleMapPath, 'utf8'))
|
|
151
|
-
if (modulePaths.size === 0) return null
|
|
174
|
+
if (moduleMaps.length === 0) return null
|
|
152
175
|
|
|
153
176
|
let sourceFileList
|
|
154
177
|
if (sourceFiles) {
|
|
@@ -169,12 +192,18 @@ export function generateModuleImpactSkeleton({ cwd, changeName, specDir = null,
|
|
|
169
192
|
if (sourceFileList.length === 0) return null
|
|
170
193
|
|
|
171
194
|
const byModule = new Map()
|
|
195
|
+
const classified = [] // {project, module, file}——syncModuleDocSidecars 按 project 落 sidecar 用
|
|
172
196
|
const unmatched = []
|
|
173
197
|
for (const f of sourceFileList) {
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
198
|
+
let hit = null
|
|
199
|
+
for (const m of moduleMaps) {
|
|
200
|
+
const mod = classifyFile(f, m.modulePaths)
|
|
201
|
+
if (mod) { hit = { project: m.project, moduleId: mod }; break }
|
|
202
|
+
}
|
|
203
|
+
if (hit) {
|
|
204
|
+
if (!byModule.has(hit.moduleId)) byModule.set(hit.moduleId, [])
|
|
205
|
+
byModule.get(hit.moduleId).push(f)
|
|
206
|
+
classified.push({ project: hit.project, module: hit.moduleId, file: f })
|
|
178
207
|
} else {
|
|
179
208
|
unmatched.push(f)
|
|
180
209
|
}
|
|
@@ -234,5 +263,5 @@ export function generateModuleImpactSkeleton({ cwd, changeName, specDir = null,
|
|
|
234
263
|
L.push('规则:execute/verify 完成文档同步后把对应行回填 done;确定不同步的行改 skipped 并在操作列写明原因。')
|
|
235
264
|
L.push('')
|
|
236
265
|
|
|
237
|
-
return { markdown: L.join('\n'), matchedCount: sourceFileList.length - unmatched.length, unmatchedCount: unmatched.length }
|
|
266
|
+
return { markdown: L.join('\n'), matchedCount: sourceFileList.length - unmatched.length, unmatchedCount: unmatched.length, classified, unmatchedFiles: unmatched }
|
|
238
267
|
}
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* spec-sync 后台异步执行(detached 子进程)。
|
|
3
|
+
*
|
|
4
|
+
* 背景(2026-09-20 用户反馈:末步 --done 因 spec-sync 网络同步约 4 分钟不返回):
|
|
5
|
+
* triggerSync 的 17 个调用点全部 fire-and-forget,但 Node 进程退出要等事件循环排空——
|
|
6
|
+
* 在飞 fetch 拖住进程,CLI 输出打完后仍要等网络收尾才返回 shell(平台慢时分钟级体感
|
|
7
|
+
* hang;本机制落地当天的会话启动命令尾部 3 行 [spec-sync] 已同步 逐条等网络,现场即
|
|
8
|
+
* 实证)。8s 总熔断(raceWithAbort)只保证「最多等 8s」,治标;本模块治本:同步整体
|
|
9
|
+
* 挪进 detached 后台子进程,父进程 spawn + unref 后立即返回,命令零网络尾巴。
|
|
10
|
+
*
|
|
11
|
+
* 架构(父侧/子侧同一模块,职责分开):
|
|
12
|
+
* - 父侧 spawnBackgroundSync(shared.js triggerSync 唯一接入点调用):
|
|
13
|
+
* ① 未连接平台预判(sync.js peekPlatformConnected 同源判据)——未连接不 spawn,
|
|
14
|
+
* 本地独立用户零开销零行为变化;② 单飞锁判定:活锁 → rerunQueued 置位合并
|
|
15
|
+
* (跑完当前轮再补一轮,防进程堆积),无锁/死锁 → spawn detached 子进程。
|
|
16
|
+
* - 子侧 runBgSyncFromEnv(node -e bootstrap 动态 import 本模块):写锁 → 循环
|
|
17
|
+
* 〔清 rerunQueued → triggerSync inline 执行一轮 → 期间有新请求则再来一轮〕,
|
|
18
|
+
* 上限 5 轮 / 总预算 ~2.5min → 清锁退出。轮前清位 + 轮后查位:清位后本轮树 hash
|
|
19
|
+
* 晚于清位时刻,天然覆盖此后到达的状态;hash 之后到达的靠置位标记触发下一轮。
|
|
20
|
+
* - 子进程 stdout/stderr 接 <runtimeRoot>/spec-sync-bg.log(append,超 1MB 截尾保
|
|
21
|
+
* 后 512KB):[spec-sync]/[sync] 的 warn/冲突横幅全量留痕,不进主命令输出也不再丢。
|
|
22
|
+
*
|
|
23
|
+
* 与既有机制的交互:
|
|
24
|
+
* - raceWithAbort / SILLYSPEC_SYNC_TIMEOUT_MS 不动——inline 路径原样。后台轮预算 =
|
|
25
|
+
* max(resolveSyncTotalTimeoutMs(), 45s):后台无人等待,放宽到 45s 提高「慢平台真的
|
|
26
|
+
* 同步成功」概率(2026-09-07 平台 >8s 响应间歇熔断的 pain 点在后台模式下自然消解)。
|
|
27
|
+
* - sync-noise.js 噪音闸 / spec-sync-conflict-*.json / 基线快照在子进程内照常工作
|
|
28
|
+
* (同一 .runtime 目录,跨进程 marker 语义不变)。
|
|
29
|
+
* - 冲突可见性不降级:横幅进日志文件 + 冲突文件照落,下一条命令 progress show 照常
|
|
30
|
+
* 红标提示(resolve 三态处置入口不变)。
|
|
31
|
+
* - SILLYSPEC_SYNC_BG=0 逃生阀:triggerSync 回落 inline(测试断言进程内行为/用户自救)。
|
|
32
|
+
*
|
|
33
|
+
* 循环 import 说明:本模块与 shared.js 双向引用(shared 调本模块 spawnBackgroundSync、
|
|
34
|
+
* 本模块两侧调 shared 的 triggerSync/resolveRuntimeRoot/resolveSyncTotalTimeoutMs)。
|
|
35
|
+
* 对 shared 的引用一律走函数内动态 import,避免静态环(ESM 环可运行但初始化序脆弱,
|
|
36
|
+
* 刻意不赌);父进程内 shared.js 已加载,动态 import 走缓存无额外成本。
|
|
37
|
+
*/
|
|
38
|
+
import { spawn } from 'child_process';
|
|
39
|
+
import { closeSync, existsSync, mkdirSync, openSync, readFileSync, renameSync, statSync, unlinkSync, writeFileSync } from 'fs';
|
|
40
|
+
import { join } from 'path';
|
|
41
|
+
|
|
42
|
+
export const BG_LOCK_FILENAME = 'spec-sync-bg.lock';
|
|
43
|
+
export const BG_LOG_FILENAME = 'spec-sync-bg.log';
|
|
44
|
+
|
|
45
|
+
// 后台轮参数:单轮预算下限(与 env SILLYSPEC_SYNC_TIMEOUT_MS 取 max)、轮数与总预算帽
|
|
46
|
+
// (防慢平台下无限续轮);锁时效(pid 活性之外的第二道判死——pid 复用假活兜底,
|
|
47
|
+
// 子进程自身总预算 ~2.5min + 收尾余量远小于该值,超时必是异常残留)。
|
|
48
|
+
const BG_ROUND_FLOOR_MS = 45_000;
|
|
49
|
+
const BG_MAX_ROUNDS = 5;
|
|
50
|
+
const BG_TOTAL_BUDGET_MS = 150_000;
|
|
51
|
+
const LOCK_STALE_MS = 15 * 60 * 1000;
|
|
52
|
+
const LOG_TRUNCATE_THRESHOLD = 1_000_000;
|
|
53
|
+
const LOG_TRUNCATE_KEEP = 512 * 1024;
|
|
54
|
+
|
|
55
|
+
/** pid 活性探测(跨平台):signal 0 探测;EPERM = 进程存在但属他户,保守按活。 */
|
|
56
|
+
function isPidAlive(pid) {
|
|
57
|
+
if (!Number.isInteger(pid) || pid <= 0) return false;
|
|
58
|
+
try {
|
|
59
|
+
process.kill(pid, 0);
|
|
60
|
+
return true;
|
|
61
|
+
} catch (e) {
|
|
62
|
+
return e && e.code === 'EPERM';
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** 读后台单飞锁(缺失/损坏 → null)。 */
|
|
67
|
+
export function readBgSyncLock(runtimeRoot) {
|
|
68
|
+
try {
|
|
69
|
+
const raw = readFileSync(join(runtimeRoot, BG_LOCK_FILENAME), 'utf8');
|
|
70
|
+
const obj = JSON.parse(raw);
|
|
71
|
+
return obj && typeof obj === 'object' ? obj : null;
|
|
72
|
+
} catch {
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function writeBgSyncLock(runtimeRoot, lock) {
|
|
78
|
+
mkdirSync(runtimeRoot, { recursive: true });
|
|
79
|
+
const tmp = join(runtimeRoot, `${BG_LOCK_FILENAME}.${process.pid}.tmp`);
|
|
80
|
+
writeFileSync(tmp, JSON.stringify(lock) + '\n', 'utf8');
|
|
81
|
+
renameSync(tmp, join(runtimeRoot, BG_LOCK_FILENAME));
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function removeBgSyncLock(runtimeRoot) {
|
|
85
|
+
try {
|
|
86
|
+
const p = join(runtimeRoot, BG_LOCK_FILENAME);
|
|
87
|
+
if (existsSync(p)) unlinkSync(p);
|
|
88
|
+
} catch { /* 清锁失败:残留锁按时效/pid 判死后自动接管,不影响正确性 */ }
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* 锁活性判定(pid 活 + 未超时)。纯函数(pid 探测单列 isPidAlive 便于注入测试)。
|
|
93
|
+
* @param {object|null} lock readBgSyncLock 产物
|
|
94
|
+
* @param {number} [now] 注入时钟(测试用)
|
|
95
|
+
*/
|
|
96
|
+
export function isBgSyncLockLive(lock, now = Date.now()) {
|
|
97
|
+
if (!lock || !Number.isInteger(lock.pid) || lock.pid <= 0) return false;
|
|
98
|
+
if (typeof lock.startedAt !== 'number') return false;
|
|
99
|
+
if (now - lock.startedAt > LOCK_STALE_MS) return false;
|
|
100
|
+
return isPidAlive(lock.pid);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** 子进程起步截尾日志(>1MB 保后 512KB,从整行边界起)。失败只影响日志体积。 */
|
|
104
|
+
function truncateLogIfNeeded(runtimeRoot) {
|
|
105
|
+
try {
|
|
106
|
+
const p = join(runtimeRoot, BG_LOG_FILENAME);
|
|
107
|
+
if (statSync(p).size <= LOG_TRUNCATE_THRESHOLD) return;
|
|
108
|
+
const buf = readFileSync(p);
|
|
109
|
+
const keep = buf.subarray(Math.max(0, buf.length - LOG_TRUNCATE_KEEP));
|
|
110
|
+
const nl = keep.indexOf(10);
|
|
111
|
+
writeFileSync(p, nl >= 0 ? keep.subarray(nl + 1) : keep);
|
|
112
|
+
} catch { /* 截断失败:继续 append */ }
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** 子进程 bootstrap:node -e 动态 import 本模块(file URL 精确定位,不依赖 CLI 安装形态)。 */
|
|
116
|
+
function buildChildBootstrap() {
|
|
117
|
+
const selfUrl = new URL('./bg-sync.js', import.meta.url).href;
|
|
118
|
+
return `import(${JSON.stringify(selfUrl)}).then(m=>m.runBgSyncFromEnv()).catch(e=>{console.error('[spec-sync-bg] 后台同步进程异常退出:',(e&&e.message)||e)});`;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* 父侧后台同步入口(triggerSync 调用,不直接被业务代码使用)。
|
|
123
|
+
*
|
|
124
|
+
* @returns {Promise<{status:'spawned'|'coalesced'|'not-connected', logPath?:string}>}
|
|
125
|
+
* spawned=已起后台子进程;coalesced=活锁在跑、本轮状态已并入其下一轮(rerunQueued);
|
|
126
|
+
* not-connected=未连接平台(调用方回落 inline 路径=与旧行为一致的静默 no-op)。
|
|
127
|
+
* spawn 失败抛异常(调用方 catch 降级 inline,绝不因后台化阻断主流程)。
|
|
128
|
+
*/
|
|
129
|
+
export async function spawnBackgroundSync(cwd, changeName, platformOpts = {}, opts = {}) {
|
|
130
|
+
const syncMod = await import('../sync.js');
|
|
131
|
+
if (!syncMod.peekPlatformConnected(cwd)) return { status: 'not-connected' };
|
|
132
|
+
|
|
133
|
+
const { resolveRuntimeRoot } = await import('./shared.js');
|
|
134
|
+
const runtimeRoot = resolveRuntimeRoot(platformOpts, platformOpts?.specRoot || join(cwd, '.sillyspec'));
|
|
135
|
+
|
|
136
|
+
const lock = readBgSyncLock(runtimeRoot);
|
|
137
|
+
if (isBgSyncLockLive(lock)) {
|
|
138
|
+
// 单飞合并:活后台轮在跑,把「还有新状态要同步」并入其下一轮。置位失败(并发写锁)
|
|
139
|
+
// 只损失合并信号——后台轮收尾锁自清,下一条命令重新 spawn,收敛不丢。
|
|
140
|
+
try { writeBgSyncLock(runtimeRoot, { ...lock, rerunQueued: true }); } catch { /* 见上 */ }
|
|
141
|
+
return { status: 'coalesced', logPath: join(runtimeRoot, BG_LOG_FILENAME) };
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// 日志 fd:子进程 stdout/stderr 直接 append 进日志文件(跨平台 fd 继承);开不了退 ignore
|
|
145
|
+
const logPath = join(runtimeRoot, BG_LOG_FILENAME);
|
|
146
|
+
let stdio = ['ignore', 'ignore', 'ignore'];
|
|
147
|
+
let logFd = null;
|
|
148
|
+
try {
|
|
149
|
+
mkdirSync(runtimeRoot, { recursive: true });
|
|
150
|
+
logFd = openSync(logPath, 'a');
|
|
151
|
+
stdio = ['ignore', logFd, logFd];
|
|
152
|
+
} catch {
|
|
153
|
+
logFd = null;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const env = {
|
|
157
|
+
...process.env,
|
|
158
|
+
SILLYSPEC_BG_SYNC_CHANGE: String(changeName),
|
|
159
|
+
SILLYSPEC_BG_SYNC_CWD: String(cwd),
|
|
160
|
+
};
|
|
161
|
+
if (platformOpts?.specRoot) env.SILLYSPEC_BG_SYNC_SPEC_ROOT = String(platformOpts.specRoot);
|
|
162
|
+
if (platformOpts?.runtimeRoot) env.SILLYSPEC_BG_SYNC_RUNTIME_ROOT = String(platformOpts.runtimeRoot);
|
|
163
|
+
|
|
164
|
+
try {
|
|
165
|
+
const child = (opts.spawnImpl || spawn)(process.execPath, ['-e', buildChildBootstrap()], {
|
|
166
|
+
cwd,
|
|
167
|
+
detached: true,
|
|
168
|
+
stdio,
|
|
169
|
+
windowsHide: true,
|
|
170
|
+
env,
|
|
171
|
+
});
|
|
172
|
+
child.unref?.();
|
|
173
|
+
} finally {
|
|
174
|
+
if (logFd !== null) {
|
|
175
|
+
try { closeSync(logFd); } catch { /* 父侧 fd 副本关闭;子进程持有继承副本 */ }
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
return { status: 'spawned', logPath };
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* 子侧入口(bootstrap 经 env 传参调起;env 注入默认值供测试覆盖)。
|
|
183
|
+
*
|
|
184
|
+
* 单飞循环:写锁(本 pid)→ 每轮清 rerunQueued 后 inline 跑一轮 triggerSync →
|
|
185
|
+
* 轮末查位,被置位且预算未尽则续轮(迟到状态收敛),否则清锁退出。
|
|
186
|
+
* 锁 pid 非本进程(双 spawn 竞态窗口被后写者接管)→ 让位退出防双跑。
|
|
187
|
+
*/
|
|
188
|
+
export async function runBgSyncFromEnv(env = process.env) {
|
|
189
|
+
const changeName = env.SILLYSPEC_BG_SYNC_CHANGE;
|
|
190
|
+
const cwd = env.SILLYSPEC_BG_SYNC_CWD;
|
|
191
|
+
if (!changeName || !cwd) return;
|
|
192
|
+
|
|
193
|
+
const platformOpts = {};
|
|
194
|
+
if (env.SILLYSPEC_BG_SYNC_SPEC_ROOT) platformOpts.specRoot = env.SILLYSPEC_BG_SYNC_SPEC_ROOT;
|
|
195
|
+
if (env.SILLYSPEC_BG_SYNC_RUNTIME_ROOT) platformOpts.runtimeRoot = env.SILLYSPEC_BG_SYNC_RUNTIME_ROOT;
|
|
196
|
+
|
|
197
|
+
const { triggerSync, resolveRuntimeRoot, resolveSyncTotalTimeoutMs } = await import('./shared.js');
|
|
198
|
+
const runtimeRoot = resolveRuntimeRoot(platformOpts, platformOpts.specRoot || join(cwd, '.sillyspec'));
|
|
199
|
+
truncateLogIfNeeded(runtimeRoot);
|
|
200
|
+
const startedAt = Date.now();
|
|
201
|
+
writeBgSyncLock(runtimeRoot, { pid: process.pid, startedAt, change: changeName, rerunQueued: false });
|
|
202
|
+
|
|
203
|
+
const roundTimeoutMs = Math.max(resolveSyncTotalTimeoutMs(), BG_ROUND_FLOOR_MS);
|
|
204
|
+
const deadline = startedAt + BG_TOTAL_BUDGET_MS;
|
|
205
|
+
for (let round = 1; round <= BG_MAX_ROUNDS; round++) {
|
|
206
|
+
// 轮前清位:本轮 triggerSync 的树 hash 晚于清位时刻,覆盖此后到达的全部状态;
|
|
207
|
+
// 读→写间隙内的置位存在丢失窗口(毫秒级),由下一条命令的同步兜底收敛。
|
|
208
|
+
const cur = readBgSyncLock(runtimeRoot);
|
|
209
|
+
writeBgSyncLock(runtimeRoot, { pid: process.pid, startedAt: cur?.startedAt || startedAt, change: changeName, rerunQueued: false });
|
|
210
|
+
console.log(`[spec-sync-bg] 第 ${round}/${BG_MAX_ROUNDS} 轮开始 ${new Date().toISOString()}: ${changeName}`);
|
|
211
|
+
try {
|
|
212
|
+
await triggerSync(cwd, changeName, platformOpts, { inline: true, timeoutMs: roundTimeoutMs });
|
|
213
|
+
} catch (e) {
|
|
214
|
+
console.warn(`[spec-sync-bg] 本轮同步异常(best-effort,下一条命令自动重试): ${(e && e.message) || e}`);
|
|
215
|
+
}
|
|
216
|
+
const lock = readBgSyncLock(runtimeRoot);
|
|
217
|
+
if (!lock || lock.pid !== process.pid) {
|
|
218
|
+
console.warn('[spec-sync-bg] 锁已不属于本进程(被并行 spawn 接管),让位退出');
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
if (!lock.rerunQueued) break;
|
|
222
|
+
if (Date.now() + roundTimeoutMs > deadline) {
|
|
223
|
+
console.warn('[spec-sync-bg] 总预算用尽,残留待同步状态由下一条命令的同步接管');
|
|
224
|
+
break;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
console.log(`[spec-sync-bg] 后台同步完成 ${new Date().toISOString()}: ${changeName}`);
|
|
228
|
+
removeBgSyncLock(runtimeRoot);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
export default { spawnBackgroundSync, runBgSyncFromEnv, readBgSyncLock, isBgSyncLockLive, isPidAlive };
|
package/src/run/command.js
CHANGED
|
@@ -511,6 +511,10 @@ export async function runCommand(args, cwd, specDir = null, opts = {}) {
|
|
|
511
511
|
console.error(`❌ 检测到祖先链有 ${ancestorSpecs.length} 个 .sillyspec 实例(monorepo 多实例),当前使用: ${specBase}`)
|
|
512
512
|
console.error(` 当前 cwd 不在 git 顶层仓库根下,静默绑定到子项目实例会导致进度/QUICKLOG 分裂。`)
|
|
513
513
|
console.error(` 修复:cd 回项目根目录,或用 --spec-dir <根>/.sillyspec 显式指定。`)
|
|
514
|
+
// 一键指定整行(R4-S-Q 实证:嵌套 worktree 场景下 agent 多次试错才拼对放行组合)——
|
|
515
|
+
// 意图实例即当前最近命中(specBase),直接给出可贴完整命令行。
|
|
516
|
+
console.error(` 一键指定(当前最近实例,整行照贴):`)
|
|
517
|
+
console.error(` sillyspec ${process.argv.slice(2).join(' ')} --spec-dir "${specBase}"`)
|
|
514
518
|
process.exit(2)
|
|
515
519
|
}
|
|
516
520
|
// 在 git 根下但有多实例(子项目也有 .sillyspec)→ 仅警告(正确实例已命中)
|