sillyspec 3.29.4 → 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/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/diagnostic-codes.js +2 -1
- package/src/index.js +33 -6
- package/src/init.js +25 -8
- package/src/machine-interface.js +88 -3
- package/src/module-impact.js +51 -22
- package/src/run/command.js +4 -0
- package/src/run/complete-handlers.js +36 -1
- package/src/run/complete.js +6 -3
- package/src/run/gate-snapshot.js +10 -4
- package/src/run/gates.js +114 -37
- package/src/run/quick-audit.js +28 -2
- package/src/run/shared.js +3 -1
- package/src/stages/execute.js +2 -2
- package/src/stages/plan-postcheck.js +33 -19
- package/src/taskcard-frontmatter.js +57 -0
- package/src/verify-postcheck.js +59 -1
- 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
|
}
|
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)→ 仅警告(正确实例已命中)
|
|
@@ -1265,6 +1265,25 @@ export async function handleWorkflowPostCheck({ stageName, steps, currentIdx, cw
|
|
|
1265
1265
|
}
|
|
1266
1266
|
return null
|
|
1267
1267
|
}
|
|
1268
|
+
/**
|
|
1269
|
+
* guard.linkedChanges/linkedChangesAuto 并入 --done 显式声明(R4-S-Q 缺陷 B 纯函数核):
|
|
1270
|
+
* 持久化值 ∪ 显式值(去重保序,persisted 在前);显式 ['none'] 语义为清空 manual 面(auto 不动);
|
|
1271
|
+
* 无变化返回原对象引用(调用点以此判「是否需要回写」)。guard 非对象原样返回。
|
|
1272
|
+
*/
|
|
1273
|
+
export function mergeGuardLinkedChanges(guard, explicitLinked, explicitAuto) {
|
|
1274
|
+
if (!guard || typeof guard !== 'object') return guard
|
|
1275
|
+
const cur = Array.isArray(guard.linkedChanges) ? guard.linkedChanges : []
|
|
1276
|
+
const curAuto = Array.isArray(guard.linkedChangesAuto) ? guard.linkedChangesAuto : []
|
|
1277
|
+
const ex = (Array.isArray(explicitLinked) ? explicitLinked : []).filter(Boolean)
|
|
1278
|
+
const exAuto = (Array.isArray(explicitAuto) ? explicitAuto : []).filter(Boolean)
|
|
1279
|
+
const next = ex.length === 1 && ex[0] === 'none' ? [] : [...new Set([...cur, ...ex])]
|
|
1280
|
+
const nextAuto = exAuto.length === 0 ? curAuto : [...new Set([...curAuto, ...exAuto])]
|
|
1281
|
+
const unchanged = next.length === cur.length && next.every((v, i) => v === cur[i])
|
|
1282
|
+
&& nextAuto.length === curAuto.length && nextAuto.every((v, i) => v === curAuto[i])
|
|
1283
|
+
if (unchanged) return guard
|
|
1284
|
+
return { ...guard, linkedChanges: next, linkedChangesAuto: nextAuto }
|
|
1285
|
+
}
|
|
1286
|
+
|
|
1268
1287
|
/**
|
|
1269
1288
|
* quick 阶段完成收尾(W6 Step6b 从 completeStep 内联块抽出):
|
|
1270
1289
|
* 强校验 QUICKLOG 条目 + 审计(auditQuickCompletion)+ 结果摘要结构校验 + 翻状态/勾 tasks.md
|
|
@@ -1274,7 +1293,7 @@ export async function handleWorkflowPostCheck({ stageName, steps, currentIdx, cw
|
|
|
1274
1293
|
* isForceBaseline/isAllowNew/platformOpts。辅助函数直接 import(safeGit/auditQuickCompletion ← shared,
|
|
1275
1294
|
* printQuickAuditReview ← quick-audit,4 个 quicklog fns ← quicklog,unlinkSync/rmSync ← fs 静态)。
|
|
1276
1295
|
*/
|
|
1277
|
-
export async function handleQuickStageCompletion({ stageName, steps, currentIdx, cwd, progress, changeName, specBase, outputText, confirm, isForceBaseline, isAllowNew, isAllowDelete, isNoDocs, sessionFlag = null, platformOpts, pm, quickFiles = [] }) {
|
|
1296
|
+
export async function handleQuickStageCompletion({ stageName, steps, currentIdx, cwd, progress, changeName, specBase, outputText, confirm, isForceBaseline, isAllowNew, isAllowDelete, isNoDocs, sessionFlag = null, platformOpts, pm, quickFiles = [], linkedChanges = [], linkedChangesAuto = [] }) {
|
|
1278
1297
|
// quick 收尾:强校验 QUICKLOG 条目 + 翻状态 + 勾 tasks.md(CLI 接管)
|
|
1279
1298
|
if (stageName === 'quick') {
|
|
1280
1299
|
// §4.6 从 session guard.json 读 guard(不依赖 progress.quickGuard)。
|
|
@@ -1293,6 +1312,22 @@ export async function handleQuickStageCompletion({ stageName, steps, currentIdx,
|
|
|
1293
1312
|
: (existsSync(legacyGuardFile) ? JSON.parse(readFileSync(legacyGuardFile, 'utf8')) : null)
|
|
1294
1313
|
} catch {}
|
|
1295
1314
|
|
|
1315
|
+
// R4-S-Q 缺陷 B:--done 时显式 --linked-changes 并入 guard(启动持久化值 ∪ 本次显式值,
|
|
1316
|
+
// 'none' 清空)再回写 sessionGuardFile。下游关账 closeQuickLinkedChanges / 资产尾蒸馏 /
|
|
1317
|
+
// quicklog「关联变更」渲染全读 guard——不并入则 --done 时声明的关联被静默丢弃(R4-S-Q
|
|
1318
|
+
// 实测:--linked-changes 两次都在命令行上,资产尾零触发 + 条目渲染「(无)」)。
|
|
1319
|
+
// guard 缺失(brownfield)不造对象——保持 D-3 无 guard 降级语义不变。
|
|
1320
|
+
try {
|
|
1321
|
+
if (guard && (linkedChanges.length > 0 || linkedChangesAuto.length > 0)) {
|
|
1322
|
+
const merged = mergeGuardLinkedChanges(guard, linkedChanges, linkedChangesAuto)
|
|
1323
|
+
if (merged !== guard) {
|
|
1324
|
+
guard = merged
|
|
1325
|
+
writeAtomicSync(sessionGuardFile, JSON.stringify(guard, null, 2))
|
|
1326
|
+
console.log(`🔗 --done 显式关联变更已并入 guard: ${(guard.linkedChanges ?? []).join(', ') || '(none 清空)'}`)
|
|
1327
|
+
}
|
|
1328
|
+
}
|
|
1329
|
+
} catch { /* 并入/回写失败不拦完成,guard 内存原值兜底 */ }
|
|
1330
|
+
|
|
1296
1331
|
// 强校验 / 收尾:本会话必须有一条真实 QUICKLOG 条目(治「报 SAFE 但漏写」bug)。
|
|
1297
1332
|
// guard 缺失(brownfield:新代码前启动的会话)不阻断——兜底补写一条记录,保住「完成必有记录」不变量。
|
|
1298
1333
|
const gitUser = safeGit(cwd, ['config', 'user.name']).value || 'unknown'
|
package/src/run/complete.js
CHANGED
|
@@ -120,7 +120,7 @@ export function synthesizeStepOutput({ stageName, stepName, cwd }) {
|
|
|
120
120
|
}
|
|
121
121
|
|
|
122
122
|
export async function completeStep(pm, progress, stageName, cwd, outputText, inputText = null, options = {}) {
|
|
123
|
-
const { printNext = true, confirm = false, changeName, platformOpts = {}, nonInteractive = false, isForceBaseline = false, isAllowNew = false, isAllowDelete = false, isNoDocs = false, isSkipApply = false, sessionFlag = null, quickFiles = [] } = options
|
|
123
|
+
const { printNext = true, confirm = false, changeName, platformOpts = {}, nonInteractive = false, isForceBaseline = false, isAllowNew = false, isAllowDelete = false, isNoDocs = false, isSkipApply = false, sessionFlag = null, quickFiles = [], linkedChanges = [], linkedChangesAuto = [] } = options
|
|
124
124
|
// specRoot(平台) > specDriftAnchor(worktree 漂移锚定主仓) > cwd/.sillyspec(本地)——
|
|
125
125
|
// 与 prompt.js resolvePromptSpecBase 同序:--done 收尾的 user-inputs.md/超长 artifact 落盘
|
|
126
126
|
// 必须与 prompt 渲染同一根,否则写进 worktree 副本目录、随 cleanup 整目录删除。
|
|
@@ -619,8 +619,11 @@ export async function completeStep(pm, progress, stageName, cwd, outputText, inp
|
|
|
619
619
|
}
|
|
620
620
|
// quick 收尾(W6 Step6b 抽至 complete-handlers.js handleQuickStageCompletion);
|
|
621
621
|
// isNoDocs(task-02)--no-docs 豁免随链透传进 auditQuickCompletion options;
|
|
622
|
-
// sessionFlag(task-03)--session 随链透传进 quick
|
|
623
|
-
|
|
622
|
+
// sessionFlag(task-03)--session 随链透传进 quick 轻量归档链所有权校验;
|
|
623
|
+
// linkedChanges/linkedChangesAuto(R4-S-Q 缺陷 B):--done 时显式 --linked-changes 随链透传——
|
|
624
|
+
// 完成侧 guard 从落盘文件读,不接 opts 会静默丢弃 --done 时声明的关联(蒸馏尾/quicklog
|
|
625
|
+
// 关联变更行/closeQuickLinkedChanges 三处消费同断)
|
|
626
|
+
await handleQuickStageCompletion({ stageName, steps, currentIdx, cwd, progress, changeName, specBase, outputText, confirm, isForceBaseline, isAllowNew, isAllowDelete, isNoDocs, sessionFlag, platformOpts, pm, quickFiles, linkedChanges, linkedChangesAuto })
|
|
624
627
|
|
|
625
628
|
// ── reopen --done 回填(坑 brainstorm-reopen-step-state-desync)──
|
|
626
629
|
// nextPendingIdx === -1 且无 waiting,说明要进阶段完成分支。此时若存在 stale 步骤
|
package/src/run/gate-snapshot.js
CHANGED
|
@@ -389,15 +389,21 @@ export function createGateSnapshot({ cwd, files, sourceRoot = null, skipImportSm
|
|
|
389
389
|
// 两份都变于祖先且互不相等(真双写分叉)→ 显式警告 + 取 cwd(主仓直写=本会话最新编辑位约定)。
|
|
390
390
|
// 行尾归一(坑 autocrlf-byte-misjudge):本机 autocrlf=true 时快照 checkout 是 CRLF、
|
|
391
391
|
// 工作区文件常是 LF——字节直比会误判「两边都改」。比较一律 CRLF→LF 归一后做。
|
|
392
|
+
// 对称 trim(坑 gate-snapshot-ancestor-trim,2026-09-20 taskcard-yaml-hardgate verify 实证):
|
|
393
|
+
// git() 助手对全部输出 .trim()(rev-parse 场景需要),git show 的祖先内容因此被剥掉尾换行——
|
|
394
|
+
// 与磁盘读取(仅 CRLF 归一)不对称,已存在文件恒 anc!==cwd 误判「双写分叉」取主仓旧版,
|
|
395
|
+
// worktree 交付的修改进不了 verify 门快照→门假红(新文件因主仓不存在幸免)。比较三侧
|
|
396
|
+
// 统一 normalizeEol+trim 同口径——首尾空白差异不参与分叉判定(语义修改不会只差首尾空白)。
|
|
392
397
|
const normalizeEol = (buf) => String(buf).replace(/\r\n/g, '\n')
|
|
398
|
+
const cmpText = (t) => normalizeEol(t).trim()
|
|
393
399
|
const headFileContent = (rel) => {
|
|
394
400
|
if (mergeBase) {
|
|
395
401
|
try {
|
|
396
402
|
const anc = git(cwd, ['show', `${mergeBase}:${rel}`])
|
|
397
|
-
if (typeof anc === 'string') return
|
|
403
|
+
if (typeof anc === 'string') return cmpText(anc)
|
|
398
404
|
} catch { /* 祖先无此文件(新增文件)→ 落到快照 HEAD 口径 */ }
|
|
399
405
|
}
|
|
400
|
-
try { return
|
|
406
|
+
try { return cmpText(readFileSync(join(snapshotRoot, rel))) } catch { return null }
|
|
401
407
|
}
|
|
402
408
|
for (const f of files) {
|
|
403
409
|
const isSillyspecRuntime = typeof f === 'string' && (f.startsWith('.sillyspec/.runtime/') || f.startsWith('.sillyspec/quicklog/') || f === '.sillyspec/local.yaml' || f === '.sillyspec/.sillyspec-platform.json')
|
|
@@ -407,8 +413,8 @@ export function createGateSnapshot({ cwd, files, sourceRoot = null, skipImportSm
|
|
|
407
413
|
const wtPath = join(sourceRoot, f), cwdPath = join(cwd, f)
|
|
408
414
|
if (existsSync(wtPath) && existsSync(cwdPath)) {
|
|
409
415
|
let wtText = null, cwdText = null
|
|
410
|
-
try { wtText =
|
|
411
|
-
try { cwdText =
|
|
416
|
+
try { wtText = cmpText(readFileSync(wtPath)) } catch { /* 读失败不介入 */ }
|
|
417
|
+
try { cwdText = cmpText(readFileSync(cwdPath)) } catch { /* 读失败不介入 */ }
|
|
412
418
|
if (wtText !== null && cwdText !== null && wtText !== cwdText) {
|
|
413
419
|
const base = headFileContent(f)
|
|
414
420
|
const wtDiff = base === null || wtText !== base
|