@joekytc/dsh-swarm 0.3.9 → 0.3.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.
- package/lib/config.d.ts +1 -1
- package/lib/dispatcher/agent-runner.js +32 -1
- package/lib/dispatcher/merge-gate.d.ts +1 -0
- package/lib/dispatcher/merge-gate.js +28 -1
- package/lib/dispatcher/session-events.d.ts +9 -1
- package/lib/dispatcher/session-events.js +14 -2
- package/lib/dispatcher/v-orchestrator.js +9 -5
- package/lib/domain/ocr-review.d.ts +5 -1
- package/lib/domain/ocr-review.js +10 -2
- package/lib/domain/planning-checklist.d.ts +30 -0
- package/lib/domain/planning-checklist.js +171 -1
- package/lib/roles/toolsets.d.ts +1 -1
- package/lib/roles/toolsets.js +7 -5
- package/lib/routes/planning-driver.d.ts +2 -1
- package/lib/routes/planning-driver.js +13 -10
- package/lib/services/ocr-cli.d.ts +5 -0
- package/lib/services/ocr-cli.js +10 -1
- package/lib/tools/main-session-tools.d.ts +1 -0
- package/lib/tools/main-session-tools.js +66 -7
- package/lib/tools/ocr-review-tools.d.ts +3 -1
- package/lib/tools/ocr-review-tools.js +23 -5
- package/lib/tools/planning-tools.d.ts +3 -0
- package/lib/tools/planning-tools.js +161 -2
- package/lib/tools/wiki-tools.js +1 -1
- package/lib/wiki/page-path.d.ts +2 -1
- package/lib/wiki/page-path.js +6 -4
- package/package.json +1 -1
- package/personas/kanban-dt/agent.cordis.yml +4 -1
- package/personas/persona-dt.md +2 -1
- package/skills/grill-me/SKILL.md +7 -0
package/lib/config.d.ts
CHANGED
|
@@ -95,7 +95,7 @@ export interface KanbanConfig {
|
|
|
95
95
|
* 打的补丁;2026-09-17 起 preset 判定统一走 session-preset.ts(引擎真相优先),此处
|
|
96
96
|
* 'ptc' 语义变为「显式放行普通 PTC 模式会话」(写面仍由 wiki_write 路径白名单硬约束)。
|
|
97
97
|
* 宿主 preset 体系演化/部署自定义模式 id 时改配置即可,无需改代码。
|
|
98
|
-
* 写面仍由 wiki_write 工具内 assertAllowedWikiPagePath
|
|
98
|
+
* 写面仍由 wiki_write 工具内 assertAllowedWikiPagePath 六类命名空间白名单硬约束。 */
|
|
99
99
|
wikiWritePresets: string[];
|
|
100
100
|
}
|
|
101
101
|
export declare const Config: Schema<KanbanConfig>;
|
|
@@ -7,7 +7,7 @@ import { ensureLocalKbRoot } from '../wiki/local-kb.js';
|
|
|
7
7
|
import { installCleanFsTools } from '../roles/clean-fs-tools.js';
|
|
8
8
|
import { mountOrRecompose } from '../roles/preset-installer.js';
|
|
9
9
|
import { buildModelCandidates, isModelUnavailableError } from './model-candidates.js';
|
|
10
|
-
import { toolName } from './session-events.js';
|
|
10
|
+
import { toolName, isGuardSynthesizedReply } from './session-events.js';
|
|
11
11
|
import { attachSessionToWorkspace, resolveOrCreateWorkspace } from './workspace-attach.js';
|
|
12
12
|
import { isPathInside, resolveTargetRepoDir } from './target-repo.js';
|
|
13
13
|
import { injectGitCredentials, resolveGitPatFromCtx } from './git-credentials.js';
|
|
@@ -83,6 +83,18 @@ export class AgentRunner {
|
|
|
83
83
|
if (task.body)
|
|
84
84
|
parts.push(`## Body
|
|
85
85
|
${task.body}`);
|
|
86
|
+
// 评审引擎(配置事实):reviewEngine.mode 决定 ocr_review 走托管还是委托。此前全链无注入点,
|
|
87
|
+
// 模型只能猜——真实事故(2026-09-23):面板配 managed 且已 wire,DT 仍按委托跑,并引用旧链
|
|
88
|
+
// 交接里「托管未配置→回落委托」的过期叙述当现状。本节热读配置,是运行时唯一权威来源。
|
|
89
|
+
if (task.assignee === 'dt') {
|
|
90
|
+
const re = this.configProvider.getEffective().reviewEngine;
|
|
91
|
+
const mode = re?.mode ?? 'delegate';
|
|
92
|
+
const provider = re?.managed?.provider ?? '';
|
|
93
|
+
const model = re?.managed?.model ?? '';
|
|
94
|
+
parts.push('## 评审引擎(配置事实,唯一权威;禁止从历史交接推断)', `- reviewEngine.mode = ${mode}${provider && model ? `(managed: provider=${provider} model=${model})` : ''}`, mode === 'managed'
|
|
95
|
+
? "- 托管:调 ocr_review{sub:'managed', from:<TARGET_BRANCH>, to:<branch>} 一次取归一化 findings;仅当工具返回「托管未配置」指引或 status !== 'completed' 时才静默改走委托(preview → rule → git diff 自评)"
|
|
96
|
+
: "- 委托:ocr_review{sub:'preview'} → ocr_review{sub:'rule'} → 自行 git diff 逐文件评审", '- 历史 handoff / 旧链交接里的「托管未配置 / 回落委托」等叙述属过期事实,不得当现状。');
|
|
97
|
+
}
|
|
86
98
|
// W local KB 模式:注入本地库根 + llm-wiki 用法 + 排序原则 + 交付约定(remote 模式不注入)。
|
|
87
99
|
// 库内复制/移动补句:DIRECT cp/mv/mkdir 工具永远匹配不到 KB 目标(无 path/file_path
|
|
88
100
|
// 实参)会被护栏拒绝——必须显式指引 W 用 write 工具做库内复制/移动。
|
|
@@ -443,6 +455,25 @@ ${task.body}`);
|
|
|
443
455
|
console.error('[dsh-swarm][debug] runner followup sent ' + taskId);
|
|
444
456
|
await agent.whenIdle();
|
|
445
457
|
console.error('[dsh-swarm][debug] runner whenIdle resolved ' + taskId);
|
|
458
|
+
// 网关合成拒答兜底(2026-09-22):jz 网关语义缓存(from-cache)/ 安全护栏(from-security-guard)
|
|
459
|
+
// 会对首轮任务 prompt 合成 usage 全 0 的拒答,角色会话拿拒答当结论空闲退出
|
|
460
|
+
// (protocol_violation 源头之一,销服一体 W2 卡会话 5 例实证)。检测到即自动
|
|
461
|
+
// followup「继续未完成的任务」重试一次(限一次,防护栏连续拦截打穿重派链);
|
|
462
|
+
// 重试后再拦则照旧走终态判据(running → protocol_violation 显形)。
|
|
463
|
+
// 读取容错对齐 session-events.ts 约定(snapshotEvents → events → []);
|
|
464
|
+
// seq > eventsBase 过滤本轮增量(防 resume 时历史 from-cache 标记误触发;fake/宿主
|
|
465
|
+
// 变体无 seq 时 Number→NaN,比较恒 false → 检测跳过,不误伤)。
|
|
466
|
+
const sessionLike = agent.session;
|
|
467
|
+
const turnEvents = typeof sessionLike.snapshotEvents === 'function'
|
|
468
|
+
? sessionLike.snapshotEvents(eventsBase)
|
|
469
|
+
: (sessionLike.events ?? []);
|
|
470
|
+
if (turnEvents.filter((e) => (Number(e?.seq) || 0) > eventsBase).some(isGuardSynthesizedReply)) {
|
|
471
|
+
console.error('[dsh-swarm][debug] runner guard-synthesized reply detected (from-cache/from-security-guard) ' + taskId + ' — auto followup retry once');
|
|
472
|
+
agent.followup({ content: [{ type: 'text', text: '继续未完成的任务:上一条回复是网关安全护栏/语义缓存误拦的合成拒答,并非你的真实结论。请忽略它,从中断处继续执行任务,直至按协议收敛终态(kanban_complete 或 kanban_block)。' }], source: { kind: 'user' } });
|
|
473
|
+
console.error('[dsh-swarm][debug] runner guard-retry followup sent ' + taskId);
|
|
474
|
+
await agent.whenIdle();
|
|
475
|
+
console.error('[dsh-swarm][debug] runner guard-retry whenIdle resolved ' + taskId);
|
|
476
|
+
}
|
|
446
477
|
// 终态判据(session 10 事故修复):whenIdle resolve 后一次 snapshot,只看任务真实状态——
|
|
447
478
|
// 不再扫 session.events 全历史工具名(旧 incarnation 的 kanban_* 事件会污染判据,
|
|
448
479
|
// 「历史上调过工具名」≠「本轮成功提交」,骗过检查后卡永驻 running)。
|
|
@@ -20,7 +20,8 @@ export function resolveMergeInput(dTask, state, fallbackRepo) {
|
|
|
20
20
|
const featureBranch = String(state.handoffs.get(dTask.id)?.metadata?.['branch'] ?? '').trim();
|
|
21
21
|
if (!targetBranch || !featureBranch)
|
|
22
22
|
return null;
|
|
23
|
-
|
|
23
|
+
const greenfield = /GREENFIELD\s*=\s*1/.test(dTask.body ?? '');
|
|
24
|
+
return { repoDir, targetBranch, featureBranch, greenfield };
|
|
24
25
|
}
|
|
25
26
|
/** 幂等判定:已存在 [merge-done] 评论,或 feature 分支已是 target 祖先(git merge-base --is-ancestor exit 0)。 */
|
|
26
27
|
export function isAlreadyMerged(state, dTaskId, repoDir, targetBranch, featureBranch, git) {
|
|
@@ -46,6 +47,32 @@ export async function runMergeGate(kanban, state, dTask, input, git = realGitRun
|
|
|
46
47
|
const { repoDir, targetBranch, featureBranch } = input;
|
|
47
48
|
if (isAlreadyMerged(state, dTask.id, repoDir, targetBranch, featureBranch, git))
|
|
48
49
|
return 'skipped';
|
|
50
|
+
if (input.greenfield) {
|
|
51
|
+
// 首合入建基线:target 分支不存在(rev-parse --verify 非零)→ 以 feature HEAD 建基线分支
|
|
52
|
+
let targetExists = true;
|
|
53
|
+
try {
|
|
54
|
+
git(['rev-parse', '--verify', targetBranch], repoDir);
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
targetExists = false;
|
|
58
|
+
}
|
|
59
|
+
if (!targetExists) {
|
|
60
|
+
try {
|
|
61
|
+
// 免 checkout 建分支:D 在 linked git worktree 中 feature 分支已检出,checkout 会硬失败;
|
|
62
|
+
// branch 自 featureBranch 指针对齐(linked-worktree 与原地场景均可用)。
|
|
63
|
+
git(['branch', targetBranch, featureBranch], repoDir);
|
|
64
|
+
git(['push', '-u', 'origin', targetBranch], repoDir);
|
|
65
|
+
const hash = git(['rev-parse', featureBranch], repoDir);
|
|
66
|
+
await kanban.comment(dTask.id, `${MERGE_DONE_PREFIX} greenfield baseline ${targetBranch} ← ${featureBranch} hash=${hash}`, 'system');
|
|
67
|
+
return 'merged';
|
|
68
|
+
}
|
|
69
|
+
catch (err) {
|
|
70
|
+
await kanban.comment(dTask.id, `${MERGE_FAILED_PREFIX} greenfield baseline ${featureBranch} → ${targetBranch} failed: ${String(err)}`, 'system');
|
|
71
|
+
return 'failed';
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
// target 已存在(后续卡)→ 落回既有 checkout/merge 路径
|
|
75
|
+
}
|
|
49
76
|
try {
|
|
50
77
|
git(['checkout', targetBranch], repoDir);
|
|
51
78
|
git(['merge', '--no-ff', featureBranch, '-m', `[AI-GEN] merge ${featureBranch} into ${targetBranch} after DT pass`], repoDir);
|
|
@@ -20,5 +20,13 @@ export declare function toolName(e: unknown): string | undefined;
|
|
|
20
20
|
export declare function toolArgs(e: unknown): Record<string, unknown>;
|
|
21
21
|
/** 取 assistant 消息的网关缓存回放标记(replayState.response.responseModel,如 'from-cache')。
|
|
22
22
|
* 非 assistant/message 或无标记返回 null。2026-09-04:远程网关回复缓存整包回放时由 SSE
|
|
23
|
-
* id/model 字段透传(dsh
|
|
23
|
+
* id/model 字段透传(dsh 纯透传),插件侧据此识别缓存劫持零产出。兼容落盘(data.message)
|
|
24
|
+
* 与 live 内存(顶层 message)两种形态。 */
|
|
24
25
|
export declare function replayModel(e: unknown): string | null;
|
|
26
|
+
/** 网关合成拒答/缓存回放标记集(usage 全 0,非真实模型输出):
|
|
27
|
+
* - from-cache:Higress 语义缓存命中(网关侧可用请求头 x-higress-skip-ai-cache: on 跳过,2026-09-22 网关团队确认)
|
|
28
|
+
* - from-security-guard:安全护栏合成拒答(销服一体 W2 卡会话 5 例实证,responseId 可供网关审计)
|
|
29
|
+
* 角色会话拿到合成文本会当结论空闲退出(protocol_violation 源头之一),必须识别。 */
|
|
30
|
+
export declare const GUARD_SYNTH_REPLAY_MODELS: ReadonlySet<string>;
|
|
31
|
+
/** 该事件是否为网关合成拒答/缓存回放的 assistant 消息(判定与标记集单一事实源)。 */
|
|
32
|
+
export declare function isGuardSynthesizedReply(e: unknown): boolean;
|
|
@@ -37,9 +37,21 @@ export function toolArgs(e) {
|
|
|
37
37
|
}
|
|
38
38
|
/** 取 assistant 消息的网关缓存回放标记(replayState.response.responseModel,如 'from-cache')。
|
|
39
39
|
* 非 assistant/message 或无标记返回 null。2026-09-04:远程网关回复缓存整包回放时由 SSE
|
|
40
|
-
* id/model 字段透传(dsh
|
|
40
|
+
* id/model 字段透传(dsh 纯透传),插件侧据此识别缓存劫持零产出。兼容落盘(data.message)
|
|
41
|
+
* 与 live 内存(顶层 message)两种形态。 */
|
|
41
42
|
export function replayModel(e) {
|
|
42
43
|
const rec = e;
|
|
43
|
-
const m = rec?.data?.message?.source?.replayState?.response?.responseModel
|
|
44
|
+
const m = rec?.data?.message?.source?.replayState?.response?.responseModel
|
|
45
|
+
?? rec?.message?.source?.replayState?.response?.responseModel;
|
|
44
46
|
return typeof m === 'string' ? m : null;
|
|
45
47
|
}
|
|
48
|
+
/** 网关合成拒答/缓存回放标记集(usage 全 0,非真实模型输出):
|
|
49
|
+
* - from-cache:Higress 语义缓存命中(网关侧可用请求头 x-higress-skip-ai-cache: on 跳过,2026-09-22 网关团队确认)
|
|
50
|
+
* - from-security-guard:安全护栏合成拒答(销服一体 W2 卡会话 5 例实证,responseId 可供网关审计)
|
|
51
|
+
* 角色会话拿到合成文本会当结论空闲退出(protocol_violation 源头之一),必须识别。 */
|
|
52
|
+
export const GUARD_SYNTH_REPLAY_MODELS = new Set(['from-cache', 'from-security-guard']);
|
|
53
|
+
/** 该事件是否为网关合成拒答/缓存回放的 assistant 消息(判定与标记集单一事实源)。 */
|
|
54
|
+
export function isGuardSynthesizedReply(e) {
|
|
55
|
+
const m = replayModel(e);
|
|
56
|
+
return m !== null && GUARD_SYNTH_REPLAY_MODELS.has(m);
|
|
57
|
+
}
|
|
@@ -25,6 +25,7 @@ export const PHASE_INSTRUCTIONS = {
|
|
|
25
25
|
'## P 阶段任务体要求(计划者,非执行者)',
|
|
26
26
|
'body 写入规划指令:读规格卡(含 file-prefetch/kb 附件=需求澄清清单)→ 产出 openspec 实施计划。仓库事实不足时先只读自查仓库代码实证(fs/search/grep 均可),再产出计划。',
|
|
27
27
|
'产物路径(OpenSpec 规范):写入目标仓库 `openspec/changes/<change_name>/` 下 proposal.md / design.md / tasks.md(change_name 依规格自拟 kebab-case,如 autoNote-tab);complete 时 metadata.artifacts_path = 该目录绝对路径。',
|
|
28
|
+
'greenfield(清单 greenfield:true):openspec 产物仍写目标目录 `openspec/changes/<change_name>/`(目录可先不存在,空目录可写);P 不 git init——建仓属 D 职责,P 保持只读。',
|
|
28
29
|
'tasks.md 拆分粒度(结构准备度,与 PT 同一判据):每个任务条目必须有单一、可独立核对的完成判据;禁止一条任务/一条测试用例混 N≥2 个互不依赖的可观察结果——按可独立验证的行为逐条拆分。完成判据必须是实施完成后的单一终态;不写执行层阶段态判据(RED/GREEN/REFACTOR、"先失败再通过"属 D 阶段执行协议,见 D 阶段模板)。',
|
|
29
30
|
'proposal.md 必须含「上游协议遵循说明」节:逐条列规格卡/澄清清单声明的工程协议引用(可定位出处:路径/明确协议名)+ 计划如何遵守;未声明任何协议则该节写「无」。禁止引用规格卡之外的协议标准。',
|
|
30
31
|
'需求对齐(颗粒度):澄清清单/规格卡的任务项、任务点、注意点 ↔ 计划任务逐条双向映射——不遗漏;超纲任务须能溯源到澄清回答。',
|
|
@@ -53,6 +54,7 @@ export const PHASE_INSTRUCTIONS = {
|
|
|
53
54
|
'## D 阶段任务体要求(执行者,唯一,非只读对齐/校验)',
|
|
54
55
|
'body 写入执行指令:先读父任务交接(W2)里的 page_path/kb_url,用 wiki_read 读取 openspec 实施计划原文,再按计划执行规格卡 solution/testing —— git worktree/branch → 改代码/README → git commit → git push(仅 feature 分支,可选)→ 自检(跑测试/构建)并附产物证据(changed_files/commit_hash)。',
|
|
55
56
|
'body 第一行以 TARGET_REPO=<真实仓库绝对路径> 声明目标仓库:必须取自规格卡 file-prefetch 附件 ref,禁止写 kanban 存储目录、禁止猜测回退。',
|
|
57
|
+
'greenfield(清单 greenfield:true 声明):TARGET_REPO 取清单 manifest.repo.localPath(无 file-prefetch 附件 ref 时);body 额外声明 GREENFIELD=1(merge-gate 首合入建基线用)。首步建仓前置:mkdir -p 目标目录 → git init -b main → 写 .gitignore/README → git add + commit 初始提交(tdd 声明用 { skipped: { reason: "greenfield scaffold" } })→ 再按既有 git worktree/branch → 改代码 → commit → push(feature) 流程。;若目标目录已有 .git(声明与事实不符)→ 跳过建仓前置,直接走既有 worktree 流程,并在 complete 摘要注明声明与事实不符',
|
|
56
58
|
'body 同时声明 TARGET_BRANCH=<目标分支名>(来自规格卡/用户声明):D 在 worktree feature 分支完成实现并验证后即 complete,禁止合并回 TARGET_BRANCH、禁止推 TARGET_BRANCH——合入由 DT 通过后 system 统一执行。',
|
|
57
59
|
'complete 时 metadata 必须带 branch=<feature 分支名>(DT 评审与 system 合入定位该分支用);git 证据 changed_files + commit_hash 必须(push 可选,可推 feature 分支)。',
|
|
58
60
|
'complete 时 metadata 建议携带 worktree_dir=<你的 feature worktree 绝对路径>:系统将在该目录实测 tdd.test_files(npx vitest run);缺失则跳过实测(仅声明校验)。',
|
|
@@ -62,7 +64,7 @@ export const PHASE_INSTRUCTIONS = {
|
|
|
62
64
|
].join('\n'),
|
|
63
65
|
dt: [
|
|
64
66
|
'## DT 阶段任务体要求(实现校验+评审,只读护栏)',
|
|
65
|
-
'body 写入实现校验指令:对 D 产物实证校验(test/build/typecheck/diff/git 证据 + open-code-review 评审),输出 verdict+issues 入交接 metadata.review_evidence。评审目标为 D 交接 metadata.branch 指向的 feature 分支,而非 TARGET_BRANCH。评审引擎:用 ocr_review 工具按配置引擎评审——委托 = preview→rule→git diff 自评;托管 = managed 一次出 findings;托管未配置时按工具指引静默改走委托;ocr 未安装则 kanban_block(\'review-tool-unavailable\') 并注明可在 GUI 配置面板安装。',
|
|
67
|
+
'body 写入实现校验指令:对 D 产物实证校验(test/build/typecheck/diff/git 证据 + open-code-review 评审),输出 verdict+issues 入交接 metadata.review_evidence。评审目标为 D 交接 metadata.branch 指向的 feature 分支,而非 TARGET_BRANCH。评审引擎:用 ocr_review 工具按配置引擎评审——委托 = preview→rule→git diff 自评;托管 = managed 一次出 findings;托管未配置时按工具指引静默改走委托;ocr 未安装则 kanban_block(\'review-tool-unavailable\') 并注明可在 GUI 配置面板安装。ocr_review 的 repo 参数缺省 = 会话工作目录;评审目标不在其内(D 的 worktree / 其他仓库)时须显式传仓库绝对路径(取 D 交接 metadata.worktree_dir 或规格卡 file-prefetch 附件 ref)。',
|
|
66
68
|
'铁律:DT 是只读校验+评审角色,绝不修改源码/产物;校验经 ToolGuard 硬性只读护栏;不注入 git 凭据。',
|
|
67
69
|
].join('\n'),
|
|
68
70
|
w3: [
|
|
@@ -87,7 +89,7 @@ export function buildPhaseInstruction(phase, ctx, kbMode) {
|
|
|
87
89
|
case 'd':
|
|
88
90
|
return '先读父任务交接(W2)里的 page_path(本地库 wiki/sources/ 下相对路径,库根见运行时上下文注入——Task 8 已注入 D),直接 fs 读实施计划原文;如需 KB 经验召回,经 skill 工具加载 llm-wiki query(本地库;kanban-d preset 已挂 tool-skill)——再按计划执行规格卡 solution/testing —— git worktree/branch → 改代码/README → git commit → git push(仅 feature 分支,可选)→ 自检并附产物证据(changed_files/commit_hash)。其余 TDD/TARGET_REPO/TARGET_BRANCH 等要求与 remote 版一致。';
|
|
89
91
|
case 'dt':
|
|
90
|
-
return `对 D 产物实证校验(test/build/typecheck/diff/git 证据 + open-code-review 评审),输出 verdict+issues 入交接 metadata.review_evidence。评审引擎:用 ocr_review 工具按配置引擎评审(委托 = preview→rule→git diff 自评;托管 = managed 一次出 findings;托管未配置时按工具指引静默改走委托;ocr 未安装则 kanban_block('review-tool-unavailable') 并注明可在 GUI
|
|
92
|
+
return `对 D 产物实证校验(test/build/typecheck/diff/git 证据 + open-code-review 评审),输出 verdict+issues 入交接 metadata.review_evidence。评审引擎:用 ocr_review 工具按配置引擎评审(委托 = preview→rule→git diff 自评;托管 = managed 一次出 findings;托管未配置时按工具指引静默改走委托;ocr 未安装则 kanban_block('review-tool-unavailable') 并注明可在 GUI 配置面板安装)。ocr_review 的 repo 缺省 = 会话工作目录,评审目标不在其内(D 的 worktree / 其他仓库)时显式传仓库绝对路径(D 交接 metadata.worktree_dir / 规格卡 file-prefetch ref)。评审页经 fs 写本地库 <库根>/wiki/queries/${ctx.chainId}/review/<name>.md(库根已由系统注入 DT 运行时上下文);库根外一律只读,其余铁律与 remote 版一致。`;
|
|
91
93
|
default:
|
|
92
94
|
return PHASE_INSTRUCTIONS[phase] ?? '';
|
|
93
95
|
}
|
|
@@ -443,9 +445,11 @@ export class VOrchestrator {
|
|
|
443
445
|
// blocked(防线A:零任务链也有数据侧终态,人工恢复=删链重跑)。零任务无锚点可评论 →
|
|
444
446
|
// 落 console.error(无任务卡载体,auditWarning 语义不符不用),orchestration.json 已留 stallCount。
|
|
445
447
|
if (!firstMatch) {
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
448
|
+
// 2026-09-22:标记集扩展 from-security-guard(安全护栏合成拒答与语义缓存同族——
|
|
449
|
+
// guard 拒答被语义缓存收录后,语义相近的新任务 prompt 全部命中回放)。
|
|
450
|
+
const guardReplay = !turnError && agent !== null && sessionEventsOf(agent).some((e) => replayModel(e) === 'from-cache' || replayModel(e) === 'from-security-guard');
|
|
451
|
+
if (guardReplay) {
|
|
452
|
+
console.error('[dsh-swarm][debug] V turn synthesized by gateway (from-cache/from-security-guard, usage=0) chain=' + chainId + ' phase=' + orch.phase + ' — 缓存/护栏污染嫌疑,按零产出计 stall');
|
|
449
453
|
}
|
|
450
454
|
orch.stallCount = (orch.stallCount ?? 0) + 1;
|
|
451
455
|
if (orch.stallCount > VOrchestrator.STALL_REWAKE_LIMIT) {
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
export type OcrSub = 'preview' | 'rule' | 'managed';
|
|
3
3
|
/** OCR 调用可选参数集合。 */
|
|
4
4
|
export interface OcrArgsInput {
|
|
5
|
+
/** 目标仓库根绝对路径(调用方解析:显式入参 → 当前会话工作目录);缺省时 ocr 用进程 current dir。 */
|
|
5
6
|
repo?: string;
|
|
6
7
|
from?: string;
|
|
7
8
|
to?: string;
|
|
@@ -12,7 +13,10 @@ export interface OcrArgsInput {
|
|
|
12
13
|
}
|
|
13
14
|
/** 构造 OCR CLI 参数:preview/rule 走 delegate,managed 走 review。
|
|
14
15
|
* preview 默认输出 text,必须显式 --format json 才能拿到可解析输出;
|
|
15
|
-
* managed 恒带 --audience agent(agent 场景标准参数,抑制 progress 输出)。
|
|
16
|
+
* managed 恒带 --audience agent(agent 场景标准参数,抑制 progress 输出)。
|
|
17
|
+
* 三子命令一律透传 --repo(preview/rule/review 均支持该 flag,实测 --help):
|
|
18
|
+
* ocr 探测仓库默认用 current dir,而本插件进程 cwd 不是目标仓库(实测为 ~/.codebuddy,
|
|
19
|
+
* 非 git 仓库)——漏传即稳定报 "Error: <cwd> is not a git repository"。 */
|
|
16
20
|
export declare function buildOcrArgs(sub: OcrSub, a: OcrArgsInput): string[];
|
|
17
21
|
/** 预览结果归一化结构。 */
|
|
18
22
|
export interface PreviewResult {
|
package/lib/domain/ocr-review.js
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
/** 构造 OCR CLI 参数:preview/rule 走 delegate,managed 走 review。
|
|
2
2
|
* preview 默认输出 text,必须显式 --format json 才能拿到可解析输出;
|
|
3
|
-
* managed 恒带 --audience agent(agent 场景标准参数,抑制 progress 输出)。
|
|
3
|
+
* managed 恒带 --audience agent(agent 场景标准参数,抑制 progress 输出)。
|
|
4
|
+
* 三子命令一律透传 --repo(preview/rule/review 均支持该 flag,实测 --help):
|
|
5
|
+
* ocr 探测仓库默认用 current dir,而本插件进程 cwd 不是目标仓库(实测为 ~/.codebuddy,
|
|
6
|
+
* 非 git 仓库)——漏传即稳定报 "Error: <cwd> is not a git repository"。 */
|
|
4
7
|
export function buildOcrArgs(sub, a) {
|
|
5
8
|
if (sub === 'preview') {
|
|
6
9
|
const args = ['delegate', 'preview', '--format', 'json'];
|
|
@@ -17,7 +20,10 @@ export function buildOcrArgs(sub, a) {
|
|
|
17
20
|
return args;
|
|
18
21
|
}
|
|
19
22
|
if (sub === 'rule') {
|
|
20
|
-
|
|
23
|
+
const args = ['delegate', 'rule', ...(a.paths ?? [])];
|
|
24
|
+
if (a.repo)
|
|
25
|
+
args.push('--repo', a.repo);
|
|
26
|
+
return args;
|
|
21
27
|
}
|
|
22
28
|
const args = ['review'];
|
|
23
29
|
if (a.commit)
|
|
@@ -31,6 +37,8 @@ export function buildOcrArgs(sub, a) {
|
|
|
31
37
|
args.push('--format', 'json', '--audience', 'agent');
|
|
32
38
|
if (a.background)
|
|
33
39
|
args.push('--background', a.background);
|
|
40
|
+
if (a.repo)
|
|
41
|
+
args.push('--repo', a.repo);
|
|
34
42
|
return args;
|
|
35
43
|
}
|
|
36
44
|
/** 解析失败的兜底结果。 */
|
|
@@ -1,5 +1,24 @@
|
|
|
1
1
|
import type { SpecCardSections } from './types.js';
|
|
2
2
|
import { type PrefetchManifest } from './prefetch-manifest.js';
|
|
3
|
+
export interface SourceEntry {
|
|
4
|
+
type: 'TAPD' | 'Jira' | '其他';
|
|
5
|
+
url: string;
|
|
6
|
+
note: string;
|
|
7
|
+
}
|
|
8
|
+
export interface PrdCollectionEntry {
|
|
9
|
+
url: string;
|
|
10
|
+
platform?: string;
|
|
11
|
+
status: 'collected' | 'blocked';
|
|
12
|
+
summary: string;
|
|
13
|
+
pages?: string[];
|
|
14
|
+
blockedReason?: string;
|
|
15
|
+
degraded?: boolean;
|
|
16
|
+
}
|
|
17
|
+
export interface PlaceholderEntry {
|
|
18
|
+
target: string;
|
|
19
|
+
value: string;
|
|
20
|
+
replace: string;
|
|
21
|
+
}
|
|
3
22
|
export interface PlanningChecklist {
|
|
4
23
|
requirementName?: string;
|
|
5
24
|
spec: SpecCardSections;
|
|
@@ -18,7 +37,13 @@ export interface PlanningChecklist {
|
|
|
18
37
|
source: string;
|
|
19
38
|
mitigation: string;
|
|
20
39
|
}>;
|
|
40
|
+
sources: SourceEntry[];
|
|
41
|
+
prdCollection: PrdCollectionEntry[];
|
|
42
|
+
placeholders?: PlaceholderEntry[];
|
|
43
|
+
greenfield?: boolean;
|
|
21
44
|
}
|
|
45
|
+
/** 闸2 禁词表:澄清答案中指代不明的缩写;宁窄后扩,避免误伤正常表述。 */
|
|
46
|
+
export declare const FORBIDDEN_SHORTHANDS: string[];
|
|
22
47
|
/** 需求澄清清单 schema 硬校验:返回错误列表(空数组=合法)。清单缺段即拒绝保存(硬闸,主 agent 会话内修正)。 */
|
|
23
48
|
export declare function validatePlanningChecklist(raw: unknown): string[];
|
|
24
49
|
/** 需求澄清清单页标题:与任务卡 title 同源同逻辑(buildChainTitle),保证 KB 可检索。 */
|
|
@@ -28,3 +53,8 @@ export declare function formatChecklistBody(c: PlanningChecklist): string;
|
|
|
28
53
|
/** 从清单页原文提取机读 PlanningChecklist(无损恢复路径):无段/损坏/校验不过一律返回 null,
|
|
29
54
|
* 由调用方回退 legacy LLM 重建流程。domain 纯函数,无副作用。 */
|
|
30
55
|
export declare function extractChecklistJson(pageMd: string): PlanningChecklist | null;
|
|
56
|
+
/** PRD 链接域名路由:已知平台直接判定,其余返回 null 交 LLM 自判。 */
|
|
57
|
+
export declare function routePrdPlatform(url: string): string | null;
|
|
58
|
+
/** PRD 原文切片:优先按一级标题(# 开头)切章,单片超 maxBytes 按行硬切。
|
|
59
|
+
* 默认 45KB/片(wiki 大文档拆页惯例,低于服务端 body 上限留余量)。 */
|
|
60
|
+
export declare function slicePrdMarkdown(md: string, maxBytes?: number): string[];
|
|
@@ -4,6 +4,8 @@ const STR_FIELDS = [
|
|
|
4
4
|
['problem', 'problem'], ['solution', 'solution'], ['testing', 'testing'], ['out_of_scope', 'out_of_scope'],
|
|
5
5
|
];
|
|
6
6
|
const ARR_FIELDS = [['user_stories', 'user_stories'], ['impl_decisions', 'impl_decisions']];
|
|
7
|
+
/** 闸2 禁词表:澄清答案中指代不明的缩写;宁窄后扩,避免误伤正常表述。 */
|
|
8
|
+
export const FORBIDDEN_SHORTHANDS = ['按推荐', '同上', '见上', '同前', '如前所述', 'same as above', 'ditto', 'see above'];
|
|
7
9
|
/** 需求澄清清单 schema 硬校验:返回错误列表(空数组=合法)。清单缺段即拒绝保存(硬闸,主 agent 会话内修正)。 */
|
|
8
10
|
export function validatePlanningChecklist(raw) {
|
|
9
11
|
const errors = [];
|
|
@@ -62,6 +64,13 @@ export function validatePlanningChecklist(raw) {
|
|
|
62
64
|
const got = typeof o === 'object' && o !== null ? Object.keys(o).join(',') : JSON.stringify(el);
|
|
63
65
|
errors.push(`checklist.clarifications[${i}] must be {"q": string, "a": string} with both non-empty (got keys: ${got}) — use keys "q" and "a", not "question"/"answer"`);
|
|
64
66
|
}
|
|
67
|
+
if (ok) {
|
|
68
|
+
const answer = o['a'].toLowerCase();
|
|
69
|
+
const hit = FORBIDDEN_SHORTHANDS.find((w) => answer.includes(w.toLowerCase()));
|
|
70
|
+
if (hit) {
|
|
71
|
+
errors.push(`checklist.clarifications[${i}].a contains forbidden shorthand "${hit}" — 每条问答必须含完整决策正文(字段名/接口路径/枚举值/组件名/位置规则),禁止指代不明缩写,展开写全`);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
65
74
|
});
|
|
66
75
|
}
|
|
67
76
|
const dbs = c['doubts'];
|
|
@@ -101,6 +110,79 @@ export function validatePlanningChecklist(raw) {
|
|
|
101
110
|
});
|
|
102
111
|
}
|
|
103
112
|
}
|
|
113
|
+
// 闸1:需求来源必挂。恢复重建旧页无来源时按错误文案中的逃生口录「无来源+原因」条目
|
|
114
|
+
const sources = c['sources'];
|
|
115
|
+
if (!Array.isArray(sources) || sources.length === 0) {
|
|
116
|
+
errors.push('checklist.sources must be a non-empty array — record requirement source links (TAPD/Jira/其他). If genuinely no source exists, record exactly [{type:"其他", url:"", note:"无来源+原因"}]');
|
|
117
|
+
}
|
|
118
|
+
else {
|
|
119
|
+
sources.forEach((el, i) => {
|
|
120
|
+
const o = el;
|
|
121
|
+
const typeOk = o !== null && typeof o === 'object' && (o['type'] === 'TAPD' || o['type'] === 'Jira' || o['type'] === '其他');
|
|
122
|
+
const urlOk = typeof o?.['url'] === 'string';
|
|
123
|
+
const noteOk = typeof o?.['note'] === 'string';
|
|
124
|
+
if (!typeOk || !urlOk || !noteOk) {
|
|
125
|
+
const got = typeof o === 'object' && o !== null ? Object.keys(o).join(',') : JSON.stringify(el);
|
|
126
|
+
errors.push(`checklist.sources[${i}] must be {"type": "TAPD"|"Jira"|"其他", "url": string, "note": string} (got keys: ${got})`);
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
if (o['url'].trim() === '' && o['note'].trim() === '') {
|
|
130
|
+
errors.push(`checklist.sources[${i}] url 为空(无来源)时 note 必须写明无来源原因`);
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
// 闸3:PRD 采集凭证。登记链接必须落到 collected(含摘要/页路径)或 blocked(含原因);
|
|
135
|
+
// 其余状态(含漏填)= 登记了却没采 → 拒收
|
|
136
|
+
const prd = c['prdCollection'];
|
|
137
|
+
if (!Array.isArray(prd)) {
|
|
138
|
+
errors.push('checklist.prdCollection must be an array — register every PRD link the user provided (empty [] only when the user gave no PRD link)');
|
|
139
|
+
}
|
|
140
|
+
else {
|
|
141
|
+
prd.forEach((el, i) => {
|
|
142
|
+
const o = el;
|
|
143
|
+
const urlOk = typeof o?.['url'] === 'string' && o['url'].trim().length > 0;
|
|
144
|
+
const status = o?.['status'];
|
|
145
|
+
if (typeof o !== 'object' || o === null || !urlOk) {
|
|
146
|
+
errors.push(`checklist.prdCollection[${i}] must be an object with non-empty "url"`);
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
if (status !== 'collected' && status !== 'blocked') {
|
|
150
|
+
errors.push(`checklist.prdCollection[${i}].status must be "collected" or "blocked" (got: ${JSON.stringify(status)}) — 链接已登记就必须完成采集或记录阻塞原因,禁止悬空`);
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
if (typeof o['summary'] !== 'string' || o['summary'].trim() === '') {
|
|
154
|
+
errors.push(`checklist.prdCollection[${i}].summary must be a non-empty string(采了什么+关键信息+缺口)`);
|
|
155
|
+
}
|
|
156
|
+
if (status === 'blocked' && (typeof o['blockedReason'] !== 'string' || o['blockedReason'].trim() === '')) {
|
|
157
|
+
errors.push(`checklist.prdCollection[${i}].blockedReason required when status="blocked"(登录态 | 缺技能 | 404 | 其他)`);
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
// 占位策略(可选):存在则三要素非空
|
|
162
|
+
const phs = c['placeholders'];
|
|
163
|
+
if (phs !== undefined) {
|
|
164
|
+
if (!Array.isArray(phs)) {
|
|
165
|
+
errors.push('checklist.placeholders must be an array when present');
|
|
166
|
+
}
|
|
167
|
+
else {
|
|
168
|
+
phs.forEach((el, i) => {
|
|
169
|
+
const o = el;
|
|
170
|
+
const ok = typeof o === 'object' && o !== null &&
|
|
171
|
+
typeof o['target'] === 'string' && o['target'].trim() !== '' &&
|
|
172
|
+
typeof o['value'] === 'string' &&
|
|
173
|
+
typeof o['replace'] === 'string' && o['replace'].trim() !== '';
|
|
174
|
+
if (!ok) {
|
|
175
|
+
const got = typeof o === 'object' && o !== null ? Object.keys(o).join(',') : JSON.stringify(el);
|
|
176
|
+
errors.push(`checklist.placeholders[${i}] must be {"target": string, "value": string, "replace": string} with target/replace non-empty (got keys: ${got})`);
|
|
177
|
+
}
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
// greenfield(可选):存在即须布尔。true=绿地(无 .git 全新项目),D 阶段建仓前置;false/缺省=既有仓库
|
|
182
|
+
const gf = c['greenfield'];
|
|
183
|
+
if (gf !== undefined && typeof gf !== 'boolean') {
|
|
184
|
+
errors.push(`checklist.greenfield must be a boolean when present (got: ${JSON.stringify(gf)})`);
|
|
185
|
+
}
|
|
104
186
|
return errors;
|
|
105
187
|
}
|
|
106
188
|
/** 需求澄清清单页标题:与任务卡 title 同源同逻辑(buildChainTitle),保证 KB 可检索。 */
|
|
@@ -126,9 +208,16 @@ export function formatChecklistBody(c) {
|
|
|
126
208
|
if (repo.branch)
|
|
127
209
|
lines.push('- 当前分支: ' + repo.branch);
|
|
128
210
|
lines.push('- 未提交改动: ' + (repo.dirtyFiles.length ? repo.dirtyFiles.map((f) => `\`${f}\``).join(', ') : '无'));
|
|
211
|
+
if (c.greenfield === true)
|
|
212
|
+
lines.push('- greenfield: true(全新项目,D 阶段 git init 建仓前置)');
|
|
129
213
|
lines.push('', '### 文件基线', '', '| 路径 | 期望 | 备注 |', '| --- | --- | --- |');
|
|
130
214
|
for (const f of files)
|
|
131
215
|
lines.push(`| ${f.path} | ${f.expected} | ${f.note ?? '-'} |`);
|
|
216
|
+
lines.push('## 需求来源', '');
|
|
217
|
+
for (const s of c.sources) {
|
|
218
|
+
lines.push(`- [${s.type}] ${s.url || '(无来源)'}${s.note ? ` — ${s.note}` : ''}`);
|
|
219
|
+
}
|
|
220
|
+
lines.push('');
|
|
132
221
|
lines.push('', '## 澄清问答', '');
|
|
133
222
|
clarifications.forEach((qa, i) => {
|
|
134
223
|
lines.push(`### Q${i + 1}. ${qa.q}`, `- **A**: ${qa.a}`, '');
|
|
@@ -151,10 +240,35 @@ export function formatChecklistBody(c) {
|
|
|
151
240
|
}
|
|
152
241
|
if (doubts.length === 0)
|
|
153
242
|
lines.push('(无)');
|
|
243
|
+
lines.push('', '## PRD 采集记录', '');
|
|
244
|
+
const prd = c.prdCollection ?? [];
|
|
245
|
+
if (prd.length === 0) {
|
|
246
|
+
lines.push('(用户未提供 PRD 链接)');
|
|
247
|
+
}
|
|
248
|
+
else {
|
|
249
|
+
for (const p of prd) {
|
|
250
|
+
lines.push(`- ${p.url} — ${p.status}${p.platform ? `(${p.platform})` : ''}${p.degraded ? ' — degraded(截图超限或写入失败,已降级为文字描述)' : ''}`);
|
|
251
|
+
lines.push(` - 摘要: ${p.summary}`);
|
|
252
|
+
if (p.status === 'blocked' && p.blockedReason)
|
|
253
|
+
lines.push(` - 阻塞原因: ${p.blockedReason}`);
|
|
254
|
+
for (const pg of p.pages ?? [])
|
|
255
|
+
lines.push(` - 原文页: ${pg}`);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
lines.push('', '## 占位策略', '');
|
|
259
|
+
const phs = c.placeholders ?? [];
|
|
260
|
+
if (phs.length === 0) {
|
|
261
|
+
lines.push('(无后端未就绪项)');
|
|
262
|
+
}
|
|
263
|
+
else {
|
|
264
|
+
lines.push('| 占位对象 | 占位值 | 替换方式 |', '| --- | --- | --- |');
|
|
265
|
+
for (const p of phs)
|
|
266
|
+
lines.push(`| ${p.target} | ${p.value} | ${p.replace} |`);
|
|
267
|
+
}
|
|
154
268
|
// 机读段(2026-09-21 恢复流程矫正):页正文是人读 markdown(有损),重启恢复若让 LLM 读页重建
|
|
155
269
|
// 再回存 = 有损再创作 + 双重编码风险(销服一体清单 7 轮失败即发生在恢复场景)。页尾内嵌无损
|
|
156
270
|
// JSON 单行段,/openspec: 路由2 直接提取灌内存建链,LLM 零参与;HTML 注释对人读零干扰。
|
|
157
|
-
const machine = JSON.stringify({ requirementName: c.requirementName ?? null, spec: c.spec, manifest: c.manifest, clarifications: c.clarifications, doubts: c.doubts, risks: c.risks ?? [] });
|
|
271
|
+
const machine = JSON.stringify({ requirementName: c.requirementName ?? null, spec: c.spec, manifest: c.manifest, clarifications: c.clarifications, doubts: c.doubts, risks: c.risks ?? [], sources: c.sources, prdCollection: c.prdCollection ?? [], placeholders: c.placeholders ?? [], greenfield: c.greenfield ?? false });
|
|
158
272
|
lines.push('', `<!-- dsh-swarm:checklist-json ${machine} -->`);
|
|
159
273
|
return lines.join('\n').replace(/\n{3,}/g, '\n\n');
|
|
160
274
|
}
|
|
@@ -180,3 +294,59 @@ export function extractChecklistJson(pageMd) {
|
|
|
180
294
|
return null;
|
|
181
295
|
return raw;
|
|
182
296
|
}
|
|
297
|
+
/** PRD 链接域名路由:已知平台直接判定,其余返回 null 交 LLM 自判。 */
|
|
298
|
+
export function routePrdPlatform(url) {
|
|
299
|
+
let host;
|
|
300
|
+
try {
|
|
301
|
+
host = new URL(url).hostname.toLowerCase();
|
|
302
|
+
}
|
|
303
|
+
catch {
|
|
304
|
+
return null;
|
|
305
|
+
}
|
|
306
|
+
if (host === 'feishu.cn' || host.endsWith('.feishu.cn'))
|
|
307
|
+
return 'feishu';
|
|
308
|
+
if (host === 'doc.weixin.qq.com')
|
|
309
|
+
return 'wecom';
|
|
310
|
+
if (host === 'modao.cc' || host.endsWith('.modao.cc'))
|
|
311
|
+
return 'modao';
|
|
312
|
+
return null;
|
|
313
|
+
}
|
|
314
|
+
/** PRD 原文切片:优先按一级标题(# 开头)切章,单片超 maxBytes 按行硬切。
|
|
315
|
+
* 默认 45KB/片(wiki 大文档拆页惯例,低于服务端 body 上限留余量)。 */
|
|
316
|
+
export function slicePrdMarkdown(md, maxBytes = 45_000) {
|
|
317
|
+
const chapters = [];
|
|
318
|
+
let cur = [];
|
|
319
|
+
for (const line of md.split('\n')) {
|
|
320
|
+
if (/^#\s/.test(line) && cur.length > 0) {
|
|
321
|
+
chapters.push(cur.join('\n'));
|
|
322
|
+
cur = [line];
|
|
323
|
+
}
|
|
324
|
+
else {
|
|
325
|
+
cur.push(line);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
if (cur.length > 0)
|
|
329
|
+
chapters.push(cur.join('\n'));
|
|
330
|
+
const parts = [];
|
|
331
|
+
for (const ch of chapters) {
|
|
332
|
+
if (Buffer.byteLength(ch, 'utf8') <= maxBytes) {
|
|
333
|
+
parts.push(ch);
|
|
334
|
+
continue;
|
|
335
|
+
}
|
|
336
|
+
let buf = [];
|
|
337
|
+
let size = 0;
|
|
338
|
+
for (const line of ch.split('\n')) {
|
|
339
|
+
const lb = Buffer.byteLength(line + '\n', 'utf8');
|
|
340
|
+
if (size + lb > maxBytes && buf.length > 0) {
|
|
341
|
+
parts.push(buf.join('\n'));
|
|
342
|
+
buf = [];
|
|
343
|
+
size = 0;
|
|
344
|
+
}
|
|
345
|
+
buf.push(line);
|
|
346
|
+
size += lb;
|
|
347
|
+
}
|
|
348
|
+
if (buf.length > 0)
|
|
349
|
+
parts.push(buf.join('\n'));
|
|
350
|
+
}
|
|
351
|
+
return parts.length > 0 ? parts : [''];
|
|
352
|
+
}
|
package/lib/roles/toolsets.d.ts
CHANGED
|
@@ -110,7 +110,7 @@ export declare function buildSwarmSessionGuard(deps?: SubagentGuardDeps): (execu
|
|
|
110
110
|
/** wiki_write 会话级放行 preset 默认白名单:与 config.ts wikiWritePresets 默认值同源,
|
|
111
111
|
* 仅供 guard deps 未接线(测试/裸调用)时兜底;生产经 dispatcher 注入热读配置。
|
|
112
112
|
* 'ptc':普通(PTC 模式)会话亦可写 wiki 命名空间——写面本身由 wiki_write 工具内的
|
|
113
|
-
*
|
|
113
|
+
* 六类命名空间白名单硬约束;2026-09-17 起 preset 判定走统一读面(session-preset.ts),
|
|
114
114
|
* 'ptc' 不再是「蜂群会话被误读成 ptc」的兜底,而是显式放行普通会话。 */
|
|
115
115
|
export declare const DEFAULT_WIKI_WRITE_PRESETS: ReadonlyArray<string>;
|
|
116
116
|
export interface StandaloneDtGuardDeps {
|
package/lib/roles/toolsets.js
CHANGED
|
@@ -405,8 +405,10 @@ export async function installRoleTools(agentCtx, role, deps) {
|
|
|
405
405
|
registry.register(tool);
|
|
406
406
|
}
|
|
407
407
|
}
|
|
408
|
-
// ocr_review:ocr CLI 三子命令(preview/rule/managed);未安装/托管未配置在 execute
|
|
409
|
-
|
|
408
|
+
// ocr_review:ocr CLI 三子命令(preview/rule/managed);未安装/托管未配置在 execute 内降级引导,注册无条件。
|
|
409
|
+
// 目标仓库由 execute 内解析(repo 参数 → exec.agent.session.header.cwd)——不注入 cwd,
|
|
410
|
+
// 避免回退到插件进程目录(非仓库,ocr 必报 not a git repository)。
|
|
411
|
+
registry.register(buildOcrReviewTool({}));
|
|
410
412
|
}
|
|
411
413
|
else if (role === 'v') {
|
|
412
414
|
for (const tool of buildSpecCardTools(deps.kanban, caller)) {
|
|
@@ -595,7 +597,7 @@ function dualVerbGitDenyReason(verb, rest, deny) {
|
|
|
595
597
|
/** wiki_write 会话级放行 preset 默认白名单:与 config.ts wikiWritePresets 默认值同源,
|
|
596
598
|
* 仅供 guard deps 未接线(测试/裸调用)时兜底;生产经 dispatcher 注入热读配置。
|
|
597
599
|
* 'ptc':普通(PTC 模式)会话亦可写 wiki 命名空间——写面本身由 wiki_write 工具内的
|
|
598
|
-
*
|
|
600
|
+
* 六类命名空间白名单硬约束;2026-09-17 起 preset 判定走统一读面(session-preset.ts),
|
|
599
601
|
* 'ptc' 不再是「蜂群会话被误读成 ptc」的兜底,而是显式放行普通会话。 */
|
|
600
602
|
export const DEFAULT_WIKI_WRITE_PRESETS = ['swarm', 'kanban-w', 'ptc'];
|
|
601
603
|
export function buildStandaloneDtGuard(deps = {}) {
|
|
@@ -665,7 +667,7 @@ export function buildStandaloneDtGuard(deps = {}) {
|
|
|
665
667
|
if (isRoleComposed(execution?.agent) || isDtChainSubagent)
|
|
666
668
|
return undefined;
|
|
667
669
|
// 放宽(2026-09-16):swarm 主会话与 W preset 会话可写 wiki——can('wiki-write') 本就含 w
|
|
668
|
-
// (permissions.ts),写面由 wiki_write 工具内 assertAllowedWikiPagePath
|
|
670
|
+
// (permissions.ts),写面由 wiki_write 工具内 assertAllowedWikiPagePath 六类命名空间
|
|
669
671
|
// 白名单硬约束;local 模式 wiki_write 未全局注册,无工具面即无暴露面。
|
|
670
672
|
// 放行面配置化(硬规则 6):白名单经 deps 热读 config.wikiWritePresets,宿主 preset
|
|
671
673
|
// 体系演化/部署自定义模式 id 时改配置即可;config 缺字段回 [](fail-closed,仅内置默认兜底测试)。
|
|
@@ -694,7 +696,7 @@ export function registerStandaloneReviewerTools(ctx, configProvider) {
|
|
|
694
696
|
const registry = ctx.get('tools');
|
|
695
697
|
if (!registry)
|
|
696
698
|
return; // 测试裸 Context 无 tools 服务,跳过注册
|
|
697
|
-
registry.register(buildOcrReviewTool({
|
|
699
|
+
registry.register(buildOcrReviewTool({})); // 目标仓库同链上路径:repo 参数 → 会话工作目录(见 ocr-review-tools)
|
|
698
700
|
const kbMode = configProvider.mode;
|
|
699
701
|
const wiki = (kbMode === 'local'
|
|
700
702
|
? new LocalWikiClient(ensureLocalKbRoot())
|
|
@@ -3,7 +3,8 @@ import type { SpecCard, SpecCardAttachment } from '../domain/types.js';
|
|
|
3
3
|
import { type PrefixRoutes } from '../config.js';
|
|
4
4
|
/** 阶段 0 规划引导:命令串从 config 派生,/openspec: 改名时文案自动跟随。
|
|
5
5
|
* 提问节奏与 skills/grill-me/SKILL.md 对齐:官方 frontier 分轮制(整批问 + 推荐答案 + 答复后解锁下一轮);
|
|
6
|
-
*
|
|
6
|
+
* 需求澄清十规则:来源必挂 + PRD 采集 + 范围边界 + 人话 + 仓库事实/缺口暂停 + 收敛录入铁律(含决策可追溯);
|
|
7
|
+
* 收敛产物仍为 planning_checklist_save(KB 清单是建链数据源)。 */
|
|
7
8
|
export declare function buildPlanningGuidance(routes: PrefixRoutes, opts?: {
|
|
8
9
|
swarm?: boolean;
|
|
9
10
|
}): string;
|