sillyspec 3.26.9 → 3.26.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -384,8 +384,10 @@ execute/verify 阶段会按实际代码变更更新此文档;archive 阶段会
384
384
  // ═══════════════════════════════════════════════════════════════
385
385
 
386
386
  /**
387
- * 构建紧凑 TaskCard 协调器步骤(单步,子代理并行写卡片)
387
+ * 构建紧凑 TaskCard 协调器步骤(单步,子代理按 batch 并行写卡片)
388
388
  * 每个 task 生成 20~40 行紧凑可执行卡片
389
+ * 允许把多个相关 task 合并为一个 batch,由单个子代理一次生成多张卡片,
390
+ * 在保留 plan 任务拆分完整性的同时减少子代理调用数量。
389
391
  */
390
392
  export function buildCoordinatorStep(changeDir, taskNames) {
391
393
  const taskList = taskNames.map(t => {
@@ -393,23 +395,9 @@ export function buildCoordinatorStep(changeDir, taskNames) {
393
395
  return `- task-${num}: ${t.name}`
394
396
  }).join('\n')
395
397
 
396
- const subagentPrompts = taskNames.map(t => {
397
- const num = t.num
398
- return `\`\`\`
399
- 任务编号:task-${num}
400
- 任务名称:${t.name}
401
- 文件路径:${changeDir}/tasks/task-${num}.md
402
- 当前时间:<now-datetime>(frontmatter 的 created_at 使用此值)
403
- 当前用户:<git-user>(frontmatter 的 author 使用此值)
404
-
405
- 操作:
406
- 1. 读取 ${changeDir}/design.md 和 ${changeDir}/plan.md 了解上下文
407
- 2. 读取相关源文件了解现有代码
408
- 3. 生成紧凑 TaskCard(20~40 行),格式如下:
409
-
410
- ---
411
- id: task-${num}
412
- title: ${t.name}
398
+ const taskcardTemplate = `---
399
+ id: <task-id>
400
+ title: <task-name>
413
401
  title_zh: <任务中文标题>
414
402
  author: <git-user>
415
403
  created_at: <now-datetime>
@@ -447,10 +435,7 @@ related_tests: # 可选。当本 task 改动会导致
447
435
  reason: 旧断言假设单例,改归属键后需同步
448
436
  ---
449
437
 
450
- {{include: taskcard-rules}}
451
- \`\`\``
452
- }).join('\n\n')
453
-
438
+ {{include: taskcard-rules}}`
454
439
 
455
440
  const prompt = `为 plan.md 中的每个任务生成紧凑 TaskCard。
456
441
 
@@ -468,21 +453,55 @@ ${taskList}
468
453
 
469
454
  ## 执行方式(必须严格遵守)
470
455
 
471
- **你必须使用 Agent tool 启动子代理来写每个卡片,不要自己写。**
456
+ **你必须使用 Agent tool 启动子代理来写卡片,不要自己写。**
472
457
 
473
458
  1. 确认 \`${changeDir}/tasks/\` 目录存在(不存在则创建)
474
- 2. 为每个任务启动一个独立子代理(Agent tool),可并行启动多个
475
- 3. 每个子代理使用对应的 prompt(见下方模板)
476
- 4. 等待所有子代理完成
477
- 5. 验证每个 task-N.md 文件已生成且非空
478
-
479
- ### 子代理 prompt 模板
480
- 为每个任务使用以下 prompt 启动子代理:
481
-
482
- ${subagentPrompts}
459
+ 2. **按 batch 分派子代理(减少总子代理数量,而不是一个 task 一个子代理):**
460
+ - 把任务按「同一 Wave + 同一模块/相近能力 + 无跨 batch 强依赖」原则分组
461
+ - **每个 batch 包含 2~4 个 task**;Wave 内任务数 ≤4 时整个 Wave 可作为一个 batch
462
+ - 有提供/消费契约的 task 尽量放到同一 batch(子代理能同时看到 consumer 与 provider,避免契约字段漏配)
463
+ - 跨 Wave 依赖的 task 不要放在同一 batch(子代理只需读 plan.md,但 batch 内 task 的 allowed_paths 不应互相阻塞)
464
+ 3. 为每个 batch 启动一个独立子代理(Agent tool),可并行启动多个 batch
465
+ 4. 每个子代理使用下方「批量 TaskCard 子代理 prompt」,一次生成该 batch 的全部 task-N.md
466
+ 5. 等待所有 batch 子代理完成
467
+ 6. 验证每个 task-N.md 文件已生成且非空
468
+
469
+ > 设计意图:plan.md 里 task 数可以较多(能力拆分完整),但 TaskCard 生成阶段要合理合并,避免子代理数量随 task 数线性爆炸。一个子代理生成 2~4 张卡片与生成 1 张卡片的 token/时间成本接近,却能显著减少协调开销。
470
+
471
+ ### 批量 TaskCard 子代理 prompt
472
+ \`\`\`
473
+ 你是一个专注的 TaskCard 生成器。你的任务是为指定 batch 生成全部 TaskCard。
474
+
475
+ ## 输入
476
+ - 变更目录:${changeDir}
477
+ - 当前时间:<now-datetime>(frontmatter 的 created_at 使用此值)
478
+ - 当前用户:<git-user>(frontmatter 的 author 使用此值)
479
+ - 本 batch 任务列表:
480
+ <由主 agent 注入:task-01: 名称 / task-02: 名称 / ...>
481
+
482
+ ## 操作
483
+ 1. 读取 ${changeDir}/design.md 和 ${changeDir}/plan.md 了解整体上下文
484
+ 2. 读取本 batch 涉及的相关源文件
485
+ 3. 为 batch 中的**每一个 task 独立生成**一个 task-N.md 文件
486
+ 4. 每个文件使用如下模板(把 <task-id> / <task-name> 替换为具体编号和名称):
487
+
488
+ ${taskcardTemplate}
489
+
490
+ ## 约束
491
+ - 每个 task 20~40 行;禁止在 TaskCard 里写实现细节之外的冗长设计
492
+ - 同 batch 内 task 的 allowed_paths 不要互相冲突
493
+ - 若 task 之间有 provides/expects_from 契约,同 batch 生成时必须字段对齐
494
+ - 只生成本 batch 声明的任务,不要多写或少写
495
+ - 不要在 TaskCard 里泄露 plan.md 中未出现的实现假设
496
+
497
+ ## 完成标志
498
+ - 本 batch 的每个 task-N.md 都已写入 ${changeDir}/tasks/
499
+ - 每个文件非空且 frontmatter 完整
500
+ \`\`\`
483
501
 
484
502
  ## 验收(生成后自查,不另开步骤)
485
503
  - 每个 task-N.md 文件存在且非空
504
+ - 所有 task 都被某个 batch 覆盖、无遗漏、无重复
486
505
  - frontmatter 字段分两组对照(与 plan-postcheck / taskcard-rules.md 同源,三处清单以此为准):
487
506
  - **硬校验 9 字段**(缺失 plan-postcheck 直接报错阻断):id、title、title_zh、allowed_paths、goal、implementation、acceptance、verify、constraints
488
507
  - **规范约定 5 字段**(应填但缺失只影响规范性,不阻断):author、created_at、priority、depends_on、blocks
@@ -496,7 +515,7 @@ ${subagentPrompts}
496
515
 
497
516
  return {
498
517
  id: 'generate_blueprints',
499
- name: '生成 TaskCard(子代理并行)',
518
+ name: '生成 TaskCard(子代理按 batch 并行)',
500
519
  prompt,
501
520
  outputHint: 'TaskCard 生成结果',
502
521
  optional: false
@@ -12,6 +12,12 @@ export const definition = {
12
12
  - CLI 是短进程,run 与 done 是独立进程,sessionId 靠 --change 跨进程传递
13
13
  - **完成每个 step 必须带 --change <quick-session-id>**(命令由 CLI 在下方注入);多会话并发时不带会命中他者会话状态
14
14
 
15
+ ### ⚠️ 危险文件预声明(预判要改 src 核心时看一眼)
16
+ step 3 --done 审计会把 src 核心文件(CLI / 状态机 / 注入框架等危险清单)、新增文件、删除文件判为危险变更并拦截。若你**预判**本 quick 的修复目标本身就是这类文件,重新启动 quick 时带上 --force-baseline(受保护/危险文件)/ --allow-new(新增文件)/ --allow-delete(删除文件)预声明——flag 会持久化进会话守卫,后续 --done 直接生效,省一轮拦截重跑。不预带也可:step 3 拦截后按提示带 flag 重跑 --done 同样解锁。
17
+
18
+ ### 🧭 文件预声明(多 agent 并发仓库推荐)
19
+ --done 审计按「时间窗 diff」归属文件:quick 窗口内**并行会话**改的文件也会被算进本会话的 QUICKLOG「文件:」行。多个 agent 同时操作本仓库时,启动 quick 建议带 \`--files <a.js,b.js>\` 声明预计要改的文件——声明即归属:文件行只落声明命中的文件,窗口内未声明脏文件以「审计:」行落盘追溯(不混入、不静默丢)。不预判也行:--done 后按 QUICKLOG 核对说明手动精修文件行兜底。
20
+
15
21
  ### 操作
16
22
  1. 检查关联变更(\`<linked-changes>\`,逗号分隔的变更名列表;显示「(无)」= 不关联变更),确定记录方式
17
23
  2. 理解任务:模糊则问一个问题确认
@@ -33,7 +39,7 @@ export const definition = {
33
39
 
34
40
  **你不要创建或修改任何 QUICKLOG 的条目骨架 / tasks.md 记录**——ql-ID 分配、「进行中」条目、状态翻转、task 勾选、轮转全由 CLI 接管,你无需手写。<quicklog-id> 可用于 design.md / plan.md / archive / 模块变更索引引用。
35
41
 
36
- > ℹ️ step 3 --done 时,CLI 已落盘结构化 QUICKLOG 条目(标题从「需求:」提取、正文四字段分行、文件行可用 --file-notes 写多行括注)。事后只需按需核对,详见 step 3「QUICKLOG 正文核对」。
42
+ > ℹ️ step 3 --done 时,CLI 已落盘结构化 QUICKLOG 条目(标题从「需求:」提取——届时写一句语义化短标题,规则见 step3 结果摘要模板;正文四字段分行、文件行可用 --file-notes 写多行括注)。事后只需按需核对,详见 step 3「QUICKLOG 正文核对」。
37
43
 
38
44
  ### 输出
39
45
  任务理解 + 上下文摘要`,
@@ -76,12 +82,14 @@ export const definition = {
76
82
  \`--output\` 是 QUICKLOG「结果:」归档的唯一来源,**必须按此结构给全四项**(逐项一句话,不可用「见前述」替代):
77
83
 
78
84
  \`\`\`
79
- 需求:用户/任务要什么
85
+ 需求:一句话语义化短标题(写「改了什么」)
80
86
  根因:为什么这样改(若纯新增/配置/样式无根因,写「无,纯新增/纯样式」)
81
87
  方案:怎么改的
82
88
  结果:验证情况(测试数 / lint / typecheck / 部署状态)
83
89
  \`\`\`
84
90
 
91
+ ⚠️ **「需求:」就是 QUICKLOG 条目标题**——CLI 提取该字段、截到首个标点(,。;,;)、超 80 字再截断。写成完整需求长句会被截成语义不完整的状语前半段(如「在 X 时,…」标题只剩「在 X 时」),事后还得手改。一句话说清改了什么(如「登录限流修复——INCR 计数误清」),需求背景放「根因:/方案:」字段。
92
+
85
93
  缺任一项,\`--done\` 会被拒并提示缺失字段,补全后重跑即可(不丢进度)。
86
94
 
87
95
  ### 📎 文件括注 --file-notes(可选,省事后手改文件行)
@@ -97,7 +105,7 @@ sillyspec run quick --done --change <id> --output "需求:… 根因:… 方
97
105
 
98
106
  ### 收尾推荐顺序(模块文档在 --done 前,QUICKLOG 在 --done 后,别记混)
99
107
  1. 【--done 前】命中模块 → 改模块文档 → \`git add -- <模块文档>\`(见下方「模块文档同步」)
100
- 2. 【--done】\`sillyspec run quick --done --change <id> --output "四字段" [--file-notes "..."]\`(CLI 自动翻完成 + 勾 task + 落盘 QUICKLOG 标题/文件/正文)
108
+ 2. 【--done】\`sillyspec run quick --done --change <id> --output "四字段" [--file-notes "..."]\`(CLI 自动翻完成 + 勾 task + 落盘 QUICKLOG 标题/文件/正文;若关联变更的 tasks.md 已全部勾选,CLI 还会自动归档该关联变更到 \`changes/archive/\`,无需手动跑 archive 阶段)
101
109
  3. 【--done 后】核对 QUICKLOG 标题(CLI 已从「需求:」自动提取,仅弱标题才手改)→ 若改了再 \`git add\`
102
110
 
103
111
  ### 操作
@@ -126,7 +134,7 @@ sillyspec run quick --done --change <id> --output "需求:… 根因:… 方
126
134
  ### 📝 QUICKLOG 正文核对(--done 之后,按需;多数无需手改)
127
135
  CLI 落盘的条目已是结构化产物(非简版骨架):标题从「需求:」自动提取、正文四字段自动分行、文件行用 --file-notes 时为多行括注。--done 后只需核对,多数情况无需手改:
128
136
 
129
- 1. 标题:CLI 已从「需求:」提取摘要刷新标题;仅当需求写得太弱(占位/笼统)才手改为真实需求摘要。
137
+ 1. 标题:CLI 已从「需求:」提取(截到首个标点、超 80 字截断);按 step3 模板写成一句语义化短标题则无需手改,写成长句被截断时才手改。
130
138
  2. 文件:用了 --file-notes 已是多行括注;没传则回填审计到的实际文件单行,可事后补括注(示例见下)。
131
139
  3. 正文:四字段已分行落盘,按需扩写充实(不允许只留一段「结果:」)。
132
140
 
@@ -138,7 +138,7 @@ export const definition = {
138
138
  e. 等待该项目所有子代理完成后,验证文件是否生成且非空
139
139
  f. 该项目完成后,继续下一个项目
140
140
  3. 所有项目完成后,运行以下命令检查产物:
141
- \`sillyspec workflow check scan-docs --project <project>\`(CLI 子命令,任意项目根可用)
141
+ \`sillyspec workflow check scan-docs --project <project>\`(CLI 子命令,任意项目根可用)
142
142
  对每个项目分别执行(将 \`<project>\` 替换为实际项目名)
143
143
  4. 如果检查报告有失败项,按报告中的角色和文件重试失败的部分
144
144
 
@@ -415,7 +415,7 @@ doc_type: module-card
415
415
  module_id: <module-id>
416
416
  ---
417
417
 
418
- # <module-id>
418
+ # <中文名>(<module-id>)
419
419
 
420
420
  ## 定位
421
421
  (负责什么,不负责什么 — 明确边界)
@@ -437,6 +437,7 @@ module_id: <module-id>
437
437
 
438
438
  规则:
439
439
  - 不要编造接口或依赖,只写 grep/rg 能搜到的
440
+ - 一级标题格式 \`# <中文名>(<module-id>)\`:中文名从模块职责提炼 2-8 字(如 认证与令牌、变更中心),括号内保留 module-id 原样——sillyhub 平台解析识别用
440
441
  - 目标长度:500-1000 字 / 80-150 行
441
442
  - 如果模块特别复杂(状态机、多角色交互、复杂领域规则),可以在 modules/details/ 下生成扩展文档(如 details/<module-id>-flow.md),agent 默认不读
442
443
  - 不要重复 _module-map.yaml 中的索引信息
@@ -88,8 +88,11 @@ export const definition = {
88
88
  10. 读取匹配到的 \`.sillyspec/docs/<project>/modules/<module>.md\`
89
89
  11. **检查模块索引可信度**:如果相关模块的 needs_review 为 true,提示"该模块索引可能不可信,需要回看模块卡片或源码"
90
90
 
91
+ ### worktree 基线锚点(CLI 注入)
92
+ {WORKTREE_BASELINE_INFO}
93
+
91
94
  ### 输出
92
- 文件加载确认清单(含 decisions.md 当前版本/未决项状态、模块文档 + 索引可信度)`,
95
+ 文件加载确认清单(含 decisions.md 当前版本/未决项状态、模块文档 + 索引可信度、worktree 基线锚点)`,
93
96
  outputHint: '文件确认清单',
94
97
  optional: false
95
98
  },
package/src/sync.js CHANGED
@@ -13,6 +13,7 @@ import { join } from 'path';
13
13
  import { resolvePlatformSpecDir } from './progress.js';
14
14
  import { safeGit } from './git-helper.js';
15
15
  import { PLATFORM_MANAGED_FILENAME } from './run/shared.js';
16
+ import { syncSpecTree } from './spec-sync.js';
16
17
 
17
18
  // sync 是 best-effort(网络失败只 warn):平台指针失效时不抛,跳过平台、回退本地。
18
19
  function safePlatformSpecDir(cwd) {
@@ -464,6 +465,15 @@ export class SyncManager {
464
465
  debugLog(`[sync] 文档同步失败(不影响进度): ${changeName}: ${err.message}`);
465
466
  }
466
467
 
468
+ // 2026-08-17-spec-file-incremental-sync:进度上行+文档直推成功后顺带推整个 spec 树
469
+ // (plan.md、tasks/、module-impact.md 等),让变更中心文件树自动更新。
470
+ // 无 daemon 时 CLI 短进程无本地清单缓存,以服务器清单为锚做增量 diff。
471
+ try {
472
+ await syncSpecTree(join(this.cwd, '.sillyspec'), this._getPlatform(), changeName);
473
+ } catch (err) {
474
+ debugLog(`[sync] spec 树增量同步失败(不影响进度): ${changeName}: ${err.message}`);
475
+ }
476
+
467
477
  return { synced: 1, errors: [] };
468
478
  }
469
479
 
package/src/worktree.js CHANGED
@@ -757,6 +757,43 @@ export class WorktreeManager {
757
757
  * partial=有残留(目录/meta/git 注册未清);skipped=无需清理;kept=native-worktree 保留。
758
758
  * residual:未清干净的路径/引用列表(空数组表示干净)。
759
759
  */
760
+ /**
761
+ * fail-closed:清理删分支前的审计引用校验(2026-08-18,两例实录:execute --done 批量完成 /
762
+ * worktree apply 后自动 cleanup 均无条件删分支 ref,task commit 变 dangling)。
763
+ * apply 只复制文件内容不携带 commit(主仓重 commit 后 hash 不同),分支 ref 一删,
764
+ * task review.json 的 base/head 引用即悬空(gc 后真丢,git diff base..head 无法复核)。
765
+ * 有引用 → cleanup 保留分支并提示手动删;校验自身异常也按"有引用"处理(宁保留勿误删)。
766
+ * @private
767
+ * @param {string} branch - 待删分支名
768
+ * @returns {string[]} 引用该分支 commit 的 review.json 路径列表(空 = 无引用,删分支安全)
769
+ */
770
+ _branchReviewReferences(branch) {
771
+ const runsDir = join(this.cwd, '.sillyspec', '.runtime', 'execute-runs');
772
+ if (!existsSync(runsDir)) return [];
773
+ const branchCommits = new Set(
774
+ (gitQuiet(this.cwd, ['rev-list', branch]) || '').split('\n').map(s => s.trim()).filter(Boolean)
775
+ );
776
+ if (branchCommits.size === 0) return [];
777
+ const refs = [];
778
+ const visit = (dir) => {
779
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
780
+ const p = join(dir, entry.name);
781
+ if (entry.isDirectory()) visit(p);
782
+ else if (entry.name === 'review.json') {
783
+ try {
784
+ const r = JSON.parse(readFileSync(p, 'utf8'));
785
+ for (const key of ['base', 'head']) {
786
+ const h = typeof r[key] === 'string' ? r[key].trim() : '';
787
+ if (h && branchCommits.has(h)) { refs.push(p); break; }
788
+ }
789
+ } catch { /* review.json 损坏不参与判定 */ }
790
+ }
791
+ }
792
+ };
793
+ visit(runsDir);
794
+ return refs;
795
+ }
796
+
760
797
  cleanup(changeName, { force = false, maxRetries = 3 } = {}) {
761
798
  const name = validateChangeName(changeName);
762
799
  const meta = this.getMeta(name);
@@ -862,12 +899,19 @@ export class WorktreeManager {
862
899
  // prune 失败不阻断
863
900
  }
864
901
 
865
- // 4. 删除分支(忽略分支不存在的错误)
902
+ // 4. 删除分支(fail-closed:task review 引用可达性校验。force 也不绕过——force 语义 =
903
+ // 丢弃内容,不含丢弃审计链;确要删用 git branch -D 手动执行)
866
904
  try {
867
- gitQuiet(this.cwd, ['branch', '-D', branch]);
868
- details.push('branch deleted');
905
+ const reviewRefs = this._branchReviewReferences(branch);
906
+ if (reviewRefs.length > 0) {
907
+ details.push(`branch kept: ${reviewRefs.length} 个 task review.json 引用分支上的 commit(base/head 审计保护)`);
908
+ console.log(`🔗 分支保留:${branch} 上有 ${reviewRefs.length} 个 task review.json 引用其 commit(apply 只复制文件内容,ref 删除后 base/head 悬空无法复核)。确要删除:git branch -D ${branch}`);
909
+ } else {
910
+ gitQuiet(this.cwd, ['branch', '-D', branch]);
911
+ details.push('branch deleted');
912
+ }
869
913
  } catch {
870
- // 分支可能已被删除,幂等跳过
914
+ // 分支可能已被删除(幂等跳过);引用校验自身异常也归入此分支——不因校验失败而误删
871
915
  }
872
916
 
873
917
  // 5. 清除 meta 目录(in-place 模式也执行——这是 in-place meta 残留的修复点)