@joekytc/dsh-swarm 0.3.8 → 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 +37 -2
- package/lib/dispatcher/dispatcher.d.ts +8 -0
- package/lib/dispatcher/dispatcher.js +21 -0
- 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 +13 -6
- package/lib/domain/delivery-contract.d.ts +2 -1
- package/lib/domain/delivery-contract.js +16 -3
- package/lib/domain/ocr-review.d.ts +5 -1
- package/lib/domain/ocr-review.js +10 -2
- package/lib/domain/planning-checklist.d.ts +33 -0
- package/lib/domain/planning-checklist.js +197 -0
- package/lib/domain/prefetch-manifest.js +15 -4
- package/lib/roles/preset-installer.d.ts +14 -0
- package/lib/roles/preset-installer.js +17 -0
- 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/kanban-tools.js +10 -2
- package/lib/tools/main-session-tools.d.ts +2 -1
- package/lib/tools/main-session-tools.js +101 -20
- 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 +248 -3
- package/lib/tools/spec-card-tools.js +8 -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 +2 -2
- 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>;
|
|
@@ -5,8 +5,9 @@ import { probeOcr } from '../services/ocr-cli.js';
|
|
|
5
5
|
import { installRoleTools, buildReadOnlyWriteGuard, buildDTWriteGuard, buildPlanWriteGuard, buildKbWriteGuard, registerDtTaskChain, unregisterDtTaskChain } from '../roles/toolsets.js';
|
|
6
6
|
import { ensureLocalKbRoot } from '../wiki/local-kb.js';
|
|
7
7
|
import { installCleanFsTools } from '../roles/clean-fs-tools.js';
|
|
8
|
+
import { mountOrRecompose } from '../roles/preset-installer.js';
|
|
8
9
|
import { buildModelCandidates, isModelUnavailableError } from './model-candidates.js';
|
|
9
|
-
import { toolName } from './session-events.js';
|
|
10
|
+
import { toolName, isGuardSynthesizedReply } from './session-events.js';
|
|
10
11
|
import { attachSessionToWorkspace, resolveOrCreateWorkspace } from './workspace-attach.js';
|
|
11
12
|
import { isPathInside, resolveTargetRepoDir } from './target-repo.js';
|
|
12
13
|
import { injectGitCredentials, resolveGitPatFromCtx } from './git-credentials.js';
|
|
@@ -82,6 +83,18 @@ export class AgentRunner {
|
|
|
82
83
|
if (task.body)
|
|
83
84
|
parts.push(`## Body
|
|
84
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
|
+
}
|
|
85
98
|
// W local KB 模式:注入本地库根 + llm-wiki 用法 + 排序原则 + 交付约定(remote 模式不注入)。
|
|
86
99
|
// 库内复制/移动补句:DIRECT cp/mv/mkdir 工具永远匹配不到 KB 目标(无 path/file_path
|
|
87
100
|
// 实参)会被护栏拒绝——必须显式指引 W 用 write 工具做库内复制/移动。
|
|
@@ -271,7 +284,10 @@ ${task.body}`);
|
|
|
271
284
|
if (presets) {
|
|
272
285
|
const presetId = 'kanban-' + task.assignee;
|
|
273
286
|
try {
|
|
274
|
-
|
|
287
|
+
// mountOrRecompose(2026-09-21):blocked/unblock 多轮唤醒 resume 同一 session 时,
|
|
288
|
+
// 二次 mount 会撞宿主 dsh-scope 一次性绑定(already bound to a parent)——收敛为
|
|
289
|
+
// 已绑走 recompose 官方重链;其余挂载错误仍降级放行(角色工具面照常注册)。
|
|
290
|
+
await mountOrRecompose(presets, agentCtx, presetId);
|
|
275
291
|
console.error('[dsh-swarm][debug] preset mounted ' + presetId + ' role=' + task.assignee + ' task=' + taskId);
|
|
276
292
|
}
|
|
277
293
|
catch (err) {
|
|
@@ -439,6 +455,25 @@ ${task.body}`);
|
|
|
439
455
|
console.error('[dsh-swarm][debug] runner followup sent ' + taskId);
|
|
440
456
|
await agent.whenIdle();
|
|
441
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
|
+
}
|
|
442
477
|
// 终态判据(session 10 事故修复):whenIdle resolve 后一次 snapshot,只看任务真实状态——
|
|
443
478
|
// 不再扫 session.events 全历史工具名(旧 incarnation 的 kanban_* 事件会污染判据,
|
|
444
479
|
// 「历史上调过工具名」≠「本轮成功提交」,骗过检查后卡永驻 running)。
|
|
@@ -85,6 +85,14 @@ export declare class Dispatcher {
|
|
|
85
85
|
/** 启动 reconcile:剔除事件流中已不存在的链的编排 entry(历史残留/外部 purge)。
|
|
86
86
|
* 原地删除并返回被移除的 chainId 列表(调用方负责持久化与日志)。 */
|
|
87
87
|
export declare function reconcileOrchestrations<T>(orch: Map<string, T>, chains: Set<string>): string[];
|
|
88
|
+
/** 启动补扫候选:快照中全部 executing 状态链(2026-09-21 重启吞编排轮事故)。
|
|
89
|
+
* EventWaker 纯事件驱动,历史 wakeable 事件(如 PT completed)被重启游标越过即永不重放;
|
|
90
|
+
* 启动时按链状态补唤醒,与 EventWaker 的实时驱动互补。 */
|
|
91
|
+
export declare function collectExecutingChains(snap: {
|
|
92
|
+
chains: Map<string, {
|
|
93
|
+
status: string;
|
|
94
|
+
}>;
|
|
95
|
+
}): string[];
|
|
88
96
|
/** 启动 reconcile:进程重启会杀死 runner 的 whenIdle 协程,上次遗留的 running 卡无人收尾,
|
|
89
97
|
* 看门狗默认 4h(staleTimeoutSeconds=14400)才回收——重启后立即把 running 孤儿卡收敛为 blocked
|
|
90
98
|
* (system comment + blockTask),中断显形且可重派续跑(重派将 resume 同一会话,进度保留)。
|
|
@@ -311,6 +311,17 @@ export function reconcileOrchestrations(orch, chains) {
|
|
|
311
311
|
orch.delete(k);
|
|
312
312
|
return removed;
|
|
313
313
|
}
|
|
314
|
+
/** 启动补扫候选:快照中全部 executing 状态链(2026-09-21 重启吞编排轮事故)。
|
|
315
|
+
* EventWaker 纯事件驱动,历史 wakeable 事件(如 PT completed)被重启游标越过即永不重放;
|
|
316
|
+
* 启动时按链状态补唤醒,与 EventWaker 的实时驱动互补。 */
|
|
317
|
+
export function collectExecutingChains(snap) {
|
|
318
|
+
const out = [];
|
|
319
|
+
for (const [chainId, chain] of snap.chains) {
|
|
320
|
+
if (chain.status === 'executing')
|
|
321
|
+
out.push(chainId);
|
|
322
|
+
}
|
|
323
|
+
return out;
|
|
324
|
+
}
|
|
314
325
|
/** 启动 reconcile:进程重启会杀死 runner 的 whenIdle 协程,上次遗留的 running 卡无人收尾,
|
|
315
326
|
* 看门狗默认 4h(staleTimeoutSeconds=14400)才回收——重启后立即把 running 孤儿卡收敛为 blocked
|
|
316
327
|
* (system comment + blockTask),中断显形且可重派续跑(重派将 resume 同一会话,进度保留)。
|
|
@@ -523,6 +534,16 @@ function startDispatcherInner(ctx, configProvider, storageDir, logFile, provider
|
|
|
523
534
|
saveOrchs();
|
|
524
535
|
logToFile(logFile, '[orch-reconcile] removed dead entries: ' + removed.join(','));
|
|
525
536
|
}
|
|
537
|
+
// 启动补扫(2026-09-21):EventWaker 纯事件驱动——实例重启后游标已越过「已完成但未推进」
|
|
538
|
+
// 的 wakeable 事件(如 PT completed)时编排轮被吞(历史事件不重放),executing 链静默停滞
|
|
539
|
+
// (销服一体 V3.0 链实测:PT done 后 W2 永不建卡)。启动时对全部 executing 链补一次 wakeV
|
|
540
|
+
// (幂等:wakeV 按 orchestration/链快照决策 + 在途合并;无可推进阶段时无害空转)。
|
|
541
|
+
for (const chainId of collectExecutingChains(snap)) {
|
|
542
|
+
logToFile(logFile, '[startup-rewake] executing chain=' + chainId);
|
|
543
|
+
void vOrch.wakeV(chainId).catch((err) => {
|
|
544
|
+
logToFile(logFile, '[startup-rewake] failed chain=' + chainId + ' ' + String(err));
|
|
545
|
+
});
|
|
546
|
+
}
|
|
526
547
|
}
|
|
527
548
|
catch (err) {
|
|
528
549
|
logToFile(logFile, '[orch-reconcile] failed: ' + String(err));
|
|
@@ -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
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { installRoleTools } from '../roles/toolsets.js';
|
|
2
|
+
import { mountOrRecompose } from '../roles/preset-installer.js';
|
|
2
3
|
import { resolveTaskParents } from '../domain/task-parents.js';
|
|
3
4
|
import { missingParentDelivery } from '../domain/delivery-contract.js';
|
|
4
5
|
import { buildRepoSlug } from '../domain/memory.js';
|
|
@@ -24,6 +25,7 @@ export const PHASE_INSTRUCTIONS = {
|
|
|
24
25
|
'## P 阶段任务体要求(计划者,非执行者)',
|
|
25
26
|
'body 写入规划指令:读规格卡(含 file-prefetch/kb 附件=需求澄清清单)→ 产出 openspec 实施计划。仓库事实不足时先只读自查仓库代码实证(fs/search/grep 均可),再产出计划。',
|
|
26
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 保持只读。',
|
|
27
29
|
'tasks.md 拆分粒度(结构准备度,与 PT 同一判据):每个任务条目必须有单一、可独立核对的完成判据;禁止一条任务/一条测试用例混 N≥2 个互不依赖的可观察结果——按可独立验证的行为逐条拆分。完成判据必须是实施完成后的单一终态;不写执行层阶段态判据(RED/GREEN/REFACTOR、"先失败再通过"属 D 阶段执行协议,见 D 阶段模板)。',
|
|
28
30
|
'proposal.md 必须含「上游协议遵循说明」节:逐条列规格卡/澄清清单声明的工程协议引用(可定位出处:路径/明确协议名)+ 计划如何遵守;未声明任何协议则该节写「无」。禁止引用规格卡之外的协议标准。',
|
|
29
31
|
'需求对齐(颗粒度):澄清清单/规格卡的任务项、任务点、注意点 ↔ 计划任务逐条双向映射——不遗漏;超纲任务须能溯源到澄清回答。',
|
|
@@ -52,6 +54,7 @@ export const PHASE_INSTRUCTIONS = {
|
|
|
52
54
|
'## D 阶段任务体要求(执行者,唯一,非只读对齐/校验)',
|
|
53
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)。',
|
|
54
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 摘要注明声明与事实不符',
|
|
55
58
|
'body 同时声明 TARGET_BRANCH=<目标分支名>(来自规格卡/用户声明):D 在 worktree feature 分支完成实现并验证后即 complete,禁止合并回 TARGET_BRANCH、禁止推 TARGET_BRANCH——合入由 DT 通过后 system 统一执行。',
|
|
56
59
|
'complete 时 metadata 必须带 branch=<feature 分支名>(DT 评审与 system 合入定位该分支用);git 证据 changed_files + commit_hash 必须(push 可选,可推 feature 分支)。',
|
|
57
60
|
'complete 时 metadata 建议携带 worktree_dir=<你的 feature worktree 绝对路径>:系统将在该目录实测 tdd.test_files(npx vitest run);缺失则跳过实测(仅声明校验)。',
|
|
@@ -61,7 +64,7 @@ export const PHASE_INSTRUCTIONS = {
|
|
|
61
64
|
].join('\n'),
|
|
62
65
|
dt: [
|
|
63
66
|
'## DT 阶段任务体要求(实现校验+评审,只读护栏)',
|
|
64
|
-
'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)。',
|
|
65
68
|
'铁律:DT 是只读校验+评审角色,绝不修改源码/产物;校验经 ToolGuard 硬性只读护栏;不注入 git 凭据。',
|
|
66
69
|
].join('\n'),
|
|
67
70
|
w3: [
|
|
@@ -86,7 +89,7 @@ export function buildPhaseInstruction(phase, ctx, kbMode) {
|
|
|
86
89
|
case 'd':
|
|
87
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 版一致。';
|
|
88
91
|
case 'dt':
|
|
89
|
-
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 版一致。`;
|
|
90
93
|
default:
|
|
91
94
|
return PHASE_INSTRUCTIONS[phase] ?? '';
|
|
92
95
|
}
|
|
@@ -442,9 +445,11 @@ export class VOrchestrator {
|
|
|
442
445
|
// blocked(防线A:零任务链也有数据侧终态,人工恢复=删链重跑)。零任务无锚点可评论 →
|
|
443
446
|
// 落 console.error(无任务卡载体,auditWarning 语义不符不用),orchestration.json 已留 stallCount。
|
|
444
447
|
if (!firstMatch) {
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
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');
|
|
448
453
|
}
|
|
449
454
|
orch.stallCount = (orch.stallCount ?? 0) + 1;
|
|
450
455
|
if (orch.stallCount > VOrchestrator.STALL_REWAKE_LIMIT) {
|
|
@@ -636,9 +641,11 @@ export class VOrchestrator {
|
|
|
636
641
|
// 无 persona 基座不得裸奔。抛错 → create/resume 失败 → getVAgent 抛 → 主推进路径并入
|
|
637
642
|
// 异常收场(stall 计数,超限 [create-failed] + blockChain);候选循环对非 model
|
|
638
643
|
// 错误立即失败(isModelUnavailableError=false → throw),不会被候选切换吞掉。
|
|
644
|
+
// mountOrRecompose(2026-09-21):V 会话跨进程重启 resume / 重入 setup 时二次 mount
|
|
645
|
+
// 会撞宿主 dsh-scope 一次性绑定——already-bound 收敛为 recompose 官方重链,其余照抛。
|
|
639
646
|
const vSessionId = orch.sessionId ?? 'kbn-v-' + orch.chainId;
|
|
640
647
|
try {
|
|
641
|
-
await presets
|
|
648
|
+
await mountOrRecompose(presets, agentCtx, V_SESSION_PRESET_ID);
|
|
642
649
|
}
|
|
643
650
|
catch (err) {
|
|
644
651
|
throw new Error(V_SESSION_PRESET_ID + ' preset mount failed for ' + vSessionId + ': ' + String(err));
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { BoardState, Handoff, Role, TaskMode } from './types.js';
|
|
2
2
|
export declare function requiredDeliveryKeys(assignee: Role, mode: TaskMode): string[];
|
|
3
|
-
/** v2:pt_decision 结构校验(needed 布尔必填;needed=true 时 reason 必填)。返回缺失键列表。
|
|
3
|
+
/** v2:pt_decision 结构校验(needed 布尔必填;needed=true 时 reason 必填)。返回缺失键列表。
|
|
4
|
+
* 2026-09-21:metadata 非对象(双重编码字符串)时报类型真因,不产生误导性 'pt_decision' 缺键文案。 */
|
|
4
5
|
export declare function missingPtDecisionKeys(handoff: Handoff | undefined): string[];
|
|
5
6
|
/** 缺失的交付键(存在但为空的字符串/非字符串均视为缺失;pt_decision 走结构校验透传细粒度键)。
|
|
6
7
|
* 可选 kbUrlBase:提供时对 w:kb 的 kb_url 做 host 前缀硬校验(防 LLM 手写错域名)、对 page_path 做
|
|
@@ -17,9 +17,14 @@ const REQUIRED_DELIVERY = {
|
|
|
17
17
|
export function requiredDeliveryKeys(assignee, mode) {
|
|
18
18
|
return REQUIRED_DELIVERY[`${assignee}:${mode}`] ?? [];
|
|
19
19
|
}
|
|
20
|
-
/** v2:pt_decision 结构校验(needed 布尔必填;needed=true 时 reason 必填)。返回缺失键列表。
|
|
20
|
+
/** v2:pt_decision 结构校验(needed 布尔必填;needed=true 时 reason 必填)。返回缺失键列表。
|
|
21
|
+
* 2026-09-21:metadata 非对象(双重编码字符串)时报类型真因,不产生误导性 'pt_decision' 缺键文案。 */
|
|
21
22
|
export function missingPtDecisionKeys(handoff) {
|
|
22
|
-
const
|
|
23
|
+
const rawMeta = handoff?.metadata ?? {};
|
|
24
|
+
if (typeof rawMeta !== 'object' || rawMeta === null) {
|
|
25
|
+
return [`pt_decision (metadata must be an object, got ${Array.isArray(rawMeta) ? 'array' : typeof rawMeta})`];
|
|
26
|
+
}
|
|
27
|
+
const d = rawMeta['pt_decision'];
|
|
23
28
|
if (typeof d !== 'object' || d === null)
|
|
24
29
|
return ['pt_decision'];
|
|
25
30
|
const o = d;
|
|
@@ -39,7 +44,11 @@ export function missingDeliveryKeys(assignee, mode, handoff, kbUrlBase) {
|
|
|
39
44
|
return [];
|
|
40
45
|
if (!handoff)
|
|
41
46
|
return keys.slice();
|
|
42
|
-
|
|
47
|
+
// 2026-09-21:metadata 非对象(模型双重编码字符串穿透 {type:'json'} 宽参数)时,m[k] 对字符串
|
|
48
|
+
// 索引静默 undefined → 误导性 "delivery required" 缺键报错。此处报类型真因(纵深防线)。
|
|
49
|
+
const rawMeta = handoff.metadata ?? {};
|
|
50
|
+
const metaOk = typeof rawMeta === 'object' && rawMeta !== null && !Array.isArray(rawMeta);
|
|
51
|
+
const m = metaOk ? rawMeta : null;
|
|
43
52
|
// 推导修正:kbUrlBase === undefined 才是宽松;'' 是 local strict
|
|
44
53
|
const hasBase = kbUrlBase !== undefined;
|
|
45
54
|
const base = hasBase ? kbUrlBase.replace(/\/$/, '') : null;
|
|
@@ -50,6 +59,10 @@ export function missingDeliveryKeys(assignee, mode, handoff, kbUrlBase) {
|
|
|
50
59
|
missing.push(...missingPtDecisionKeys(handoff));
|
|
51
60
|
continue;
|
|
52
61
|
}
|
|
62
|
+
if (!metaOk) {
|
|
63
|
+
missing.push(`${k} (metadata must be an object, got ${Array.isArray(rawMeta) ? 'array' : typeof rawMeta})`);
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
53
66
|
const v = m[k];
|
|
54
67
|
if (strict && k === 'kb_url' && base === '') {
|
|
55
68
|
// local 模式:kb_url 必须显式空串(非缺失键)
|
|
@@ -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,10 +37,24 @@ 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 可检索。 */
|
|
25
50
|
export declare function buildChecklistTitle(c: PlanningChecklist): string;
|
|
26
51
|
/** 需求澄清清单落库 body:标题【需求】+ 各段可读 markdown(非裸 JSON)。KB 与临时目录两分支共用。 */
|
|
27
52
|
export declare function formatChecklistBody(c: PlanningChecklist): string;
|
|
53
|
+
/** 从清单页原文提取机读 PlanningChecklist(无损恢复路径):无段/损坏/校验不过一律返回 null,
|
|
54
|
+
* 由调用方回退 legacy LLM 重建流程。domain 纯函数,无副作用。 */
|
|
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[];
|