@webpresso/agent-kit 3.1.28 → 3.1.30
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/catalog/AGENTS.md.tpl +4 -4
- package/catalog/agent/rules/ci-cost-local-first.md +139 -0
- package/catalog/agent/rules/ci-test-perf.md +5 -1
- package/catalog/agent/rules/cmd-execution.md +9 -0
- package/catalog/agent/rules/pre-implementation.md +2 -1
- package/catalog/agent/rules/test-scan-perf.md +80 -0
- package/catalog/agent/rules/workflow-skills-routing.md +4 -2
- package/catalog/agent/skills/fix/SKILL.md +3 -0
- package/catalog/agent/skills/verify/SKILL.md +18 -0
- package/dist/esm/audit/blueprint-pr-coverage.js +68 -7
- package/dist/esm/audit/registry.d.ts +4 -4
- package/dist/esm/audit/registry.js +3 -0
- package/dist/esm/audit/roadmap-links.js +6 -0
- package/dist/esm/audit/test-scan-perf.d.ts +18 -0
- package/dist/esm/audit/test-scan-perf.js +275 -0
- package/dist/esm/blueprint/lifecycle/engine.d.ts +9 -0
- package/dist/esm/blueprint/lifecycle/engine.js +53 -6
- package/dist/esm/blueprint/lifecycle/review-provenance.d.ts +6 -0
- package/dist/esm/blueprint/lifecycle/review-provenance.js +93 -12
- package/dist/esm/blueprint/markdown/helpers.js +6 -2
- package/dist/esm/ci/act-replay.js +110 -7
- package/dist/esm/ci/act-runner.d.ts +4 -0
- package/dist/esm/ci/act-runner.js +85 -26
- package/dist/esm/ci/act-worktree-git.d.ts +11 -0
- package/dist/esm/ci/act-worktree-git.js +219 -0
- package/dist/esm/cli/commands/audit.js +3 -0
- package/dist/esm/cli/commands/blueprint/mutations.js +35 -18
- package/dist/esm/cli/commands/blueprint/router.js +27 -2
- package/dist/esm/cli/commands/ci.d.ts +8 -1
- package/dist/esm/cli/commands/ci.js +80 -21
- package/dist/esm/cli/commands/dash/keymap.js +1 -0
- package/dist/esm/cli/commands/dash/launch-preferences.d.ts +15 -0
- package/dist/esm/cli/commands/dash/launch-preferences.js +46 -17
- package/dist/esm/cli/commands/dash/plan-once-label.d.ts +7 -0
- package/dist/esm/cli/commands/dash/plan-once-label.js +5 -0
- package/dist/esm/cli/commands/dash/plan-once-membership.d.ts +67 -0
- package/dist/esm/cli/commands/dash/plan-once-membership.js +151 -0
- package/dist/esm/cli/commands/dash/plan-once-merge.d.ts +143 -0
- package/dist/esm/cli/commands/dash/plan-once-merge.js +238 -0
- package/dist/esm/cli/commands/dash/plan-once.d.ts +22 -2
- package/dist/esm/cli/commands/dash/plan-once.js +41 -4
- package/dist/esm/cli/commands/dash/tui/attention-hud.d.ts +9 -0
- package/dist/esm/cli/commands/dash/tui/attention-hud.js +9 -0
- package/dist/esm/cli/commands/dash/tui/chat-bridge.d.ts +41 -0
- package/dist/esm/cli/commands/dash/tui/chat-bridge.js +73 -0
- package/dist/esm/cli/commands/dash/tui/chat-mode.d.ts +47 -0
- package/dist/esm/cli/commands/dash/tui/chat-mode.js +98 -0
- package/dist/esm/cli/commands/dash/tui/frame.d.ts +3 -0
- package/dist/esm/cli/commands/dash/tui/frame.js +3 -1
- package/dist/esm/cli/commands/dash/tui/palette.d.ts +1 -1
- package/dist/esm/cli/commands/dash/tui/palette.js +14 -0
- package/dist/esm/cli/commands/dash/tui/runtime.js +239 -4
- package/dist/esm/cli/commands/dash/tui/state.d.ts +41 -0
- package/dist/esm/cli/commands/dash/tui/state.js +257 -4
- package/dist/esm/cli/commands/format.js +0 -2
- package/dist/esm/cli/commands/init/config.d.ts +0 -1
- package/dist/esm/cli/commands/init/config.js +1 -4
- package/dist/esm/cli/commands/init/scaffolders/agent-hooks/emitters/claude.d.ts +5 -6
- package/dist/esm/cli/commands/init/scaffolders/agent-hooks/emitters/claude.js +6 -13
- package/dist/esm/cli/commands/init/scaffolders/agent-hooks/ir.d.ts +0 -2
- package/dist/esm/cli/commands/init/scaffolders/agent-hooks/ir.js +0 -2
- package/dist/esm/cli/commands/init/scaffolders/agent-hooks/merge.d.ts +6 -3
- package/dist/esm/cli/commands/init/scaffolders/agent-hooks/merge.js +39 -26
- package/dist/esm/cli/commands/init/scaffolders/codex-mcp/index.js +61 -23
- package/dist/esm/cli/commands/logs.d.ts +15 -0
- package/dist/esm/cli/commands/logs.js +192 -3
- package/dist/esm/cli/commands/quality-log-store.d.ts +1 -0
- package/dist/esm/cli/commands/quality-log-store.js +187 -51
- package/dist/esm/cli/commands/review.js +5 -8
- package/dist/esm/cli/commands/secrets.d.ts +1 -0
- package/dist/esm/cli/commands/secrets.js +4 -1
- package/dist/esm/cli/direct-provider-launch.js +25 -1
- package/dist/esm/hooks/doctor.d.ts +15 -0
- package/dist/esm/hooks/doctor.js +56 -0
- package/dist/esm/hooks/pretool-guard/validators/forbidden-commands.js +4 -4
- package/dist/esm/hooks/pretool-guard/validators/mcp-redirect.d.ts +0 -1
- package/dist/esm/hooks/pretool-guard/validators/mcp-redirect.js +8 -5
- package/dist/esm/hooks/pretool-guard/validators/worktree-discipline.d.ts +9 -7
- package/dist/esm/hooks/pretool-guard/validators/worktree-discipline.js +126 -14
- package/dist/esm/mcp/tools/_shared/audit-kinds.d.ts +1 -1
- package/dist/esm/mcp/tools/audit.d.ts +2 -1
- package/dist/esm/mcp/tools/audit.js +10 -0
- package/dist/esm/mcp/tools/audits.d.ts +1 -0
- package/dist/esm/mcp/tools/ci-act.js +9 -11
- package/dist/esm/paths/state-root.js +3 -0
- package/dist/esm/review/authority.js +17 -2
- package/dist/esm/review/availability.js +18 -5
- package/dist/esm/review/subject.d.ts +15 -0
- package/dist/esm/review/subject.js +105 -10
- package/dist/esm/runtime/executor.d.ts +1 -0
- package/dist/esm/runtime/executor.js +2 -1
- package/dist/esm/secret-gate/runner.d.ts +1 -0
- package/dist/esm/secret-gate/runner.js +4 -1
- package/dist/esm/worktrees/current.d.ts +1 -0
- package/dist/esm/worktrees/current.js +1 -0
- package/dist/esm/worktrees/project-agent-surfaces.d.ts +1 -0
- package/dist/esm/worktrees/project-agent-surfaces.js +2 -1
- package/package.json +14 -14
|
@@ -7,6 +7,7 @@ import { CONTROL_PLANE_HOSTS } from "#cli/commands/dash/tui/hosts.js";
|
|
|
7
7
|
import { isWizardProvider } from "#cli/commands/dash/session-wizard.js";
|
|
8
8
|
import { browserOverlayLines, createBlueprintBrowser, reduceBlueprintBrowserKey, } from "#cli/commands/dash/tui/browser.js";
|
|
9
9
|
import { filterWorktreePickerItems } from "#cli/commands/dash/worktree-picker.js";
|
|
10
|
+
import { DEFAULT_CONTROL_PLANE_CHAT_MODE, reduceChatModeSwitch, toggleControlPlaneChatMode, } from "#cli/commands/dash/tui/chat-mode.js";
|
|
10
11
|
export function createInitialTuiState(input) {
|
|
11
12
|
const menu = {
|
|
12
13
|
selectedKey: input.agents[0] ? `group:${input.agents[0].key ?? "claude"}` : null,
|
|
@@ -16,6 +17,10 @@ export function createInitialTuiState(input) {
|
|
|
16
17
|
menu: reconcileSelection(menu, input.agents),
|
|
17
18
|
agents: input.agents,
|
|
18
19
|
permissionMode: input.permissionMode,
|
|
20
|
+
chatMode: input.chatMode ?? DEFAULT_CONTROL_PLANE_CHAT_MODE,
|
|
21
|
+
chatSession: null,
|
|
22
|
+
chatDraft: "",
|
|
23
|
+
chatOpenGeneration: 0,
|
|
19
24
|
overlay: { kind: "none" },
|
|
20
25
|
status: null,
|
|
21
26
|
warnings: [],
|
|
@@ -53,7 +58,7 @@ export function reduceTuiKey(state, key) {
|
|
|
53
58
|
if (state.overlay.kind === "help") {
|
|
54
59
|
if (key.kind === "escape" ||
|
|
55
60
|
(key.kind === "char" && (key.value === "?" || key.value === "q"))) {
|
|
56
|
-
return { state:
|
|
61
|
+
return { state: dismissTransientOverlay(state), effects: [] };
|
|
57
62
|
}
|
|
58
63
|
return { state, effects: [] };
|
|
59
64
|
}
|
|
@@ -63,6 +68,10 @@ export function reduceTuiKey(state, key) {
|
|
|
63
68
|
if (state.overlay.kind === "browser" && state.browser) {
|
|
64
69
|
return reduceBrowserKey(state, key);
|
|
65
70
|
}
|
|
71
|
+
// Integrated-chat overlay uses browser chrome without blueprint browser state.
|
|
72
|
+
if (state.overlay.kind === "browser" && !state.browser && state.chatMode === "integrated-chat") {
|
|
73
|
+
return reduceIntegratedChatKey(state, key);
|
|
74
|
+
}
|
|
66
75
|
if (state.overlay.kind === "wizard" && state.wizard) {
|
|
67
76
|
return reduceWizardKey(state, key);
|
|
68
77
|
}
|
|
@@ -110,6 +119,19 @@ export function reduceTuiKey(state, key) {
|
|
|
110
119
|
effects: [{ kind: "launch", command: "wp fleet watch", provider: "fleet", newRun: true }],
|
|
111
120
|
};
|
|
112
121
|
}
|
|
122
|
+
if (key.kind === "char" && key.value === "m") {
|
|
123
|
+
return {
|
|
124
|
+
state: {
|
|
125
|
+
...state,
|
|
126
|
+
browser: null,
|
|
127
|
+
overlay: { kind: "browser", title: "Plan-once merge", lines: ["loading…"] },
|
|
128
|
+
},
|
|
129
|
+
effects: [{ kind: "palette-command", id: "merge.status" }],
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
if (key.kind === "char" && key.value === "c") {
|
|
133
|
+
return toggleChatMode(state);
|
|
134
|
+
}
|
|
113
135
|
if (key.kind === "char" && key.value === "n") {
|
|
114
136
|
const provider = wizardProviderFromSelection(state) ?? "claude";
|
|
115
137
|
const blocked = hostBlockedReason(state, provider);
|
|
@@ -269,7 +291,7 @@ function reducePaletteKey(state, key) {
|
|
|
269
291
|
const { query, cursor } = state.overlay;
|
|
270
292
|
const matches = filterPaletteCommands(query);
|
|
271
293
|
if (key.kind === "escape" || (key.kind === "ctrl" && key.value === "c")) {
|
|
272
|
-
return { state:
|
|
294
|
+
return { state: dismissTransientOverlay(state), effects: [] };
|
|
273
295
|
}
|
|
274
296
|
if (key.kind === "backspace") {
|
|
275
297
|
const nextQuery = query.slice(0, -1);
|
|
@@ -310,8 +332,9 @@ function reducePaletteKey(state, key) {
|
|
|
310
332
|
if (key.kind === "enter") {
|
|
311
333
|
const command = matches[cursor];
|
|
312
334
|
if (!command)
|
|
313
|
-
return { state:
|
|
314
|
-
|
|
335
|
+
return { state: dismissTransientOverlay(state), effects: [] };
|
|
336
|
+
// Dismiss palette first; integrated-chat restores compose overlay when still active.
|
|
337
|
+
return applyPaletteCommand(dismissTransientOverlay(state), command.id);
|
|
315
338
|
}
|
|
316
339
|
if (key.kind === "char") {
|
|
317
340
|
const nextQuery = query + key.value;
|
|
@@ -433,6 +456,21 @@ export function applyPaletteCommand(state, id) {
|
|
|
433
456
|
return reduceTuiKey(state, { kind: "char", value: "b" });
|
|
434
457
|
case "fleet.open":
|
|
435
458
|
return reduceTuiKey(state, { kind: "char", value: "f" });
|
|
459
|
+
case "merge.status":
|
|
460
|
+
return reduceTuiKey(state, { kind: "char", value: "m" });
|
|
461
|
+
case "chat.mode.toggle":
|
|
462
|
+
return toggleChatMode(state);
|
|
463
|
+
case "chat.mode.open":
|
|
464
|
+
// Runtime-owned open; pure path only re-emits when already requesting open.
|
|
465
|
+
return {
|
|
466
|
+
state,
|
|
467
|
+
effects: [{ kind: "palette-command", id: "chat.mode.open" }],
|
|
468
|
+
};
|
|
469
|
+
case "chat.mode.send":
|
|
470
|
+
return {
|
|
471
|
+
state,
|
|
472
|
+
effects: [{ kind: "palette-command", id: "chat.mode.send" }],
|
|
473
|
+
};
|
|
436
474
|
case "permission.toggle":
|
|
437
475
|
return reduceTuiKey(state, { kind: "char", value: "d" });
|
|
438
476
|
case "help.open":
|
|
@@ -445,6 +483,221 @@ export function applyPaletteCommand(state, id) {
|
|
|
445
483
|
return { state, effects: [] };
|
|
446
484
|
}
|
|
447
485
|
}
|
|
486
|
+
/**
|
|
487
|
+
* Toggle host-pane ↔ integrated-chat. Integrated chat requires an ACP-capable
|
|
488
|
+
* selected host and an existing dash run identity (no second registry).
|
|
489
|
+
*/
|
|
490
|
+
export function toggleChatMode(state) {
|
|
491
|
+
if (state.chatMode === "integrated-chat") {
|
|
492
|
+
return leaveIntegratedChat(state);
|
|
493
|
+
}
|
|
494
|
+
const host = selectedDashProvider(state);
|
|
495
|
+
const target = toggleControlPlaneChatMode(state.chatMode);
|
|
496
|
+
const decision = reduceChatModeSwitch({
|
|
497
|
+
mode: state.chatMode,
|
|
498
|
+
target,
|
|
499
|
+
host,
|
|
500
|
+
});
|
|
501
|
+
if (!decision.ok) {
|
|
502
|
+
// Refused: no persist effect.
|
|
503
|
+
return { state: { ...state, status: decision.error }, effects: [] };
|
|
504
|
+
}
|
|
505
|
+
const run = closeTargetFromState(state);
|
|
506
|
+
if (!run?.runId) {
|
|
507
|
+
return {
|
|
508
|
+
state: {
|
|
509
|
+
...state,
|
|
510
|
+
status: "Select a host session (run) before switching to integrated chat",
|
|
511
|
+
},
|
|
512
|
+
effects: [],
|
|
513
|
+
};
|
|
514
|
+
}
|
|
515
|
+
const nextGen = state.chatOpenGeneration + 1;
|
|
516
|
+
return {
|
|
517
|
+
state: {
|
|
518
|
+
...state,
|
|
519
|
+
chatMode: "integrated-chat",
|
|
520
|
+
chatDraft: "",
|
|
521
|
+
chatOpenGeneration: nextGen,
|
|
522
|
+
browser: null,
|
|
523
|
+
wizard: null,
|
|
524
|
+
status: "mode:integrated-chat (opening…)",
|
|
525
|
+
overlay: {
|
|
526
|
+
kind: "browser",
|
|
527
|
+
title: "Integrated chat",
|
|
528
|
+
lines: ["opening chat bridge…", "type to compose · ⏎ send · esc → host-pane"],
|
|
529
|
+
},
|
|
530
|
+
},
|
|
531
|
+
// Persist after bridge open succeeds in runtime; open effect only here.
|
|
532
|
+
effects: [{ kind: "palette-command", id: "chat.mode.open" }],
|
|
533
|
+
};
|
|
534
|
+
}
|
|
535
|
+
/**
|
|
536
|
+
* After agents load: if preferred is integrated-chat and gates pass, apply the
|
|
537
|
+
* normal toggle path once. Never forces integrated-chat chrome without host/run.
|
|
538
|
+
*/
|
|
539
|
+
export function applyPreferredChatModeSeed(state, preferred) {
|
|
540
|
+
if (preferred === "host-pane") {
|
|
541
|
+
if (state.chatMode === "host-pane")
|
|
542
|
+
return { state, effects: [] };
|
|
543
|
+
return leaveIntegratedChat(state);
|
|
544
|
+
}
|
|
545
|
+
// preferred integrated-chat
|
|
546
|
+
if (state.chatMode === "integrated-chat")
|
|
547
|
+
return { state, effects: [] };
|
|
548
|
+
return toggleChatMode(state);
|
|
549
|
+
}
|
|
550
|
+
function leaveIntegratedChat(state) {
|
|
551
|
+
return {
|
|
552
|
+
state: {
|
|
553
|
+
...state,
|
|
554
|
+
chatMode: "host-pane",
|
|
555
|
+
chatSession: null,
|
|
556
|
+
chatDraft: "",
|
|
557
|
+
// Bump generation so in-flight open/send cannot re-enter chat mode.
|
|
558
|
+
chatOpenGeneration: state.chatOpenGeneration + 1,
|
|
559
|
+
overlay: { kind: "none" },
|
|
560
|
+
browser: null,
|
|
561
|
+
status: "mode:host-pane",
|
|
562
|
+
},
|
|
563
|
+
effects: [{ kind: "persist-chat-mode", mode: "host-pane" }],
|
|
564
|
+
};
|
|
565
|
+
}
|
|
566
|
+
function reduceIntegratedChatKey(state, key) {
|
|
567
|
+
// Escape (or Ctrl+C with empty draft) returns to host-pane. Printable `c` is
|
|
568
|
+
// a normal composer character so operators can type freely.
|
|
569
|
+
if (key.kind === "escape") {
|
|
570
|
+
return leaveIntegratedChat(state);
|
|
571
|
+
}
|
|
572
|
+
// Empty draft: pass through menu global chords (?, q, palette, help).
|
|
573
|
+
if (state.chatDraft.length === 0) {
|
|
574
|
+
if (key.kind === "ctrl" && (key.value === "p" || key.value === "k")) {
|
|
575
|
+
return {
|
|
576
|
+
state: { ...state, overlay: { kind: "palette", query: "", cursor: 0 } },
|
|
577
|
+
effects: [],
|
|
578
|
+
};
|
|
579
|
+
}
|
|
580
|
+
if (key.kind === "ctrl" && key.value === "x") {
|
|
581
|
+
return { state: { ...state, overlay: { kind: "help" } }, effects: [] };
|
|
582
|
+
}
|
|
583
|
+
if (key.kind === "char" && key.value === "q") {
|
|
584
|
+
return { state: { ...state, quit: true }, effects: [{ kind: "quit" }] };
|
|
585
|
+
}
|
|
586
|
+
if (key.kind === "char" && key.value === "?") {
|
|
587
|
+
return { state: { ...state, overlay: { kind: "help" } }, effects: [] };
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
if (key.kind === "ctrl" && key.value === "c") {
|
|
591
|
+
if (state.chatDraft.length === 0)
|
|
592
|
+
return leaveIntegratedChat(state);
|
|
593
|
+
return {
|
|
594
|
+
state: withChatOverlay({ ...state, chatDraft: "" }),
|
|
595
|
+
effects: [],
|
|
596
|
+
};
|
|
597
|
+
}
|
|
598
|
+
if (key.kind === "backspace") {
|
|
599
|
+
if (state.chatDraft.length === 0)
|
|
600
|
+
return { state, effects: [] };
|
|
601
|
+
return {
|
|
602
|
+
state: withChatOverlay({ ...state, chatDraft: state.chatDraft.slice(0, -1) }),
|
|
603
|
+
effects: [],
|
|
604
|
+
};
|
|
605
|
+
}
|
|
606
|
+
if (key.kind === "enter") {
|
|
607
|
+
const text = state.chatDraft.trim();
|
|
608
|
+
if (!text) {
|
|
609
|
+
return { state: { ...state, status: "type a message before send" }, effects: [] };
|
|
610
|
+
}
|
|
611
|
+
if (!state.chatSession) {
|
|
612
|
+
return {
|
|
613
|
+
state: { ...state, status: "chat session not open yet" },
|
|
614
|
+
effects: [],
|
|
615
|
+
};
|
|
616
|
+
}
|
|
617
|
+
// Keep draft until runtime drains it after send.
|
|
618
|
+
return {
|
|
619
|
+
state: {
|
|
620
|
+
...withChatOverlay({
|
|
621
|
+
...state,
|
|
622
|
+
chatDraft: text,
|
|
623
|
+
status: "sending…",
|
|
624
|
+
}),
|
|
625
|
+
},
|
|
626
|
+
effects: [{ kind: "palette-command", id: "chat.mode.send" }],
|
|
627
|
+
};
|
|
628
|
+
}
|
|
629
|
+
if (key.kind === "char") {
|
|
630
|
+
const nextDraft = state.chatDraft + key.value;
|
|
631
|
+
return {
|
|
632
|
+
state: withChatOverlay({ ...state, chatDraft: nextDraft }),
|
|
633
|
+
effects: [],
|
|
634
|
+
};
|
|
635
|
+
}
|
|
636
|
+
return { state, effects: [] };
|
|
637
|
+
}
|
|
638
|
+
function withChatOverlay(state) {
|
|
639
|
+
return {
|
|
640
|
+
...state,
|
|
641
|
+
overlay: {
|
|
642
|
+
kind: "browser",
|
|
643
|
+
title: "Integrated chat",
|
|
644
|
+
lines: formatIntegratedChatLines(state),
|
|
645
|
+
},
|
|
646
|
+
};
|
|
647
|
+
}
|
|
648
|
+
/** Dismiss help/palette without stranding integrated-chat (restore browser compose overlay). */
|
|
649
|
+
function dismissTransientOverlay(state) {
|
|
650
|
+
if (state.chatMode === "integrated-chat") {
|
|
651
|
+
return withChatOverlay(state);
|
|
652
|
+
}
|
|
653
|
+
return { ...state, overlay: { kind: "none" } };
|
|
654
|
+
}
|
|
655
|
+
function formatIntegratedChatLines(state) {
|
|
656
|
+
const session = state.chatSession;
|
|
657
|
+
const body = session
|
|
658
|
+
? [
|
|
659
|
+
`chat · ${session.host} · run ${session.runId.slice(0, 8)}`,
|
|
660
|
+
session.label ? `label · ${session.label}` : "label · —",
|
|
661
|
+
"scope · integrated-chat (⏎ send · esc → host-pane)",
|
|
662
|
+
"─".repeat(32),
|
|
663
|
+
...session.messages.map((msg) => {
|
|
664
|
+
const tag = msg.role === "user" ? "you" : msg.role === "assistant" ? "host" : "sys";
|
|
665
|
+
return `${tag} · ${msg.text}`;
|
|
666
|
+
}),
|
|
667
|
+
]
|
|
668
|
+
: ["opening chat bridge…"];
|
|
669
|
+
return [...body, "", `compose> ${state.chatDraft}█`];
|
|
670
|
+
}
|
|
671
|
+
function selectedDashProvider(state) {
|
|
672
|
+
const host = wizardProviderFromSelection(state);
|
|
673
|
+
return host && isDashProvider(host) ? host : null;
|
|
674
|
+
}
|
|
675
|
+
/** Runtime applies hermetic/real bridge result into state + overlay. */
|
|
676
|
+
export function applyChatBridgeSession(state, session, error, options = {}) {
|
|
677
|
+
// Stale completion: operator already left or re-entered chat.
|
|
678
|
+
if (options.expectedGeneration !== undefined &&
|
|
679
|
+
options.expectedGeneration !== state.chatOpenGeneration) {
|
|
680
|
+
return state;
|
|
681
|
+
}
|
|
682
|
+
if (error || !session) {
|
|
683
|
+
return {
|
|
684
|
+
...state,
|
|
685
|
+
chatMode: "host-pane",
|
|
686
|
+
chatSession: null,
|
|
687
|
+
chatDraft: "",
|
|
688
|
+
overlay: { kind: "none" },
|
|
689
|
+
status: error ?? "chat bridge failed",
|
|
690
|
+
};
|
|
691
|
+
}
|
|
692
|
+
const next = {
|
|
693
|
+
...state,
|
|
694
|
+
chatMode: "integrated-chat",
|
|
695
|
+
chatSession: session,
|
|
696
|
+
browser: null,
|
|
697
|
+
status: `mode:integrated-chat · ${session.host}`,
|
|
698
|
+
};
|
|
699
|
+
return withChatOverlay(next);
|
|
700
|
+
}
|
|
448
701
|
/** Open the worktree/permission wizard with a real wizard object (not overlay-only). */
|
|
449
702
|
export function openLaunchWizard(state, provider) {
|
|
450
703
|
return {
|
|
@@ -105,11 +105,8 @@ function parseConfigFile(path) {
|
|
|
105
105
|
const overrides = Array.isArray(rules?.overrides) ? rules.overrides : [];
|
|
106
106
|
const durablePlanningRoot = readOptionalString(parsed.durablePlanningRoot);
|
|
107
107
|
const blueprintsDir = readOptionalString(parsed.blueprintsDir);
|
|
108
|
-
const serverName = readOptionalString(mcp?.serverName);
|
|
109
108
|
const toolPrefix = readOptionalString(mcp?.toolPrefix);
|
|
110
|
-
const normalizedMcp =
|
|
111
|
-
? { ...(serverName ? { serverName } : {}), ...(toolPrefix ? { toolPrefix } : {}) }
|
|
112
|
-
: undefined;
|
|
109
|
+
const normalizedMcp = toolPrefix ? { toolPrefix } : undefined;
|
|
113
110
|
const guard = parsed.guard;
|
|
114
111
|
const packageManager = guard?.packageManager === "vp-only" ? "vp-only" : undefined;
|
|
115
112
|
const rawScriptRoutes = guard?.scriptRoutes && typeof guard.scriptRoutes === "object"
|
|
@@ -12,12 +12,11 @@ import type { HooksMap, MatcherSet } from "#cli/commands/init/scaffolders/agent-
|
|
|
12
12
|
* event key. Specs with a `matcher` key reference the corresponding field
|
|
13
13
|
* in the provided MatcherSet.
|
|
14
14
|
*
|
|
15
|
-
* PreToolUse (pretool-guard)
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
* (e.g. `rm -rf`) for non-git Bash (residual: git Bash may hit both groups).
|
|
15
|
+
* PreToolUse (pretool-guard) keeps the Bash matcher in its own group but emits
|
|
16
|
+
* exactly one guard entry per tool, so the guard spawns once per call. A prior
|
|
17
|
+
* git-scoped `if: Bash(git *)` group was removed: its command was byte-identical
|
|
18
|
+
* to the Bash catch-all, so it added no enforcement, doubled guard spawns on
|
|
19
|
+
* git commands, and tripped the hook-surface duplicate-managed-hook audit.
|
|
21
20
|
*/
|
|
22
21
|
export declare function buildClaudeHookGroups(input: {
|
|
23
22
|
resolveBin: (name: string) => string;
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* Extracted from buildWebpressoHookGroups in index.ts. The function
|
|
6
6
|
* buildWebpressoHookGroups in index.ts now delegates here for backward compat.
|
|
7
7
|
*/
|
|
8
|
-
import {
|
|
8
|
+
import { WP_HOOK_SPECS } from "#cli/commands/init/scaffolders/agent-hooks/ir.js";
|
|
9
9
|
function pushGroup(result, event, group) {
|
|
10
10
|
const existing = result[event] ?? [];
|
|
11
11
|
result[event] = [...existing, group];
|
|
@@ -28,12 +28,11 @@ function splitPreToolMatchers(matcher) {
|
|
|
28
28
|
* event key. Specs with a `matcher` key reference the corresponding field
|
|
29
29
|
* in the provided MatcherSet.
|
|
30
30
|
*
|
|
31
|
-
* PreToolUse (pretool-guard)
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
* (e.g. `rm -rf`) for non-git Bash (residual: git Bash may hit both groups).
|
|
31
|
+
* PreToolUse (pretool-guard) keeps the Bash matcher in its own group but emits
|
|
32
|
+
* exactly one guard entry per tool, so the guard spawns once per call. A prior
|
|
33
|
+
* git-scoped `if: Bash(git *)` group was removed: its command was byte-identical
|
|
34
|
+
* to the Bash catch-all, so it added no enforcement, doubled guard spawns on
|
|
35
|
+
* git commands, and tripped the hook-surface duplicate-managed-hook audit.
|
|
37
36
|
*/
|
|
38
37
|
export function buildClaudeHookGroups(input) {
|
|
39
38
|
const { resolveBin, matchers } = input;
|
|
@@ -54,12 +53,6 @@ export function buildClaudeHookGroups(input) {
|
|
|
54
53
|
});
|
|
55
54
|
}
|
|
56
55
|
if (hasBash) {
|
|
57
|
-
// Git-hot path: only spawn when Bash looks like git (spawn reduction).
|
|
58
|
-
pushGroup(result, "PreToolUse", {
|
|
59
|
-
matcher: "Bash",
|
|
60
|
-
hooks: [{ ...baseHook, if: CLAUDE_PRETOOL_GIT_IF }],
|
|
61
|
-
});
|
|
62
|
-
// Security catch-all for non-git Bash (catastrophic commands, etc.).
|
|
63
56
|
pushGroup(result, "PreToolUse", {
|
|
64
57
|
matcher: "Bash",
|
|
65
58
|
hooks: [baseHook],
|
|
@@ -22,8 +22,6 @@ export type HookGroup = {
|
|
|
22
22
|
hooks: HookEntry[];
|
|
23
23
|
};
|
|
24
24
|
export type HooksMap = Record<string, HookGroup[]>;
|
|
25
|
-
/** Claude-only PreToolUse `if` for git-hot Bash paths (commit-author / git discipline). */
|
|
26
|
-
export declare const CLAUDE_PRETOOL_GIT_IF = "Bash(git *)";
|
|
27
25
|
export declare const HOOK_EVENT_NAMES: readonly ["SessionStart", "PreToolUse", "PostToolUse", "PostToolUseFailure", "UserPromptSubmit", "Stop", "PermissionRequest", "SubagentStart", "SubagentStop", "SessionEnd", "PreCompact", "PostCompact"];
|
|
28
26
|
export type HookEventName = (typeof HOOK_EVENT_NAMES)[number];
|
|
29
27
|
export type MatcherSet = {
|
|
@@ -7,8 +7,6 @@
|
|
|
7
7
|
* output formats.
|
|
8
8
|
*/
|
|
9
9
|
import { assertValidHookSpecs } from "./contract.js";
|
|
10
|
-
/** Claude-only PreToolUse `if` for git-hot Bash paths (commit-author / git discipline). */
|
|
11
|
-
export const CLAUDE_PRETOOL_GIT_IF = "Bash(git *)";
|
|
12
10
|
// Known hook event names across the documented Claude/Codex lifecycle. Used by
|
|
13
11
|
// migration/validation paths to identify legacy flat-form keys and by CLI
|
|
14
12
|
// helpers like `wp hooks dispatch` / `wp hooks demo` to accept a wider event
|
|
@@ -6,9 +6,12 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import type { HookGroup, HooksMap } from "./ir.js";
|
|
8
8
|
/**
|
|
9
|
-
* Ensure `group` is present in `groups`.
|
|
10
|
-
*
|
|
11
|
-
* (matcher, timeout, if
|
|
9
|
+
* Ensure `group` is present in `groups`. The first hook with the same managed
|
|
10
|
+
* identity (command target + matcher) is updated to mirror the incoming spec
|
|
11
|
+
* metadata (matcher, timeout, `if` — dropped when the spec no longer sets it)
|
|
12
|
+
* while preserving the consumer's materialized command form. Any further hooks
|
|
13
|
+
* with the same identity are stale spec revisions (e.g. a legacy `if`-gated
|
|
14
|
+
* duplicate) and are pruned so repair converges to one entry per identity.
|
|
12
15
|
* If no matching hook is found, append the group.
|
|
13
16
|
*/
|
|
14
17
|
export declare function ensureGroup(groups: HookGroup[], group: HookGroup): HookGroup[];
|
|
@@ -36,18 +36,16 @@ function commandMatches(left, right) {
|
|
|
36
36
|
return leftTarget !== null && extractCommandTarget(right) === leftTarget;
|
|
37
37
|
}
|
|
38
38
|
/**
|
|
39
|
-
* Managed hooks
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
39
|
+
* Managed hooks may emit multiple groups that share the same command target
|
|
40
|
+
* only when their matchers differ (e.g. Claude PreToolUse: non-Bash group +
|
|
41
|
+
* Bash group). Identity is (command target, matcher). The permission-rule `if`
|
|
42
|
+
* is spec-owned metadata, not identity: treating it as identity let a spec
|
|
43
|
+
* revision that changed `if` strand the old entry as a duplicate that
|
|
44
|
+
* `wp audit hook-surface` then flagged (duplicate-managed-hook).
|
|
43
45
|
*/
|
|
44
46
|
function sameManagedHookIdentity(existingGroup, existingHook, incomingGroup, incomingHook) {
|
|
45
47
|
if (!commandMatches(existingHook.command, incomingHook.command))
|
|
46
48
|
return false;
|
|
47
|
-
const existingIf = typeof existingHook.if === "string" ? existingHook.if : "";
|
|
48
|
-
const incomingIf = typeof incomingHook.if === "string" ? incomingHook.if : "";
|
|
49
|
-
if (existingIf !== incomingIf)
|
|
50
|
-
return false;
|
|
51
49
|
const existingMatcher = existingGroup.matcher ?? "";
|
|
52
50
|
const incomingMatcher = incomingGroup.matcher ?? "";
|
|
53
51
|
if (existingMatcher === incomingMatcher)
|
|
@@ -57,38 +55,53 @@ function sameManagedHookIdentity(existingGroup, existingHook, incomingGroup, inc
|
|
|
57
55
|
return existingMatcher === "" || incomingMatcher === "";
|
|
58
56
|
}
|
|
59
57
|
/**
|
|
60
|
-
* Ensure `group` is present in `groups`.
|
|
61
|
-
*
|
|
62
|
-
* (matcher, timeout, if
|
|
58
|
+
* Ensure `group` is present in `groups`. The first hook with the same managed
|
|
59
|
+
* identity (command target + matcher) is updated to mirror the incoming spec
|
|
60
|
+
* metadata (matcher, timeout, `if` — dropped when the spec no longer sets it)
|
|
61
|
+
* while preserving the consumer's materialized command form. Any further hooks
|
|
62
|
+
* with the same identity are stale spec revisions (e.g. a legacy `if`-gated
|
|
63
|
+
* duplicate) and are pruned so repair converges to one entry per identity.
|
|
63
64
|
* If no matching hook is found, append the group.
|
|
64
65
|
*/
|
|
65
66
|
export function ensureGroup(groups, group) {
|
|
66
67
|
const incomingHook = group.hooks[0];
|
|
67
68
|
if (!incomingHook)
|
|
68
69
|
return groups;
|
|
69
|
-
let
|
|
70
|
-
const nextGroups =
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
70
|
+
let updated = false;
|
|
71
|
+
const nextGroups = [];
|
|
72
|
+
for (const existingGroup of groups) {
|
|
73
|
+
const hooks = [];
|
|
74
|
+
let matchedHere = false;
|
|
75
|
+
for (const hook of existingGroup.hooks) {
|
|
76
|
+
if (!sameManagedHookIdentity(existingGroup, hook, group, incomingHook)) {
|
|
77
|
+
hooks.push(hook);
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
if (updated)
|
|
81
|
+
continue; // stale duplicate of an already-updated identity
|
|
82
|
+
updated = true;
|
|
83
|
+
matchedHere = true;
|
|
84
|
+
const merged = {
|
|
77
85
|
...hook,
|
|
78
86
|
...incomingHook,
|
|
79
87
|
// Preserve the consumer's already-materialized command form when
|
|
80
88
|
// only the matcher/timeout/if changed. Codex command path migration is
|
|
81
89
|
// handled by normalizeCodexAgentKitCommands before this merge.
|
|
82
90
|
command: hook.command,
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
|
|
91
|
+
};
|
|
92
|
+
if (incomingHook.if === undefined)
|
|
93
|
+
delete merged.if;
|
|
94
|
+
hooks.push(merged);
|
|
95
|
+
}
|
|
96
|
+
if (hooks.length === 0)
|
|
97
|
+
continue; // group held only stale duplicates
|
|
98
|
+
nextGroups.push({
|
|
86
99
|
...existingGroup,
|
|
87
|
-
...(group.matcher !== undefined ? { matcher: group.matcher } : {}),
|
|
100
|
+
...(matchedHere && group.matcher !== undefined ? { matcher: group.matcher } : {}),
|
|
88
101
|
hooks,
|
|
89
|
-
};
|
|
90
|
-
}
|
|
91
|
-
if (
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
if (updated)
|
|
92
105
|
return nextGroups;
|
|
93
106
|
return [...groups, group];
|
|
94
107
|
}
|