sillyspec 3.28.3 → 3.28.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/.claude/skills/sillyspec-auto/SKILL.md +54 -54
- package/.claude/skills/sillyspec-brainstorm/SKILL.md +114 -114
- package/.claude/skills/sillyspec-continue/SKILL.md +17 -17
- package/.claude/skills/sillyspec-execute/SKILL.md +222 -222
- package/.claude/skills/sillyspec-knowledge/SKILL.md +40 -40
- package/.claude/skills/sillyspec-plan/SKILL.md +156 -156
- package/.claude/skills/sillyspec-propose/SKILL.md +16 -16
- package/.claude/skills/sillyspec-resume/SKILL.md +21 -21
- package/.claude/skills/sillyspec-state/SKILL.md +25 -25
- package/.claude/skills/sillyspec-workspace/SKILL.md +33 -33
- package/AGENTS.md +25 -0
- package/INSTRUCTIONS.md +12 -0
- package/package.json +1 -1
- package/results-review-tier.md +18 -0
- package/src/agent-session-log.js +4 -3
- package/src/archive-delta.js +82 -0
- package/src/change-delete.js +2 -1
- package/src/change-list.js +18 -3
- package/src/change-risk-profile.js +77 -6
- package/src/commit-suggest.js +52 -1
- package/src/config-cat.js +17 -1
- package/src/constants.js +3 -1
- package/src/decision-distill.js +45 -5
- package/src/decisions-io.js +96 -0
- package/src/design-facts.js +3 -0
- package/src/dispatch/probe.js +31 -8
- package/src/docs-check.js +102 -3
- package/src/doctor-diagnostics.js +441 -5
- package/src/foreign-declared.js +53 -2
- package/src/index.js +606 -22
- package/src/mcp-server.js +144 -0
- package/src/module-impact.js +84 -12
- package/src/modules.js +46 -34
- package/src/plan-adopt-waves.js +13 -5
- package/src/progress/change-registry.js +34 -0
- package/src/progress.js +75 -2
- package/src/quick-recommend.js +6 -1
- package/src/quicklog.js +70 -8
- package/src/review-dispatch.js +377 -0
- package/src/run/archive-distill.js +48 -0
- package/src/run/command.js +48 -13
- package/src/run/complete-handlers.js +215 -9
- package/src/run/complete.js +184 -2
- package/src/run/gate-snapshot.js +88 -0
- package/src/run/gates.js +206 -58
- package/src/run/prompt.js +150 -14
- package/src/run/quick-audit.js +38 -13
- package/src/run/shared.js +183 -12
- package/src/run/stage.js +52 -2
- package/src/run/verify-quality-scan.js +336 -0
- package/src/runtime-hygiene.js +84 -0
- package/src/scope-audit.js +735 -0
- package/src/sillyhub-mcp/client.js +58 -3
- package/src/spec-sync.js +63 -8
- package/src/stage-contract.js +83 -3
- package/src/stage-review-checklist.js +44 -0
- package/src/stage-review.js +125 -4
- package/src/stages/archive.js +60 -167
- package/src/stages/brainstorm.js +7 -17
- package/src/stages/doctor.js +41 -515
- package/src/stages/execute.js +13 -10
- package/src/stages/plan-postcheck.js +191 -7
- package/src/stages/plan.js +7 -35
- package/src/stages/quick.js +11 -11
- package/src/stages/verify.js +24 -39
- package/src/sync-noise.js +158 -0
- package/src/sync.js +93 -27
- package/src/task-review.js +133 -17
- package/src/taskcard.js +46 -3
- package/src/validate-artifacts.js +163 -0
- package/src/verify-facts-schema.js +141 -0
- package/src/verify-postcheck.js +347 -29
- package/src/verify-probes.js +275 -24
- package/src/workflow.js +6 -0
- package/src/worktree-apply.js +167 -24
- package/src/worktree.js +5 -1
- package/templates/agents-instruction.md +1 -0
|
@@ -54,8 +54,11 @@ export class SillyHubMcpClient {
|
|
|
54
54
|
: DEFAULT_TIMEOUT_MS;
|
|
55
55
|
// 缺 url 或 token → 视为未配置,所有方法降级不发网络
|
|
56
56
|
this._configured = Boolean(this._url && this._token);
|
|
57
|
-
//
|
|
58
|
-
|
|
57
|
+
// 端点必须带尾斜杠。双形态兼容(2026-09-10 成对签发批次,活体实证 /mcp/mcp/ 404):
|
|
58
|
+
// ① origin 形态(历史 local.yaml / §7.4 兜底写入,如 https://host)——本客户端拼 /mcp/;
|
|
59
|
+
// ② 完整端点形态(平台 mcp-tokens 签发响应的 gateway_url 如 https://host/mcp/,及平台侧
|
|
60
|
+
// daemon local-yaml-writer 写的 origin+'/mcp')——路径已含 /mcp 不再叠加,叠加必 404。
|
|
61
|
+
this._endpoint = !this._url ? '' : (/\/mcp$/.test(this._url) ? `${this._url}/` : `${this._url}/mcp/`);
|
|
59
62
|
this._rpcId = 0;
|
|
60
63
|
// MCP streamable HTTP session 状态:惰性 initialize 后才有(见 _ensureSession)
|
|
61
64
|
this._sessionId = null;
|
|
@@ -541,8 +544,11 @@ export class SillyHubMcpClient {
|
|
|
541
544
|
const result = await this._callTool('dispatch_worker', args);
|
|
542
545
|
if (result === null) return { workerId: null, status: 'unavailable' };
|
|
543
546
|
const value = this._parseToolReturnValue(result);
|
|
547
|
+
// id 兜底(2026-09-10 spike + 平台侧 22cdf89d1 回归实证):dispatch_worker 实返
|
|
548
|
+
// {id, role, objective, status, agent_type…}——旧解析只认 worker_id/workerId,派发
|
|
549
|
+
// 成功时 workerId=null,后续轮询/get_worker_result 全断(平台侧交付遗留①)。
|
|
544
550
|
const workerId = value && typeof value === 'object'
|
|
545
|
-
? (value.worker_id ?? value.workerId ?? null)
|
|
551
|
+
? (value.worker_id ?? value.workerId ?? value.id ?? null)
|
|
546
552
|
: null;
|
|
547
553
|
const status = value && typeof value === 'object'
|
|
548
554
|
? (value.status ?? 'unknown')
|
|
@@ -550,6 +556,24 @@ export class SillyHubMcpClient {
|
|
|
550
556
|
return { workerId, status };
|
|
551
557
|
}
|
|
552
558
|
|
|
559
|
+
/**
|
|
560
|
+
* daemon 在线状态(调 get_daemon_status tool——平台侧 2026-09-10 P1-3 新增,read scope、
|
|
561
|
+
* 无副作用,比 DB status 强:后者有 45s 假在线窗口,本工具吃 ws_hub 实时连接)。
|
|
562
|
+
* fail-open 三态:online=true/false 为明确判定;null = 未知(旧 backend 无此 tool /
|
|
563
|
+
* token 缺 read scope / 调用异常)——调用方不得据此判不可用。
|
|
564
|
+
* @returns {Promise<{online: boolean|null, raw: object|null}>}
|
|
565
|
+
*/
|
|
566
|
+
async getDaemonStatus() {
|
|
567
|
+
if (!this._configured) return { online: null, raw: null };
|
|
568
|
+
const result = await this._callTool('get_daemon_status', {}, { quiet: true });
|
|
569
|
+
if (result === null || result.isError) return { online: null, raw: null };
|
|
570
|
+
const value = this._parseToolReturnValue(result);
|
|
571
|
+
const online = value && typeof value === 'object' && typeof value.daemon_online === 'boolean'
|
|
572
|
+
? value.daemon_online
|
|
573
|
+
: null;
|
|
574
|
+
return { online, raw: value ?? null };
|
|
575
|
+
}
|
|
576
|
+
|
|
553
577
|
/**
|
|
554
578
|
* 列出 mission 下的 worker(调 list_workers tool)。
|
|
555
579
|
* @param {string} missionId
|
|
@@ -565,6 +589,37 @@ export class SillyHubMcpClient {
|
|
|
565
589
|
return [];
|
|
566
590
|
}
|
|
567
591
|
|
|
592
|
+
/**
|
|
593
|
+
* 取 worker 结果产物(调 get_worker_result tool,FR-04 终态回收链——worker completed 后
|
|
594
|
+
* 经此取 artifacts,此前该 tool 无 CLI 封装)。
|
|
595
|
+
*
|
|
596
|
+
* artifacts 原样透传不裁剪:平台实返形态([{kind, content_ref, id}…] 或演进形态)整组
|
|
597
|
+
* 透传——kind 匹配 / JSON 提取等语义解析归消费方(review-dispatch 的
|
|
598
|
+
* extractReviewFromArtifacts),本封装保持传输层纯度(task-02 constraints)。
|
|
599
|
+
* @param {object} p
|
|
600
|
+
* @param {string} p.missionId
|
|
601
|
+
* @param {string} p.workerId
|
|
602
|
+
* @returns {Promise<{workerId: string|null, status: string, artifacts: Array}>}
|
|
603
|
+
* 正常 → {workerId, status, artifacts} 原样解析(status 缺省 'unknown',artifacts 缺省 []);
|
|
604
|
+
* 未配置 / 调用失败(null) / tool isError → { workerId: null, status: 'unavailable', artifacts: [] }
|
|
605
|
+
* (与 dispatchWorker/getDaemonStatus 同族 best-effort,绝不向上抛)
|
|
606
|
+
*/
|
|
607
|
+
async getWorkerResult({ missionId, workerId } = {}) {
|
|
608
|
+
if (!this._configured) return { workerId: null, status: 'unavailable', artifacts: [] };
|
|
609
|
+
const result = await this._callTool('get_worker_result', { mission_id: missionId, worker_id: workerId });
|
|
610
|
+
if (result === null || result.isError) return { workerId: null, status: 'unavailable', artifacts: [] };
|
|
611
|
+
const value = this._parseToolReturnValue(result);
|
|
612
|
+
// workerId 兜底链与 dispatchWorker 同款(2026-09-10 平台实返 {id,…} 形态实证)
|
|
613
|
+
const wid = value && typeof value === 'object'
|
|
614
|
+
? (value.worker_id ?? value.workerId ?? value.id ?? null)
|
|
615
|
+
: null;
|
|
616
|
+
const status = value && typeof value === 'object'
|
|
617
|
+
? (value.status ?? 'unknown')
|
|
618
|
+
: 'unknown';
|
|
619
|
+
const artifacts = value && Array.isArray(value.artifacts) ? value.artifacts : [];
|
|
620
|
+
return { workerId: wid, status, artifacts };
|
|
621
|
+
}
|
|
622
|
+
|
|
568
623
|
/**
|
|
569
624
|
* 终止 worker lease 防双写(UB-6 超时 fallback 用)。
|
|
570
625
|
*
|
package/src/spec-sync.js
CHANGED
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
import { createHash } from 'crypto';
|
|
19
19
|
import { existsSync, readdirSync, readFileSync, statSync, mkdirSync, writeFileSync, unlinkSync } from 'fs';
|
|
20
20
|
import { join, relative, sep } from 'path';
|
|
21
|
+
import { bindSyncNoiseRoot, syncConnectionWarn, isConnectionClassStatus, noteSyncSuccess } from './sync-noise.js';
|
|
21
22
|
|
|
22
23
|
// 与 sillyhub-daemon/src/spec-sync.ts 共用排除口径(task-07 / FR-06 / D-008@v2):
|
|
23
24
|
// .runtime(有点)/ runtime(无点)/ projects(本地环境文件)在顶层排除;
|
|
@@ -457,6 +458,41 @@ function clearSpecConflictMarker(specRoot, changeName) {
|
|
|
457
458
|
} catch { /* 清理失败不影响同步结果 */ }
|
|
458
459
|
}
|
|
459
460
|
|
|
461
|
+
// follower/stale 提示的跨轮去重(坑 spec-sync-follow-banner-spam,2026-09-08 用户反馈③):
|
|
462
|
+
// 「N 个本地未改动文件自动跟随服务器」的信息只在集合**首次出现/变化**时有价值。基线快照锚定
|
|
463
|
+
// local-at-last-sync(见 writeBaseSnapshot 注释),本地落后于服务器的文件每轮都重新算出同样的
|
|
464
|
+
// follower 集合(computeSpecOps 生成 op → dropFollowServerUpdates 再丢掉),平台未就绪 POST
|
|
465
|
+
// 失败时基线还不落盘 → 同一集合每步重刷,实证 57 文件跟随逐条命令刷屏。marker 记上次已报集合,
|
|
466
|
+
// 未变则降 debugLog;集合清空时清 key(此后同集合再现算「变化」重新报)。
|
|
467
|
+
const FOLLOW_REPORT_MARKER = 'spec-sync-follow-reported.json';
|
|
468
|
+
|
|
469
|
+
/**
|
|
470
|
+
* 判定 follower/stale 集合相对上次已报告是否变化(变化时更新 marker)。
|
|
471
|
+
* @param {string} specRoot
|
|
472
|
+
* @param {'follow'|'stale'} key
|
|
473
|
+
* @param {string[]} list 本轮集合
|
|
474
|
+
* @returns {boolean} true=有变化应报告;false=与上次相同应静默
|
|
475
|
+
*/
|
|
476
|
+
export function isFollowerSetChanged(specRoot, key, list) {
|
|
477
|
+
const sorted = [...(list || [])].sort();
|
|
478
|
+
const p = join(specRoot, '.runtime', FOLLOW_REPORT_MARKER);
|
|
479
|
+
let cur = {};
|
|
480
|
+
try { cur = JSON.parse(readFileSync(p, 'utf8')) || {}; } catch { /* 无 marker = 首报 */ }
|
|
481
|
+
const prev = Array.isArray(cur[key]) ? cur[key] : [];
|
|
482
|
+
const same = prev.length === sorted.length && prev.every((v, i) => v === sorted[i]);
|
|
483
|
+
if (same) return false;
|
|
484
|
+
try {
|
|
485
|
+
if (sorted.length === 0) delete cur[key];
|
|
486
|
+
else cur[key] = sorted;
|
|
487
|
+
if (Object.keys(cur).length === 0) { try { unlinkSync(p) } catch {} }
|
|
488
|
+
else {
|
|
489
|
+
mkdirSync(join(specRoot, '.runtime'), { recursive: true });
|
|
490
|
+
writeFileSync(p, JSON.stringify(cur) + '\n', 'utf8');
|
|
491
|
+
}
|
|
492
|
+
} catch { /* marker 失败 = 退化为逐轮报告(只损失降噪) */ }
|
|
493
|
+
return sorted.length > 0;
|
|
494
|
+
}
|
|
495
|
+
|
|
460
496
|
/**
|
|
461
497
|
* 同步本地 .sillyspec 树到平台(CLI 直跑增量同步入口)。
|
|
462
498
|
*
|
|
@@ -470,6 +506,9 @@ export async function syncSpecTree(specRoot, platform, changeName, opts = {}) {
|
|
|
470
506
|
debugLog('[spec-sync] 未连接平台(本地合法状态);跳过 spec 树增量同步');
|
|
471
507
|
return { synced: 0 };
|
|
472
508
|
}
|
|
509
|
+
// 绑定连接类失败噪音闸(sync-noise.js):本目录所有 [spec-sync]/[sync] 失败 warn 的
|
|
510
|
+
// 跨进程静默窗口 marker 落这里(与 SyncManager 构造器绑定同目录)。
|
|
511
|
+
try { bindSyncNoiseRoot(join(specRoot, '.runtime')); } catch { /* 直通降级 */ }
|
|
473
512
|
// HUB-09:单请求超时与外部熔断 signal 合并——trigger* 熔断时在飞请求被真实取消
|
|
474
513
|
const withSignal = (timeoutMs) => (opts.signal ? AbortSignal.any([AbortSignal.timeout(timeoutMs), opts.signal]) : AbortSignal.timeout(timeoutMs));
|
|
475
514
|
|
|
@@ -488,13 +527,18 @@ export async function syncSpecTree(specRoot, platform, changeName, opts = {}) {
|
|
|
488
527
|
// SILLYSPEC_DEBUG_SYNC=1 才可见),文件迟到平台且无任何线索(multi-agent-platform
|
|
489
528
|
// 2026-08-18-workspace-file-browser 实证:design/decisions 迟到 27 分钟、plan.md 迟到
|
|
490
529
|
// 8 分钟才被后续步骤的同步补上)。失败可见、成功不打扰。
|
|
491
|
-
|
|
530
|
+
// 连接类失败(404=端点未就绪 / 5xx)走噪音闸(sync-noise.js):平台未就绪期每步
|
|
531
|
+
// 自动同步重刷同一行,淹没门禁输出;其余状态码(4xx 业务态)每轮该看见。
|
|
532
|
+
const line = `[spec-sync] 拉取清单失败 HTTP ${res.status}(文件树本次未同步,下次自动重试): ${changeName}`;
|
|
533
|
+
if (isConnectionClassStatus(res.status)) syncConnectionWarn(line);
|
|
534
|
+
else console.warn(line);
|
|
492
535
|
return { synced: 0 };
|
|
493
536
|
}
|
|
494
537
|
const body = await res.json().catch(() => ({}));
|
|
495
538
|
serverManifest = body.files || {};
|
|
496
539
|
} catch (err) {
|
|
497
|
-
|
|
540
|
+
const line = `[spec-sync] 拉取清单异常(文件树本次未同步,下次自动重试): ${changeName}: ${describeSyncError(err)}`;
|
|
541
|
+
syncConnectionWarn(line); // 网络/超时/熔断均连接类
|
|
498
542
|
return { synced: 0 };
|
|
499
543
|
}
|
|
500
544
|
|
|
@@ -513,13 +557,19 @@ export async function syncSpecTree(specRoot, platform, changeName, opts = {}) {
|
|
|
513
557
|
if (!opts.forcePush) {
|
|
514
558
|
const dropped = dropFollowServerUpdates(ops, localFiles, baseHashes);
|
|
515
559
|
if (dropped.followed.length > 0) {
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
)
|
|
560
|
+
// 集合未变不重报(坑 spec-sync-follow-banner-spam):本地落后集合每轮都重算出,
|
|
561
|
+
// 平台未就绪期同一批文件逐条命令刷屏。集合变化(新增漂移/清空后再现)才报。
|
|
562
|
+
if (isFollowerSetChanged(specRoot, 'follow', dropped.followed)) {
|
|
563
|
+
console.log(
|
|
564
|
+
`[spec-sync] ${dropped.followed.length} 个本地未改动文件自动跟随服务器(他端推进,内容未变不回推): ${dropped.followed.slice(0, 5).join(', ')}${dropped.followed.length > 5 ? ' 等' : ''}`
|
|
565
|
+
);
|
|
566
|
+
} else {
|
|
567
|
+
debugLog(`[spec-sync] ${dropped.followed.length} 个本地未改动文件自动跟随服务器(集合与上次相同,不重报)`);
|
|
568
|
+
}
|
|
519
569
|
}
|
|
520
570
|
ops = dropped.ops;
|
|
521
571
|
const filtered = filterStaleUpdates(ops, localFiles, lastSyncTs);
|
|
522
|
-
if (filtered.stale.length > 0) {
|
|
572
|
+
if (filtered.stale.length > 0 && isFollowerSetChanged(specRoot, 'stale', filtered.stale)) {
|
|
523
573
|
console.warn(
|
|
524
574
|
`[spec-sync] 拦下 ${filtered.stale.length} 个旧副本回推(本地自上次同步未改动而服务器已前进,如确需以本地为准:重存后重推): ${filtered.stale.slice(0, 5).join(', ')}${filtered.stale.length > 5 ? ' 等' : ''}`
|
|
525
575
|
);
|
|
@@ -544,7 +594,9 @@ export async function syncSpecTree(specRoot, platform, changeName, opts = {}) {
|
|
|
544
594
|
signal: withSignal(30000),
|
|
545
595
|
});
|
|
546
596
|
if (!res.ok) {
|
|
547
|
-
|
|
597
|
+
const line = `[spec-sync] 同步请求失败 HTTP ${res.status}(文件树本次未同步,下次自动重试): ${changeName}`;
|
|
598
|
+
if (isConnectionClassStatus(res.status)) syncConnectionWarn(line);
|
|
599
|
+
else console.warn(line);
|
|
548
600
|
return { synced: 0 };
|
|
549
601
|
}
|
|
550
602
|
const body = await res.json().catch(() => ({}));
|
|
@@ -586,6 +638,7 @@ export async function syncSpecTree(specRoot, platform, changeName, opts = {}) {
|
|
|
586
638
|
writeLastSyncTs(specRoot);
|
|
587
639
|
writeBaseSnapshot(specRoot, localFiles);
|
|
588
640
|
clearSpecConflictMarker(specRoot, changeName);
|
|
641
|
+
noteSyncSuccess();
|
|
589
642
|
console.log(`[spec-sync] 冲突已自动消解(0 个需人工裁决;本会话改动 ${ops.length - followers.length > 0 ? '已同步' : '无差异'}): ${changeName}`);
|
|
590
643
|
return { synced: Math.max(ops.length - followers.length, 0), conflict: false, autoResolved: followers.length };
|
|
591
644
|
}
|
|
@@ -614,6 +667,7 @@ export async function syncSpecTree(specRoot, platform, changeName, opts = {}) {
|
|
|
614
667
|
console.warn(`⚠️ 处置:sillyspec platform resolve ${changeName} --keep-local | --take-platform | --abort(冲突详情: ${conflictPath || '(写入失败)'})`);
|
|
615
668
|
return { synced: 0, conflict: true, serverVersions: real, autoResolved: followers.length, conflictPath };
|
|
616
669
|
}
|
|
670
|
+
noteSyncSuccess();
|
|
617
671
|
console.log(`[spec-sync] 已同步 ${ops.length} 个文件变更: ${changeName}`);
|
|
618
672
|
// 本轮无冲突 = 此前的未决 spec 树冲突已不存在,清残留标记(否则 status 永久红标、
|
|
619
673
|
// 横幅去重单行提示不消——坑 spec-sync-conflict-banner-spam)
|
|
@@ -622,7 +676,8 @@ export async function syncSpecTree(specRoot, platform, changeName, opts = {}) {
|
|
|
622
676
|
writeBaseSnapshot(specRoot, localFiles);
|
|
623
677
|
return { synced: ops.length };
|
|
624
678
|
} catch (err) {
|
|
625
|
-
|
|
679
|
+
const line = `[spec-sync] 同步异常(文件树本次未同步,下次自动重试): ${changeName}: ${describeSyncError(err)}`;
|
|
680
|
+
syncConnectionWarn(line); // 网络/超时/熔断均连接类
|
|
626
681
|
return { synced: 0 };
|
|
627
682
|
}
|
|
628
683
|
}
|
package/src/stage-contract.js
CHANGED
|
@@ -10,6 +10,8 @@ import { join, basename } from 'path'
|
|
|
10
10
|
import { safeGit } from './git-helper.js'
|
|
11
11
|
import { nowWallClock } from './datetime.js'
|
|
12
12
|
import { detectChangeRisk, checkIntegrationEvidence, VERIFICATION_NEEDS, RISK_LEVEL_CAUSES } from './change-risk-profile.js'
|
|
13
|
+
import { parseEvidenceSlots } from './verify-facts-schema.js'
|
|
14
|
+
import { IR_STRICT_SINCE } from './constants.js'
|
|
13
15
|
import { SCAN_REQUIRED_DOCS, AUXILIARY_STAGES } from './constants.js'
|
|
14
16
|
import { evaluateRules } from './stage-contract-engine.js'
|
|
15
17
|
import { getRule } from './stage-contract-spec.js'
|
|
@@ -457,6 +459,10 @@ function validatePlanOutputs(cwd, changeName, context = {}) {
|
|
|
457
459
|
* 标题放宽:含「结论/Conclusion/Result/结果」的二级标题均可(B3c),
|
|
458
460
|
* PASS/FAIL 可在标题行本身(如「## 验收结论:✅ PASS」)或紧邻标题的正文里。
|
|
459
461
|
* 历史教训:原正则锚定确切「## 结论」,用户写「## 验收结论:✅ PASS」不被识别。
|
|
462
|
+
* 2026-09-08 刀③起降级为 legacy 回退:首选结论枚举槽(extractVerifyConclusionSlot),
|
|
463
|
+
* 仅当文件完全没有槽行时才走本窗口扫描(存量变更兼容);槽存在即不走本函数——
|
|
464
|
+
* 骨架占位符 `<待填:三选一>` 不含枚举词,两种解析下都 fail-closed(修掉旧占位符
|
|
465
|
+
* `<待填:PASS 或 FAIL>` 被窗口正则误读成 PASS 的自通过缺陷)。
|
|
460
466
|
*/
|
|
461
467
|
function extractVerifyConclusion(verify) {
|
|
462
468
|
// 遍历所有含关键词的二级标题,取其 400 字符窗口内含 PASS/FAIL 的那个(坑
|
|
@@ -481,6 +487,54 @@ function extractVerifyConclusion(verify) {
|
|
|
481
487
|
return best || ''
|
|
482
488
|
}
|
|
483
489
|
|
|
490
|
+
/**
|
|
491
|
+
* 结论枚举槽提取(刀③,2026-09-08):verify-probes --init 骨架生成的固定槽行
|
|
492
|
+
* `结论枚举:`<枚举值>…``——解析只认该行的枚举词,标题措辞劫持面归零。
|
|
493
|
+
* @param {string} verify - verify-result.md 全文
|
|
494
|
+
* @returns {string|null} 枚举值(PASS / PASS WITH NOTES / FAIL);'' = 有槽未填(fail-closed);
|
|
495
|
+
* null = 文件无槽行(调用方走 legacy 窗口扫描)
|
|
496
|
+
*/
|
|
497
|
+
export function extractVerifyConclusionSlot(verify) {
|
|
498
|
+
if (!/^结论枚举:/im.test(verify)) return null
|
|
499
|
+
const m = verify.match(/^结论枚举:`?(PASS WITH NOTES|PASS|FAIL)\b/im)
|
|
500
|
+
// 交替序 PASS WITH NOTES 在前,防裸 PASS 抢先截断
|
|
501
|
+
return m ? m[1] : ''
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
/**
|
|
505
|
+
* 结论解析统一入口:槽优先,无槽走 legacy 窗口扫描并对存量格式发迁移提示。
|
|
506
|
+
* @param {string} verify - verify-result.md 全文
|
|
507
|
+
* @param {string[]} [warnings] - 传入时追加 legacy 迁移 warning
|
|
508
|
+
* @returns {string} 枚举值或 ''(未识别/未填)
|
|
509
|
+
*/
|
|
510
|
+
function resolveVerifyConclusion(verify, warnings) {
|
|
511
|
+
const slot = extractVerifyConclusionSlot(verify)
|
|
512
|
+
if (slot !== null) return slot
|
|
513
|
+
if (warnings) {
|
|
514
|
+
warnings.push(
|
|
515
|
+
'verify-result.md 结论未用「结论枚举:」槽行(旧自由格式,关键词窗口解析对标题措辞敏感——已有两次历史坑)。' +
|
|
516
|
+
'新变更请用 `sillyspec verify-probes --change <变更> --init` 骨架的槽格式;窗口解析仅为存量变更保留。'
|
|
517
|
+
)
|
|
518
|
+
}
|
|
519
|
+
return extractVerifyConclusion(verify)
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
/**
|
|
523
|
+
* IR 严格档判别(审核 P2-2):design.md frontmatter created_at ≥ IR_STRICT_SINCE。
|
|
524
|
+
* 与 verify-postcheck isStrictChange 同锚(常量单源 constants.js),此处只读 design
|
|
525
|
+
* 文件(不接 DB——validator 层保持纯文件判定,db 侧判别归 isStrictChange)。
|
|
526
|
+
*/
|
|
527
|
+
function isIrStrictVerifyChange(changeDir) {
|
|
528
|
+
try {
|
|
529
|
+
const dp = join(changeDir, 'design.md')
|
|
530
|
+
if (!existsSync(dp)) return false
|
|
531
|
+
const fm = readFileSync(dp, 'utf8').match(/^---\r?\n([\s\S]*?)\r?\n---/)
|
|
532
|
+
const cm = fm && fm[1].match(/^created_at:\s*(\d{4}-\d{2}-\d{2})/m)
|
|
533
|
+
if (!cm) return false
|
|
534
|
+
return cm[1] >= IR_STRICT_SINCE
|
|
535
|
+
} catch { return false }
|
|
536
|
+
}
|
|
537
|
+
|
|
484
538
|
function validateVerifyOutputs(cwd, changeName, context = {}) {
|
|
485
539
|
const { specRoot } = context
|
|
486
540
|
const changeDir = resolveChangeDir(cwd, changeName, specRoot)
|
|
@@ -509,7 +563,17 @@ function validateVerifyOutputs(cwd, changeName, context = {}) {
|
|
|
509
563
|
// ── FAIL 结论门控(适用于所有变更,不限风险等级)──
|
|
510
564
|
// verify-result.md 结论为 FAIL 时,verify 阶段不能 completed。
|
|
511
565
|
// 历史教训:CLI 曾不校验结论,AI 写 FAIL 后 verify 仍被标记完成并提示"验证通过可以归档"。
|
|
512
|
-
|
|
566
|
+
// 刀③起结论解析槽优先(extractVerifyConclusionSlot),一处解析两处消费。
|
|
567
|
+
const conclusionStr = resolveVerifyConclusion(verify, warnings)
|
|
568
|
+
// ── 严格档结论槽必在(2026-09-09 外部审核 P2-2):有槽 fail-closed,但整行删「结论枚举:」
|
|
569
|
+
// 曾可逃回 legacy 窗口正则蹭关键词。IR 严格档变更(design created_at ≥ IR_STRICT_SINCE)
|
|
570
|
+
// 无槽直接 ERROR——存量变更不受影响(legacy 回退保留)。──
|
|
571
|
+
if (!/^结论枚举:/im.test(verify)) {
|
|
572
|
+
const strict = isIrStrictVerifyChange(changeDir)
|
|
573
|
+
if (strict) {
|
|
574
|
+
errors.push('严格档变更(created_at ≥ ' + IR_STRICT_SINCE + ')verify-result.md 缺「结论枚举:」槽行——删槽回退关键词窗口的通道已关闭。修复:跑 `sillyspec verify-probes --change <变更名> --init` 补槽段(已有正文不覆盖),把 <待填:三选一> 替换为枚举值。')
|
|
575
|
+
}
|
|
576
|
+
}
|
|
513
577
|
if (conclusionStr === 'FAIL') {
|
|
514
578
|
errors.push(getRule('verify.conclusion.fail-gate').failMessage)
|
|
515
579
|
} else if (!conclusionStr) {
|
|
@@ -521,7 +585,7 @@ function validateVerifyOutputs(cwd, changeName, context = {}) {
|
|
|
521
585
|
designContent: readIfExists(join(changeDir, 'design.md')),
|
|
522
586
|
planContent: readIfExists(join(changeDir, 'plan.md')),
|
|
523
587
|
})
|
|
524
|
-
const conclusion =
|
|
588
|
+
const conclusion = conclusionStr // 槽优先解析结果复用(此前同输入重复扫两遍)
|
|
525
589
|
// ── 否定抑制审计(坑 risk-negation-blindness)──
|
|
526
590
|
// 关键词命中被同句否定语境抑制而不参与判级时必须透出:抑制是把双刃剑(「本次不新增
|
|
527
591
|
// daemon」真豁免 vs 靠堆否定措辞静默降级逃证据门控),这里让每次抑制可见可核对;
|
|
@@ -573,7 +637,23 @@ function validateVerifyOutputs(cwd, changeName, context = {}) {
|
|
|
573
637
|
}
|
|
574
638
|
}
|
|
575
639
|
} catch { /* 回执损坏按无回执处理(fail 回原字面匹配) */ }
|
|
576
|
-
|
|
640
|
+
// v2(2026-09-08-ir-verify-facts FR-04):回执槽优先——verify-result.md「## 集成验证回执」
|
|
641
|
+
// 槽在场时传结构化 runtimeEvidence 走一致性校验(绿判据),无槽降 legacy literals。
|
|
642
|
+
// verifyStartAt 由调用方经 context 传入(gates 接线侧 ProgressManager.getStageCompletedAt
|
|
643
|
+
// 同步算好——runValidators 引擎是同步的,此处不做 DB IO;缺省 → auditRuntimeReceipt
|
|
644
|
+
// fail-soft 跳过 mtime 核验)。
|
|
645
|
+
let runtimeEvidence = null
|
|
646
|
+
try {
|
|
647
|
+
const slotsEv = parseEvidenceSlots(verify)
|
|
648
|
+
if (slotsEv.hasReceiptSlot) runtimeEvidence = slotsEv.runtimeEvidence
|
|
649
|
+
} catch { /* 解析失败按无槽 legacy */ }
|
|
650
|
+
const evidenceCheck = checkIntegrationEvidence(verify, changeRiskProfile.requiredVerification, {
|
|
651
|
+
extraEvidenceText: receiptText,
|
|
652
|
+
...(runtimeEvidence ? { runtimeEvidence } : {}),
|
|
653
|
+
...(context.verifyStartAt ? { verifyStartAt: context.verifyStartAt } : {}),
|
|
654
|
+
cwd,
|
|
655
|
+
specBase: context.specRoot || join(cwd, '.sillyspec'),
|
|
656
|
+
})
|
|
577
657
|
if (!evidenceCheck.ok) {
|
|
578
658
|
// A: 报错说人话 —— 把「缺哪一项、要写/做什么、判级原因」逐条列出,
|
|
579
659
|
// 让 agent 不必靠改结论文案撞墙。detail 指明真实启动须是本变更实际改动的
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 三 stage 审查清单单一来源(2026-09-10-review-dispatch task-01 / FR-05 / D-001@1)
|
|
3
|
+
*
|
|
4
|
+
* 条目从 stages/{brainstorm,plan,execute}.js 的 prompt 文本逐字迁移(2026-09-10)——
|
|
5
|
+
* 文字/标点/全半角/markdown 记号零改动:下游 plan-postcheck、review-dispatch
|
|
6
|
+
* worker_prompt(task-03)等可能字面引用,改写即漂移。
|
|
7
|
+
* 一致性由 test/stage-review-checklist.test.mjs 钉死(内嵌迁移前快照逐条比对)。
|
|
8
|
+
*
|
|
9
|
+
* 条目为纯文本,不含序号/checkbox/缩进等结构性前缀(渲染层负责前缀形态):
|
|
10
|
+
* - brainstorm:前 3 条 =「交叉审查模型」三层检查(prompt 内渲染为「N. 」序号行),
|
|
11
|
+
* 后 6 条 =「交叉点抽取」(渲染为「- 」列表行);
|
|
12
|
+
* - plan:渲染为「- [ ] 」checkbox 行(审查清单段);
|
|
13
|
+
* - execute:渲染为 2 空格缩进的「N. 」序号行(QA 段「以下三项始终必查」)。
|
|
14
|
+
*/
|
|
15
|
+
export const REVIEW_CHECKLISTS = {
|
|
16
|
+
brainstorm: [
|
|
17
|
+
'**定义层**:模糊概念是否有可测试定义。例如"高可用""异常数据""本地缓存""重试"。',
|
|
18
|
+
'**一致性层**:跨章节/跨产物是否打架。例如数据流 vs 容错策略、schema vs 输入格式、非目标 vs tasks。',
|
|
19
|
+
'**可行性层**:关键假设是否有来源。例如 P99 延迟、上游 SLA、缓存 TTL、数据量、权限模型、兼容旧配置。',
|
|
20
|
+
'模块 A 依赖模块 B 的实体/状态/接口',
|
|
21
|
+
'requirements.md 的 FR 与 design.md 的数据模型/API/状态机',
|
|
22
|
+
'design.md 的容错策略与数据流、缓存、重试、回滚',
|
|
23
|
+
'tasks.md 的执行范围与 design.md 的非目标',
|
|
24
|
+
'decisions.md 的 D-xxx@vN 与 design.md 当前说法',
|
|
25
|
+
'scan/module docs 或源码中的真实约束与 design.md 假设',
|
|
26
|
+
],
|
|
27
|
+
plan: [
|
|
28
|
+
'task 编号与 Wave checkbox 格式正确,execute 依赖此格式解析任务',
|
|
29
|
+
'plan_level 档位与实际复杂度匹配(none/light/full 没选错)',
|
|
30
|
+
'跨任务契约:task-A 的产出(接口/DTO/响应)被 task-B 消费时,consumer 是否在 TaskCard expects_from 声明所需字段、provider 是否在 provides 承诺、两边字段一致?(plan-postcheck 会硬校验,此处是独立视角复查)',
|
|
31
|
+
'文件覆盖:design.md 文件变更清单中的每个源码文件,是否都被至少一个 task 的 allowed_paths 覆盖?(漏覆盖 = execute 必然漏改。跨仓变更对账口径:design 清单按「## <repo-key> 仓变更」分段、路径相对各仓根,task 卡 repo: + 同口径相对路径匹配——allowed_paths 带仓库名前缀或绝对路径 = 永不命中、对账不上)',
|
|
32
|
+
'不存在 P0/P1 unresolved blocker 残留',
|
|
33
|
+
'没有实现细节泄漏到 plan.md(接口签名/代码示例应在 tasks/task-NN.md)',
|
|
34
|
+
'关键路径与 Wave 依赖合理(无循环依赖、无遗漏前置)',
|
|
35
|
+
'Wave 分组说明:依赖方向违规(depends_on 同 Wave / 后置 Wave)在 --done 时会按拓扑**自动修复**(提案经一致性+文件面验证后落盘);共享文件的手工分 Wave 串行是合法安全模式,不会被自动改写',
|
|
36
|
+
'连带测试归属:本批改动是否会导致既有测试断言失效(改共享/被多 task 依赖源文件、改被测试精确匹配的值如 UI 文案/按钮文本/错误信息/常量/枚举字面量、改函数签名或返回结构等单文件场景)?此类 task 是否在 related_tests 声明了失效测试、且路径在 allowed_paths 内(或由独立测试 task 覆盖)?(漏声明 = execute 阶段测试债、主代理事后兜底)',
|
|
37
|
+
'acceptance 字段对照实际 schema/类型源文件核验存在性与形态,不凭 design.md 文字臆断(plan-postcheck best-effort grep 会给 allowed_paths 源文件未命中的 snake_case/camelCase 标识符提 warning,此处是语义层复查;臆断 = execute 阶段返工)',
|
|
38
|
+
],
|
|
39
|
+
execute: [
|
|
40
|
+
'跨 task 交界(A 产出的接口/数据结构与 B 的消费是否对得上)',
|
|
41
|
+
'design.md 整体对照(最终实现拼起来是否仍符合设计意图,而非仅各 task 局部合规)',
|
|
42
|
+
'组装行为(全量测试/构建/启动通过——单 task 测试全绿 ≠ 组装正确)',
|
|
43
|
+
],
|
|
44
|
+
}
|
package/src/stage-review.js
CHANGED
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
import { existsSync, readFileSync, mkdirSync, readdirSync, writeFileSync } from 'fs'
|
|
17
17
|
import { join } from 'path'
|
|
18
18
|
import { createHash } from 'crypto'
|
|
19
|
+
import jsYaml from 'js-yaml'
|
|
19
20
|
import { VALID_VERDICTS, REVIEW_SCHEMA_VERSION } from './task-review.js'
|
|
20
21
|
import { detectSpecDirTypo } from './spec-dir-typo.js'
|
|
21
22
|
import { resolveRuntimeRoot } from './run/shared.js'
|
|
@@ -31,6 +32,65 @@ const STAGE_MAIN_DOC = { brainstorm: 'design.md', plan: 'plan.md', execute: 'des
|
|
|
31
32
|
// stage → reviewType(对齐 gates.js Stage Review Gate 的映射)
|
|
32
33
|
const STAGE_REVIEW_TYPE = { brainstorm: 'design', plan: 'plan', execute: 'acceptance' }
|
|
33
34
|
|
|
35
|
+
// ── 独立审查通道优先序(2026-09-10 用户裁决:通道顺序是主观权衡,归用户配置不归 CLI 拍死)──
|
|
36
|
+
// 可选通道(tier=independent 时按序尝试首个可用):
|
|
37
|
+
// agent-tool 宿主 Agent tool 派子代理(现状默认首位)
|
|
38
|
+
// platform CLI 直发平台 worker(sillyspec review-dispatch,P2 落地——P1 指引标注「暂跳过」)
|
|
39
|
+
// host-mcp 宿主自有 MCP 派活(能派独立执行者的 MCP tool)
|
|
40
|
+
// self 降级自审(兜底;reviewerNotes 首行「降级:」+ reviewer.channel="self",gate ⚠️ 留痕)
|
|
41
|
+
const REVIEW_CHANNELS = ['agent-tool', 'platform', 'host-mcp', 'self']
|
|
42
|
+
/** 缺省序 = 现状行为(agent-tool 打头),未配置用户零行为变化。 */
|
|
43
|
+
export const DEFAULT_REVIEW_CHANNEL_PRIORITY = ['agent-tool', 'platform', 'host-mcp', 'self']
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* 读 local.yaml `review_dispatch.channel_priority`(best-effort 绝不抛)。
|
|
47
|
+
* 语义规则:未知通道名过滤(warn 一次);`self` 永远隐式垫底(防「全配置但全不可用 → 无路可走」);
|
|
48
|
+
* 空/缺段/解析失败 → 缺省序。配置只在**可用通道之间**起偏好作用——宿主没 Agent tool 时该通道
|
|
49
|
+
* 自然跳过(PI 上实际生效序退化为 platform → self),配的是偏好不是能力。
|
|
50
|
+
* @param {string} cwd 项目根(local.yaml 定位)
|
|
51
|
+
* @returns {string[]} 通道优先序(恒非空,末位恒为 'self')
|
|
52
|
+
*/
|
|
53
|
+
export function readReviewChannelPriority(cwd) {
|
|
54
|
+
let raw = null
|
|
55
|
+
try {
|
|
56
|
+
const p = join(cwd || process.cwd(), '.sillyspec', 'local.yaml')
|
|
57
|
+
if (existsSync(p)) raw = jsYaml.load(readFileSync(p, 'utf8'))
|
|
58
|
+
} catch { raw = null }
|
|
59
|
+
const rd = raw && typeof raw === 'object' && raw.review_dispatch && typeof raw.review_dispatch === 'object'
|
|
60
|
+
? raw.review_dispatch
|
|
61
|
+
: null
|
|
62
|
+
const arr = rd && Array.isArray(rd.channel_priority) ? rd.channel_priority : null
|
|
63
|
+
if (!arr || arr.length === 0) return DEFAULT_REVIEW_CHANNEL_PRIORITY.slice()
|
|
64
|
+
const picked = []
|
|
65
|
+
for (const v of arr) {
|
|
66
|
+
if (typeof v !== 'string') continue
|
|
67
|
+
const name = v.trim()
|
|
68
|
+
if (!REVIEW_CHANNELS.includes(name)) {
|
|
69
|
+
if (name) console.warn(`[stage-review] review_dispatch.channel_priority 未知通道「${name}」已忽略(可选:${REVIEW_CHANNELS.join(' / ')})`)
|
|
70
|
+
continue
|
|
71
|
+
}
|
|
72
|
+
if (name !== 'self' && !picked.includes(name)) picked.push(name)
|
|
73
|
+
}
|
|
74
|
+
if (picked.length === 0) return DEFAULT_REVIEW_CHANNEL_PRIORITY.slice()
|
|
75
|
+
return [...picked, 'self']
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* reviewer.channel 识别(gate 审计分支依据)。优先级:结构化 reviewer.channel(合法值)>
|
|
80
|
+
* reviewerNotes 首行「降级:」约定(2026-09-10 负面①降级条款的落款形态,向后兼容)>
|
|
81
|
+
* 'unspecified'(既无标记的存量 review——历史产物默认宿主子代理口径,gate 不特殊处理)。
|
|
82
|
+
* @param {object|null} review 解析后的 review.json
|
|
83
|
+
* @returns {string} 'agent-tool'|'platform'|'host-mcp'|'self'|'unspecified'
|
|
84
|
+
*/
|
|
85
|
+
export function classifyReviewerChannel(review) {
|
|
86
|
+
const ch = review && typeof review === 'object' && review.reviewer
|
|
87
|
+
&& typeof review.reviewer === 'object' && typeof review.reviewer.channel === 'string'
|
|
88
|
+
? review.reviewer.channel.trim()
|
|
89
|
+
: ''
|
|
90
|
+
if (REVIEW_CHANNELS.includes(ch)) return ch
|
|
91
|
+
return isDegradedSelfReview(review) ? 'self' : 'unspecified'
|
|
92
|
+
}
|
|
93
|
+
|
|
34
94
|
/**
|
|
35
95
|
* 渲染 review.json 产物契约(markdown)给 review 子代理事前看 —— schema 表 + 完整 JSON 示例 +
|
|
36
96
|
* docHash 算法 + 重算提示 + 位置。复用 validateStageReviewSchema 的同源常量,事前给的 == 事后查的。
|
|
@@ -38,10 +98,13 @@ const STAGE_REVIEW_TYPE = { brainstorm: 'design', plan: 'plan', execute: 'accept
|
|
|
38
98
|
* 历史教训:review 子代理靠读 stage-review.js 源码 + 翻模板才搞清 schema,常错(漏 schemaVersion /
|
|
39
99
|
* checklist 按层嵌套对象 / docHash 用主文档改版前的旧 sha256)。本函数把契约前置进 prompt。
|
|
40
100
|
*
|
|
41
|
-
* @param {{ stage?: string, changeDir?: string, reviewRunId?: string, tier?: string }} opts
|
|
101
|
+
* @param {{ stage?: string, changeDir?: string, reviewRunId?: string, tier?: string, channelPriority?: string[] }} opts
|
|
102
|
+
* channelPriority:通道优先序(readReviewChannelPriority 产物)。tier=independent 时渲染
|
|
103
|
+
* 「审查执行通道」段(按用户配置序,缺省现状序);缺省参数则函数自读(cwd 取 process.cwd,
|
|
104
|
+
* prompt.js/gates.js 等有精确 cwd 的调用方显式传入)。
|
|
42
105
|
* @returns {string} markdown 段;tier=self 返回简短提示(无需 review.json)
|
|
43
106
|
*/
|
|
44
|
-
export function renderReviewJsonContract({ stage, changeDir, reviewRunId, tier } = {}) {
|
|
107
|
+
export function renderReviewJsonContract({ stage, changeDir, reviewRunId, tier, channelPriority } = {}) {
|
|
45
108
|
if (tier === 'self') {
|
|
46
109
|
return '> review.json 契约:tier=self(此刻 design.md 变更文件数 ≤3,当前 agent 自审即可)。⚠️ 注意:本判定基于此刻 design.md 快照——Stage Review Gate 会以 --done 时刻的 design.md 重新判定,若 design 后续扩大到 >3 文件,tier 将升级为 independent 并硬要求独立审查子代理产出 review.json。以 gate 实际校验结果为准(FAILED 即 independent 需补 review.json),勿据本提示提前断言无需 review.json。'
|
|
47
110
|
}
|
|
@@ -50,7 +113,25 @@ export function renderReviewJsonContract({ stage, changeDir, reviewRunId, tier }
|
|
|
50
113
|
const mainDocRel = changeDir ? 'changes/<change>/' + mainDoc : mainDoc
|
|
51
114
|
const stageLbl = stage || '<stage>'
|
|
52
115
|
const runIdLbl = reviewRunId || '<reviewRunId>'
|
|
116
|
+
// 通道指引(2026-09-10 用户裁决:顺序归配置)。platform 通道 P2(review-dispatch 命令)落地前
|
|
117
|
+
// 标注暂跳过——指引不引用不存在的命令(stage prompt 铁律「不编造 CLI 子命令」)。
|
|
118
|
+
const priority = Array.isArray(channelPriority) && channelPriority.length > 0
|
|
119
|
+
? channelPriority
|
|
120
|
+
: readReviewChannelPriority(process.cwd())
|
|
121
|
+
const CHANNEL_DESC = {
|
|
122
|
+
'agent-tool': '宿主 Agent tool 派独立子代理(subagent_type: general,独立上下文)',
|
|
123
|
+
'platform': 'CLI 直发平台独立 worker(`sillyspec review-dispatch --change <变更名> --stage <stage>` 派发,`--status` 轮询回收——独立进程/会话,宿主无 Agent/MCP 亦可用)',
|
|
124
|
+
'host-mcp': '宿主自有 MCP 派独立执行者(能派活的 MCP tool,如平台 MCP 的 dispatch 类工具)',
|
|
125
|
+
'self': '降级自审:主代理切换审查者角色,reviewerNotes **首行**记「降级:环境无子代理可用」+ reviewer.channel="self"(gate 放行但留 ⚠️ 审计行),逐条结论附源码锚点补偿独立性',
|
|
126
|
+
}
|
|
127
|
+
const channelLines = priority.map((c, i) => `${i + 1}. **${c}** —— ${CHANNEL_DESC[c] || c}`).join('\n')
|
|
53
128
|
const L = [
|
|
129
|
+
'## 审查执行通道(tier=independent,按 local.yaml `review_dispatch.channel_priority` 优先序,**按序尝试首个可用通道**;顺序是用户配置的偏好,不是能力判定——某通道在当前宿主不可用就跳到下一个)',
|
|
130
|
+
'',
|
|
131
|
+
channelLines,
|
|
132
|
+
'',
|
|
133
|
+
'无论走哪个通道,review.json 落款 `reviewer: { "channel": "<通道名>", "missionId": null, "model": null }`(channel 用上方通道名;platform 通道填 missionId,可查模型时填 model)。这是审计面:gate 按 channel 留痕,self 触发 ⚠️——如实声明,勿伪装通道。',
|
|
134
|
+
'',
|
|
54
135
|
'## review.json 产物契约(CLI Stage Review Gate 将硬校验,以下为精确 schema —— 事前给的 == 事后查的)',
|
|
55
136
|
'',
|
|
56
137
|
'> 路径:`{SPEC_ROOT}/.runtime/stage-reviews/' + stageLbl + '-' + runIdLbl + '/review.json`(本次 reviewType=' + reviewType + ',主审查文档=' + mainDocRel + ')',
|
|
@@ -63,7 +144,8 @@ export function renderReviewJsonContract({ stage, changeDir, reviewRunId, tier }
|
|
|
63
144
|
'- `docHash`: reviewedFiles[0] 文件内容的 sha256(hex,见下方算法)',
|
|
64
145
|
'- `requiredEvidence`: 非空数组(specVerdict 或 qualityVerdict = cannot_verify 时必填,反逃逸)',
|
|
65
146
|
'- `checklist`(可选): 扁平数组 —— 每项 { item: string, result: ∈ { ' + CHECKLIST_RESULTS.join(' / ') + ' }, note?: string }。注意是**扁平数组**,不是按层(定义/一致/可行性)嵌套对象',
|
|
66
|
-
'- `
|
|
147
|
+
'- `reviewer`(可选,审计字段): { "channel": "agent-tool|platform|host-mcp|self", "missionId"?: string, "model"?: string } —— 审查执行通道落款(见上方「审查执行通道」段)',
|
|
148
|
+
'- `reviewerNotes`: 说明(verdict=fail 时写明阻断项;宿主环境无 Agent tool 降级自审时**首行**记「降级:环境无子代理可用」——gate 放行但留 ⚠️ 审计行,勿伪装子代理审查)',
|
|
67
149
|
'',
|
|
68
150
|
'### docHash(一键代填,勿手算)',
|
|
69
151
|
'- **推荐**:docHash 先占位(如 `"TODO"`),review.json 写完后跑',
|
|
@@ -89,6 +171,7 @@ export function renderReviewJsonContract({ stage, changeDir, reviewRunId, tier }
|
|
|
89
171
|
' "checklist": [',
|
|
90
172
|
' { "item": "<审查项>", "result": "pass", "note": "<证据/说明>" }',
|
|
91
173
|
' ],',
|
|
174
|
+
' "reviewer": { "channel": "<agent-tool|platform|host-mcp|self>", "missionId": null, "model": null },',
|
|
92
175
|
' "reviewerNotes": "<总结>"',
|
|
93
176
|
'}',
|
|
94
177
|
'```',
|
|
@@ -323,6 +406,21 @@ export function getLatestStageReviewRunId(runtimeRoot, stage, changeName) {
|
|
|
323
406
|
}
|
|
324
407
|
}
|
|
325
408
|
|
|
409
|
+
/**
|
|
410
|
+
* 降级自审检测(2026-09-10 用户反馈①:PI agent 等宿主环境无 Agent tool,tier=independent
|
|
411
|
+
* 硬要求子代理时只能降级自审——stage prompts 降级条款约定 reviewerNotes **首行**记
|
|
412
|
+
* 「降级:环境无子代理可用」)。gate 侧配套:检测到该标记 → 放行但留 ⚠️ 审计行——独立性折损
|
|
413
|
+
* 可见可追溯,与「宁可见的折损,不可静默的伪装」口径一致(静默放行会把降级 review 与真子代理
|
|
414
|
+
* review 混同,事后审计无法区分)。best-effort:无标记视为未降级(向后兼容,不构成新阻断)。
|
|
415
|
+
* @param {object|null} review - 解析后的 review.json(validateStageReview 的返回值 .review)
|
|
416
|
+
* @returns {boolean}
|
|
417
|
+
*/
|
|
418
|
+
export function isDegradedSelfReview(review) {
|
|
419
|
+
const notes = review && typeof review.reviewerNotes === 'string' ? review.reviewerNotes : ''
|
|
420
|
+
const firstLine = notes.split(/\r?\n/, 1)[0].trim()
|
|
421
|
+
return /^降级[::]/.test(firstLine)
|
|
422
|
+
}
|
|
423
|
+
|
|
326
424
|
/**
|
|
327
425
|
* stage-level review 总校验(brainstorm/plan/execute-acceptance 的 done gate)
|
|
328
426
|
*
|
|
@@ -356,7 +454,7 @@ export function validateStageReview(opts) {
|
|
|
356
454
|
const reviewPath = join(reviewDir, 'review.json')
|
|
357
455
|
|
|
358
456
|
if (!reviewRunId || !existsSync(reviewPath)) {
|
|
359
|
-
const errs = [`缺少 ${stage} 阶段的 stage review.json — tier=independent 要求独立审查子代理产出(期望路径:${reviewPath}
|
|
457
|
+
const errs = [`缺少 ${stage} 阶段的 stage review.json — tier=independent 要求独立审查子代理产出(期望路径:${reviewPath});宿主环境无 Agent tool(如 PI agent)时按 prompt 降级条款由当前 agent 自审产出,reviewerNotes 首行记「降级:环境无子代理可用」`]
|
|
360
458
|
const typo = detectSpecDirTypo(runtimeRoot)
|
|
361
459
|
if (typo) errs.push(`💡 路径疑似拼错:发现 ${typo.typoDir} 目录(应为 ${typo.canonical}),review.json 可能误存于此——检查是否把 .sillyspec 拼成了变体`)
|
|
362
460
|
return {
|
|
@@ -497,6 +595,25 @@ export function printStageReviewResult(result, context = {}) {
|
|
|
497
595
|
console.error(` ${reviewDir}/review.json`)
|
|
498
596
|
}
|
|
499
597
|
console.error(`\n 提示:tier=independent 要求独立审查子代理产出 review.json,补全后重新 --done`)
|
|
598
|
+
console.error(` 宿主环境无 Agent tool(如 PI agent)→ 降级条款:当前 agent 切审查者角色自审产出,reviewerNotes 首行记「降级:环境无子代理可用」(gate 放行但留 ⚠️ 审计行),勿伪装子代理审查`)
|
|
599
|
+
// 在途区分(2026-09-10-review-dispatch FR-06):review-dispatch 已派发平台审查但 worker 未终态时,
|
|
600
|
+
// 提示区分「在途」而非只有笼统缺件——agent 看得懂该等(--status 回收)还是该处置(--kill 后降级)。
|
|
601
|
+
// 内联读在途记录(同步 readFileSync,不经 import——review-dispatch.js 静态 import 本模块,反向
|
|
602
|
+
// import 成环;路径契约 = review-dispatch.js dispatchRecordPath:runtimeRoot/review-dispatch-<change>.json)。
|
|
603
|
+
// fail-open:无 changeName / 无记录 / 读失败 → 零输出(旧口径)。
|
|
604
|
+
if (changeName && runtimeRoot) {
|
|
605
|
+
try {
|
|
606
|
+
const recPath = join(runtimeRoot, `review-dispatch-${changeName}.json`)
|
|
607
|
+
if (existsSync(recPath)) {
|
|
608
|
+
const rec = JSON.parse(readFileSync(recPath, 'utf8'))
|
|
609
|
+
if (rec && typeof rec === 'object' && (rec.state === 'in-flight' || rec.state === 'dispatching')) {
|
|
610
|
+
console.error(`\n 🛰️ 平台审查在途:mission ${rec.missionId}(state ${rec.lastState || rec.state},最近进展 ${rec.lastStateAt ? new Date(rec.lastStateAt).toLocaleString('zh-CN') : '未知'})`)
|
|
611
|
+
console.error(` → 先 sillyspec review-dispatch --status --change ${changeName} 轮询回收(worker completed 后自动落盘 review.json);`)
|
|
612
|
+
console.error(` → 或 sillyspec review-dispatch --kill --change ${changeName} 显式放弃后走通道降级。`)
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
} catch { /* fail-open */ }
|
|
616
|
+
}
|
|
500
617
|
console.error(` 可用 sillyspec register-stage-review --change <名> --stage ${stage} [--from <已有review.json>] 一步生成 run 目录 + review.json 骨架(docHash 自动算)+ 写 marker + 自检,省掉手动建目录/写 marker`)
|
|
501
618
|
// docHash 失配精确指路(2026-08-21 agent-手工产出审计项②):改版后忘重算是最高频失败,
|
|
502
619
|
// 一键重算命令带上真实 change 名,agent 可直接照抄执行(--refresh-hash 保 verdict)。
|
|
@@ -623,6 +740,7 @@ export function registerStageReview({ changeName, stage, fromFile, cwd, platform
|
|
|
623
740
|
reviewedFiles,
|
|
624
741
|
docHash,
|
|
625
742
|
requiredEvidence: [`待独立审查子代理对照 ${mainDoc} 逐节核验(骨架由 register-stage-review 生成)`],
|
|
743
|
+
reviewer: { channel: null, missionId: null, model: null },
|
|
626
744
|
reviewerNotes: '骨架由 register-stage-review 生成,verdict 待独立审查子代理填写',
|
|
627
745
|
}
|
|
628
746
|
mode = 'skeleton'
|
|
@@ -633,6 +751,9 @@ export function registerStageReview({ changeName, stage, fromFile, cwd, platform
|
|
|
633
751
|
const reviewPath = join(reviewDir, 'review.json')
|
|
634
752
|
mkdirSync(reviewDir, { recursive: true })
|
|
635
753
|
writeFileSync(reviewPath, JSON.stringify(review, null, 2) + '\n')
|
|
754
|
+
// stage-reviews 回收不在此处(ql-20260908-005 已定分野):变更归属类证据走归档时精确
|
|
755
|
+
// 回收(pruneArchivedChangeRuntime 按 reviewedFiles 归属);写入侧滚动是启发式,
|
|
756
|
+
// 活跃变更的 review 可能因他端高频写而跌出 keep 窗,不进证据类目录。
|
|
636
757
|
|
|
637
758
|
const markerPath = stageReviewMarkerPath(runtimeRoot, stage, changeName)
|
|
638
759
|
if (existsSync(markerPath)) {
|