@webpresso/agent-kit 3.1.26 → 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.
Files changed (150) hide show
  1. package/catalog/AGENTS.md.tpl +7 -11
  2. package/catalog/agent/commands/verify.md +2 -2
  3. package/catalog/agent/rules/changeset-release.md +4 -2
  4. package/catalog/agent/rules/ci-cost-local-first.md +139 -0
  5. package/catalog/agent/rules/ci-test-perf.md +5 -1
  6. package/catalog/agent/rules/cmd-execution.md +9 -0
  7. package/catalog/agent/rules/pre-implementation.md +2 -1
  8. package/catalog/agent/rules/test-scan-perf.md +80 -0
  9. package/catalog/agent/rules/workflow-skills-routing.md +9 -4
  10. package/catalog/agent/skills/autopilot/SKILL.md +48 -11
  11. package/catalog/agent/skills/claude/SKILL.md +7 -0
  12. package/catalog/agent/skills/codex/SKILL.md +7 -0
  13. package/catalog/agent/skills/fix/SKILL.md +36 -8
  14. package/catalog/agent/skills/grok/SKILL.md +7 -0
  15. package/catalog/agent/skills/investigate/SKILL.md +12 -1
  16. package/catalog/agent/skills/pll/SKILL.md +53 -48
  17. package/catalog/agent/skills/team/SKILL.md +26 -9
  18. package/catalog/agent/skills/ultragoal/SKILL.md +147 -19
  19. package/catalog/agent/skills/verify/SKILL.md +89 -51
  20. package/dist/esm/audit/blueprint-lifecycle-sql.d.ts +8 -0
  21. package/dist/esm/audit/blueprint-lifecycle-sql.js +25 -1
  22. package/dist/esm/audit/blueprint-pr-coverage.js +68 -7
  23. package/dist/esm/audit/registry.d.ts +4 -4
  24. package/dist/esm/audit/registry.js +3 -0
  25. package/dist/esm/audit/roadmap-links.js +6 -0
  26. package/dist/esm/audit/test-scan-perf.d.ts +18 -0
  27. package/dist/esm/audit/test-scan-perf.js +275 -0
  28. package/dist/esm/blueprint/lifecycle/engine.d.ts +9 -0
  29. package/dist/esm/blueprint/lifecycle/engine.js +53 -6
  30. package/dist/esm/blueprint/lifecycle/review-provenance.d.ts +6 -0
  31. package/dist/esm/blueprint/lifecycle/review-provenance.js +93 -12
  32. package/dist/esm/blueprint/markdown/helpers.js +6 -2
  33. package/dist/esm/ci/act-replay.js +110 -7
  34. package/dist/esm/ci/act-runner.d.ts +4 -0
  35. package/dist/esm/ci/act-runner.js +85 -26
  36. package/dist/esm/ci/act-worktree-git.d.ts +11 -0
  37. package/dist/esm/ci/act-worktree-git.js +219 -0
  38. package/dist/esm/ci/native-session-memory-cache.d.ts +9 -1
  39. package/dist/esm/ci/native-session-memory-cache.js +9 -1
  40. package/dist/esm/ci/vitest-ci-shards.d.ts +6 -4
  41. package/dist/esm/ci/vitest-ci-shards.js +6 -4
  42. package/dist/esm/cli/auto-update/guard-tombstone.d.ts +25 -0
  43. package/dist/esm/cli/auto-update/guard-tombstone.js +67 -0
  44. package/dist/esm/cli/auto-update/installer.d.ts +4 -17
  45. package/dist/esm/cli/auto-update/installer.js +6 -57
  46. package/dist/esm/cli/auto-update/self-invocation.d.ts +37 -0
  47. package/dist/esm/cli/auto-update/self-invocation.js +62 -0
  48. package/dist/esm/cli/commands/audit.js +4 -0
  49. package/dist/esm/cli/commands/blueprint/mutations.js +35 -18
  50. package/dist/esm/cli/commands/blueprint/router.js +27 -2
  51. package/dist/esm/cli/commands/ci.d.ts +8 -1
  52. package/dist/esm/cli/commands/ci.js +80 -21
  53. package/dist/esm/cli/commands/dash/index.js +3 -3
  54. package/dist/esm/cli/commands/dash/keymap.js +1 -0
  55. package/dist/esm/cli/commands/dash/launch-preferences.d.ts +15 -0
  56. package/dist/esm/cli/commands/dash/launch-preferences.js +46 -17
  57. package/dist/esm/cli/commands/dash/plan-once-attention.d.ts +32 -0
  58. package/dist/esm/cli/commands/dash/plan-once-attention.js +50 -0
  59. package/dist/esm/cli/commands/dash/plan-once-label.d.ts +7 -0
  60. package/dist/esm/cli/commands/dash/plan-once-label.js +5 -0
  61. package/dist/esm/cli/commands/dash/plan-once-membership.d.ts +67 -0
  62. package/dist/esm/cli/commands/dash/plan-once-membership.js +151 -0
  63. package/dist/esm/cli/commands/dash/plan-once-merge.d.ts +143 -0
  64. package/dist/esm/cli/commands/dash/plan-once-merge.js +238 -0
  65. package/dist/esm/cli/commands/dash/plan-once-schema.d.ts +73 -0
  66. package/dist/esm/cli/commands/dash/plan-once-schema.js +119 -0
  67. package/dist/esm/cli/commands/dash/plan-once.d.ts +142 -0
  68. package/dist/esm/cli/commands/dash/plan-once.js +261 -0
  69. package/dist/esm/cli/commands/dash/tui/attention-hud.d.ts +9 -0
  70. package/dist/esm/cli/commands/dash/tui/attention-hud.js +9 -0
  71. package/dist/esm/cli/commands/dash/tui/chat-bridge.d.ts +41 -0
  72. package/dist/esm/cli/commands/dash/tui/chat-bridge.js +73 -0
  73. package/dist/esm/cli/commands/dash/tui/chat-mode.d.ts +47 -0
  74. package/dist/esm/cli/commands/dash/tui/chat-mode.js +98 -0
  75. package/dist/esm/cli/commands/dash/tui/frame.d.ts +3 -0
  76. package/dist/esm/cli/commands/dash/tui/frame.js +3 -1
  77. package/dist/esm/cli/commands/dash/tui/ipc-protocol.d.ts +87 -0
  78. package/dist/esm/cli/commands/dash/tui/ipc-protocol.js +128 -0
  79. package/dist/esm/cli/commands/dash/tui/native-host.d.ts +34 -0
  80. package/dist/esm/cli/commands/dash/tui/native-host.js +256 -0
  81. package/dist/esm/cli/commands/dash/tui/native-path.d.ts +11 -0
  82. package/dist/esm/cli/commands/dash/tui/native-path.js +41 -0
  83. package/dist/esm/cli/commands/dash/tui/palette.d.ts +1 -1
  84. package/dist/esm/cli/commands/dash/tui/palette.js +14 -0
  85. package/dist/esm/cli/commands/dash/tui/runtime.js +311 -4
  86. package/dist/esm/cli/commands/dash/tui/state.d.ts +41 -0
  87. package/dist/esm/cli/commands/dash/tui/state.js +257 -4
  88. package/dist/esm/cli/commands/format.js +0 -2
  89. package/dist/esm/cli/commands/init/config.d.ts +0 -1
  90. package/dist/esm/cli/commands/init/config.js +1 -4
  91. package/dist/esm/cli/commands/init/scaffolders/agent-hooks/emitters/claude.d.ts +5 -6
  92. package/dist/esm/cli/commands/init/scaffolders/agent-hooks/emitters/claude.js +6 -13
  93. package/dist/esm/cli/commands/init/scaffolders/agent-hooks/ir.d.ts +0 -2
  94. package/dist/esm/cli/commands/init/scaffolders/agent-hooks/ir.js +0 -2
  95. package/dist/esm/cli/commands/init/scaffolders/agent-hooks/merge.d.ts +6 -3
  96. package/dist/esm/cli/commands/init/scaffolders/agent-hooks/merge.js +39 -26
  97. package/dist/esm/cli/commands/init/scaffolders/agent-kit-global/index.js +1 -1
  98. package/dist/esm/cli/commands/init/scaffolders/codex-mcp/index.js +61 -23
  99. package/dist/esm/cli/commands/logs.d.ts +15 -0
  100. package/dist/esm/cli/commands/logs.js +192 -3
  101. package/dist/esm/cli/commands/package-manager.js +2 -1
  102. package/dist/esm/cli/commands/quality-log-store.d.ts +1 -0
  103. package/dist/esm/cli/commands/quality-log-store.js +187 -51
  104. package/dist/esm/cli/commands/review.js +5 -8
  105. package/dist/esm/cli/commands/secrets.d.ts +1 -0
  106. package/dist/esm/cli/commands/secrets.js +4 -1
  107. package/dist/esm/cli/commands/self-install-guard.d.ts +14 -32
  108. package/dist/esm/cli/commands/self-install-guard.js +17 -63
  109. package/dist/esm/cli/direct-provider-launch.js +25 -1
  110. package/dist/esm/hooks/doctor.d.ts +15 -0
  111. package/dist/esm/hooks/doctor.js +56 -0
  112. package/dist/esm/hooks/guard-switch/index.js +8 -4
  113. package/dist/esm/hooks/post-tool/lint-after-edit.js +6 -4
  114. package/dist/esm/hooks/precompact/index.js +5 -3
  115. package/dist/esm/hooks/pretool-guard/validators/forbidden-commands.js +4 -4
  116. package/dist/esm/hooks/pretool-guard/validators/mcp-redirect.d.ts +0 -1
  117. package/dist/esm/hooks/pretool-guard/validators/mcp-redirect.js +8 -5
  118. package/dist/esm/hooks/pretool-guard/validators/worktree-discipline.d.ts +9 -7
  119. package/dist/esm/hooks/pretool-guard/validators/worktree-discipline.js +126 -14
  120. package/dist/esm/hooks/sessionstart/index.d.ts +10 -0
  121. package/dist/esm/hooks/sessionstart/index.js +61 -4
  122. package/dist/esm/hooks/shared/types.d.ts +11 -0
  123. package/dist/esm/hooks/shared/types.js +21 -0
  124. package/dist/esm/mcp/tools/_shared/audit-kinds.d.ts +1 -1
  125. package/dist/esm/mcp/tools/audit.d.ts +2 -1
  126. package/dist/esm/mcp/tools/audit.js +10 -0
  127. package/dist/esm/mcp/tools/audits.d.ts +1 -0
  128. package/dist/esm/mcp/tools/ci-act.js +9 -11
  129. package/dist/esm/paths/state-root.js +9 -0
  130. package/dist/esm/review/authority.js +17 -2
  131. package/dist/esm/review/availability.js +18 -5
  132. package/dist/esm/review/subject.d.ts +15 -0
  133. package/dist/esm/review/subject.js +105 -10
  134. package/dist/esm/runtime/executor.d.ts +1 -0
  135. package/dist/esm/runtime/executor.js +2 -1
  136. package/dist/esm/secret-gate/runner.d.ts +1 -0
  137. package/dist/esm/secret-gate/runner.js +4 -1
  138. package/dist/esm/session-memory/current-session.js +2 -0
  139. package/dist/esm/session-memory/native-runtime.js +5 -3
  140. package/dist/esm/session-memory/native-warm-markers.d.ts +18 -0
  141. package/dist/esm/session-memory/native-warm-markers.js +22 -0
  142. package/dist/esm/session-memory/sync/types.d.ts +4 -0
  143. package/dist/esm/session-memory/types.d.ts +3 -1
  144. package/dist/esm/session-memory/types.js +2 -0
  145. package/dist/esm/ultragoal/runtime.js +44 -2
  146. package/dist/esm/worktrees/current.d.ts +1 -0
  147. package/dist/esm/worktrees/current.js +1 -0
  148. package/dist/esm/worktrees/project-agent-surfaces.d.ts +1 -0
  149. package/dist/esm/worktrees/project-agent-surfaces.js +2 -1
  150. package/package.json +17 -14
@@ -0,0 +1,119 @@
1
+ /**
2
+ * Schema-first wire format for multi-host plan-once propose / confirm / cancel.
3
+ * Pure Zod surface — no I/O. Fan-out apply lives in plan-once.ts.
4
+ */
5
+ import { isAbsolute } from "node:path";
6
+ import { z } from "zod";
7
+ import { DASH_PROVIDERS } from "#cli/commands/dash/provider-capabilities.js";
8
+ /** Bump only with a coordinated migration; validators reject unknown versions. */
9
+ export const PLAN_ONCE_SCHEMA_VERSION = 1;
10
+ /** Agent hosts only — never fleet (fleet is a registry-derived provider, not a launch host). */
11
+ export const PLAN_ONCE_HOSTS = DASH_PROVIDERS;
12
+ export const planOnceHostSchema = z.enum(PLAN_ONCE_HOSTS);
13
+ export const planOnceHostEntrySchema = z
14
+ .object({
15
+ host: planOnceHostSchema,
16
+ /** Absolute workdir for this host launch. */
17
+ workdir: z
18
+ .string()
19
+ .min(1)
20
+ .refine(isAbsolute, { message: "Plan host workdir must be absolute" }),
21
+ brief: z.string().min(1).optional(),
22
+ label: z.string().min(1).optional(),
23
+ /** Extra provider args after permission mode (not a free-form command bag). */
24
+ extraArgs: z.array(z.string()).optional(),
25
+ })
26
+ .strict();
27
+ export const multiHostPlanProposalSchema = z
28
+ .object({
29
+ schemaVersion: z.literal(PLAN_ONCE_SCHEMA_VERSION),
30
+ planId: z.string().uuid(),
31
+ hosts: z.array(planOnceHostEntrySchema).min(1),
32
+ proposedAt: z.string().datetime().optional(),
33
+ })
34
+ .strict();
35
+ export const planOnceDecisionSchema = z.discriminatedUnion("kind", [
36
+ z
37
+ .object({
38
+ kind: z.literal("confirm"),
39
+ planId: z.string().uuid(),
40
+ })
41
+ .strict(),
42
+ z
43
+ .object({
44
+ kind: z.literal("cancel"),
45
+ planId: z.string().uuid(),
46
+ })
47
+ .strict(),
48
+ ]);
49
+ function issueFromZod(error) {
50
+ return error.issues.map((item) => {
51
+ const path = item.path.join(".") || undefined;
52
+ const message = item.message;
53
+ if (path === "schemaVersion" || message.toLowerCase().includes("schemaversion")) {
54
+ return { code: "bad-schema-version", message, path };
55
+ }
56
+ if (path === "hosts" && message.toLowerCase().includes("at least")) {
57
+ return { code: "empty-hosts", message, path };
58
+ }
59
+ // Check workdir before host: path "hosts.0.workdir" contains the substring "host".
60
+ if (path?.includes("workdir") || message.toLowerCase().includes("absolute")) {
61
+ return { code: "bad-workdir", message, path };
62
+ }
63
+ const lastSegment = item.path[item.path.length - 1];
64
+ if (lastSegment === "host" || message.toLowerCase().includes("invalid enum")) {
65
+ return { code: "unknown-host", message, path };
66
+ }
67
+ if (path === "planId") {
68
+ return { code: "plan-id", message, path };
69
+ }
70
+ return { code: "schema", message, path };
71
+ });
72
+ }
73
+ /** Parse + structural validate an untrusted plan proposal. */
74
+ export function parseMultiHostPlanProposal(input) {
75
+ if (input !== null &&
76
+ typeof input === "object" &&
77
+ !Array.isArray(input) &&
78
+ "schemaVersion" in input &&
79
+ input.schemaVersion !== PLAN_ONCE_SCHEMA_VERSION) {
80
+ return {
81
+ ok: false,
82
+ issues: [
83
+ {
84
+ code: "bad-schema-version",
85
+ message: `Unsupported plan schemaVersion (expected ${PLAN_ONCE_SCHEMA_VERSION})`,
86
+ path: "schemaVersion",
87
+ },
88
+ ],
89
+ };
90
+ }
91
+ const parsed = multiHostPlanProposalSchema.safeParse(input);
92
+ if (!parsed.success) {
93
+ const issues = issueFromZod(parsed.error);
94
+ // Empty array is a common operator mistake — ensure empty-hosts is explicit.
95
+ if (input !== null &&
96
+ typeof input === "object" &&
97
+ !Array.isArray(input) &&
98
+ Array.isArray(input.hosts) &&
99
+ input.hosts.length === 0 &&
100
+ !issues.some((i) => i.code === "empty-hosts")) {
101
+ return {
102
+ ok: false,
103
+ issues: [
104
+ { code: "empty-hosts", message: "Plan must include at least one host", path: "hosts" },
105
+ ...issues,
106
+ ],
107
+ };
108
+ }
109
+ return { ok: false, issues };
110
+ }
111
+ return { ok: true, plan: parsed.data };
112
+ }
113
+ export function parsePlanOnceDecision(input) {
114
+ const parsed = planOnceDecisionSchema.safeParse(input);
115
+ if (!parsed.success) {
116
+ return { ok: false, issues: issueFromZod(parsed.error) };
117
+ }
118
+ return { ok: true, decision: parsed.data };
119
+ }
@@ -0,0 +1,142 @@
1
+ /**
2
+ * Multi-host plan-once confirm/cancel reducers and fan-out apply.
3
+ *
4
+ * Design:
5
+ * - Schema-first parse lives in plan-once-schema.ts (pure).
6
+ * - Confirm-once: one human decision for the full host set; cancel never launches.
7
+ * - Fan-out reuses DashLaunchCoordinator + existing selectAgent path only.
8
+ * - **Registry ownership:** `DashPaneController.select` (via selectAgent) is the
9
+ * sole createDashRun owner for launches. plan-once must NOT pre-create runs
10
+ * (newRun + runId is rejected; dual create would orphan plan-once rows without
11
+ * tmuxPaneId). After success, plan-once only **labels** the select-created run.
12
+ * - Per-host permission via resolveLaunchPermissionMode (no policy fork).
13
+ * - Concurrency: sequential (required by DashLaunchCoordinator single-pending).
14
+ * - Partial failure: continue remaining hosts; failed hosts reported honestly.
15
+ */
16
+ import type { DashLaunchCoordinator, DashLaunchResult } from "#cli/commands/dash/launch-coordinator.js";
17
+ import { type MultiHostPlanProposal, type PlanOnceDecision, type PlanOnceValidationIssue } from "#cli/commands/dash/plan-once-schema.js";
18
+ import { type DashPermissionMode, type DashProvider } from "#cli/commands/dash/provider-capabilities.js";
19
+ import { type CreateDashRunInput } from "#cli/commands/dash/run-registry.js";
20
+ import type { DashRun, DashRunState } from "#cli/commands/dash/run-types.js";
21
+ export { PLAN_ONCE_SCHEMA_VERSION, parseMultiHostPlanProposal, parsePlanOnceDecision, multiHostPlanProposalSchema, planOnceDecisionSchema, type MultiHostPlanProposal, type PlanOnceDecision, type PlanOnceHostEntry, type PlanOnceValidationIssue, } from "#cli/commands/dash/plan-once-schema.js";
22
+ export { planOnceRunLabel } from "#cli/commands/dash/plan-once-label.js";
23
+ /** Documented fan-out concurrency — sequential because coordinator admits one pending launch. */
24
+ export declare const PLAN_ONCE_FANOUT_CONCURRENCY: "sequential";
25
+ export type PlanOnceFanOutConcurrency = typeof PLAN_ONCE_FANOUT_CONCURRENCY;
26
+ export type PlanOnceSessionStatus = "proposed" | "confirmed" | "cancelled" | "applying" | "applied" | "apply-partial" | "apply-failed";
27
+ export type PlanOnceSession = {
28
+ readonly status: PlanOnceSessionStatus;
29
+ readonly plan: MultiHostPlanProposal;
30
+ };
31
+ export type PlanOnceReduceResult = {
32
+ readonly ok: true;
33
+ readonly session: PlanOnceSession;
34
+ } | {
35
+ readonly ok: false;
36
+ readonly error: string;
37
+ };
38
+ /** Pure confirm-once / cancel reducer — no registry side effects. */
39
+ export declare function createPlanOnceSession(plan: MultiHostPlanProposal): PlanOnceSession;
40
+ export declare function reducePlanOnceDecision(session: PlanOnceSession, decision: PlanOnceDecision): PlanOnceReduceResult;
41
+ /** Parse decision then reduce — convenience for IPC/menu handlers. */
42
+ export declare function applyPlanOnceDecision(session: PlanOnceSession, rawDecision: unknown): PlanOnceReduceResult;
43
+ export type ConfirmPlanOnceWithMembershipResult = {
44
+ readonly ok: true;
45
+ readonly session: PlanOnceSession;
46
+ /** Set when confirm succeeds and membership was written. */
47
+ readonly membershipWritten: boolean;
48
+ } | {
49
+ readonly ok: false;
50
+ readonly error: string;
51
+ };
52
+ /**
53
+ * Confirm-once with durable membership write. Cancel never writes membership.
54
+ * Lazy-imports membership IO so pure reducers stay free of FS side effects.
55
+ */
56
+ export declare function confirmPlanOnceWithMembership(input: {
57
+ readonly session: PlanOnceSession;
58
+ readonly decision: PlanOnceDecision | unknown;
59
+ readonly cwd?: string;
60
+ /** Injected for tests; default writes real membership store. */
61
+ readonly writeMembership?: (plan: MultiHostPlanProposal, cwd?: string) => Promise<unknown>;
62
+ }): Promise<ConfirmPlanOnceWithMembershipResult>;
63
+ export type FanOutHostResult = {
64
+ readonly host: DashProvider;
65
+ readonly workdir: string;
66
+ /** Dash run id from selectAgent (sole registry create path). */
67
+ readonly runId: string | null;
68
+ readonly status: "launched" | "failed" | "discarded";
69
+ readonly permissionMode: DashPermissionMode;
70
+ readonly command: string;
71
+ readonly error: string | null;
72
+ };
73
+ export type FanOutApplyResult = {
74
+ readonly planId: string;
75
+ readonly concurrency: PlanOnceFanOutConcurrency;
76
+ readonly hosts: readonly FanOutHostResult[];
77
+ readonly launchedCount: number;
78
+ readonly failedCount: number;
79
+ readonly discardedCount: number;
80
+ };
81
+ export type PlanOnceLaunchPort = {
82
+ launch(command: string, context?: {
83
+ readonly cwd?: string;
84
+ readonly newRun?: boolean;
85
+ readonly prepared?: boolean;
86
+ readonly provider?: DashProvider;
87
+ readonly argv?: readonly string[];
88
+ /** Never pass with newRun:true — pane-controller rejects that combo. */
89
+ readonly runId?: string;
90
+ }): Promise<DashLaunchResult>;
91
+ };
92
+ /**
93
+ * Registry port used **only** to label/update the run selectAgent already created.
94
+ * createRun is available for tests that simulate selectAgent; apply must not call it.
95
+ */
96
+ export type PlanOnceRegistryPort = {
97
+ createRun(input: CreateDashRunInput): Promise<DashRun>;
98
+ updateRun(id: string, update: {
99
+ readonly state?: DashRunState;
100
+ readonly error?: string | null;
101
+ readonly label?: string | null;
102
+ readonly tmuxPaneId?: string | null;
103
+ readonly providerSessionId?: string | null;
104
+ }, options?: {
105
+ readonly cwd?: string;
106
+ }): Promise<DashRun>;
107
+ };
108
+ export type PlanOnceApplyDeps = {
109
+ readonly launch: PlanOnceLaunchPort;
110
+ readonly registry: PlanOnceRegistryPort;
111
+ readonly resolvePermissionMode: (args?: readonly string[], provider?: DashProvider) => DashPermissionMode;
112
+ /** Registry file scope (dash instance cwd). Host workdirs may differ. */
113
+ readonly registryCwd: string;
114
+ };
115
+ /**
116
+ * Apply a confirmed plan: sequential fan-out through launch coordinator.
117
+ *
118
+ * Does **not** pre-create registry rows. selectAgent (pane-controller) creates
119
+ * exactly one dash run per host on newRun. On success, plan-once labels that run.
120
+ */
121
+ export declare function applyConfirmedMultiHostPlan(input: {
122
+ readonly session: PlanOnceSession;
123
+ readonly launch: PlanOnceLaunchPort | DashLaunchCoordinator;
124
+ readonly registryCwd: string;
125
+ readonly registry?: PlanOnceRegistryPort;
126
+ readonly resolvePermissionMode?: PlanOnceApplyDeps["resolvePermissionMode"];
127
+ }): Promise<{
128
+ readonly ok: true;
129
+ readonly result: FanOutApplyResult;
130
+ readonly session: PlanOnceSession;
131
+ } | {
132
+ readonly ok: false;
133
+ readonly error: string;
134
+ }>;
135
+ /** Propose → parse helper for callers that only have untrusted JSON. */
136
+ export declare function proposeMultiHostPlan(raw: unknown): {
137
+ readonly ok: true;
138
+ readonly session: PlanOnceSession;
139
+ } | {
140
+ readonly ok: false;
141
+ readonly issues: readonly PlanOnceValidationIssue[];
142
+ };
@@ -0,0 +1,261 @@
1
+ import { resolveLaunchPermissionMode } from "#cli/commands/dash/launch-preferences.js";
2
+ import { planOnceRunLabel } from "#cli/commands/dash/plan-once-label.js";
3
+ import { parseMultiHostPlanProposal, parsePlanOnceDecision, } from "#cli/commands/dash/plan-once-schema.js";
4
+ import { buildDashLaunchCommand, } from "#cli/commands/dash/provider-capabilities.js";
5
+ import { updateDashRun } from "#cli/commands/dash/run-registry.js";
6
+ export { PLAN_ONCE_SCHEMA_VERSION, parseMultiHostPlanProposal, parsePlanOnceDecision, multiHostPlanProposalSchema, planOnceDecisionSchema, } from "#cli/commands/dash/plan-once-schema.js";
7
+ export { planOnceRunLabel } from "#cli/commands/dash/plan-once-label.js";
8
+ /** Documented fan-out concurrency — sequential because coordinator admits one pending launch. */
9
+ export const PLAN_ONCE_FANOUT_CONCURRENCY = "sequential";
10
+ /** Pure confirm-once / cancel reducer — no registry side effects. */
11
+ export function createPlanOnceSession(plan) {
12
+ return { status: "proposed", plan };
13
+ }
14
+ export function reducePlanOnceDecision(session, decision) {
15
+ if (decision.planId !== session.plan.planId) {
16
+ return { ok: false, error: `Decision planId ${decision.planId} does not match session plan` };
17
+ }
18
+ if (session.status !== "proposed") {
19
+ return {
20
+ ok: false,
21
+ error: `Cannot ${decision.kind} plan in status ${session.status} (confirm-once from proposed only)`,
22
+ };
23
+ }
24
+ if (decision.kind === "confirm") {
25
+ return { ok: true, session: { ...session, status: "confirmed" } };
26
+ }
27
+ return { ok: true, session: { ...session, status: "cancelled" } };
28
+ }
29
+ /** Parse decision then reduce — convenience for IPC/menu handlers. */
30
+ export function applyPlanOnceDecision(session, rawDecision) {
31
+ const parsed = parsePlanOnceDecision(rawDecision);
32
+ if (!parsed.ok) {
33
+ return {
34
+ ok: false,
35
+ error: parsed.issues.map((i) => i.message).join("; ") || "Invalid plan decision",
36
+ };
37
+ }
38
+ return reducePlanOnceDecision(session, parsed.decision);
39
+ }
40
+ /**
41
+ * Confirm-once with durable membership write. Cancel never writes membership.
42
+ * Lazy-imports membership IO so pure reducers stay free of FS side effects.
43
+ */
44
+ export async function confirmPlanOnceWithMembership(input) {
45
+ // Always schema-parse the public entrypoint (no cast-on-kind shortcut).
46
+ const parsed = parsePlanOnceDecision(input.decision);
47
+ if (!parsed.ok) {
48
+ return {
49
+ ok: false,
50
+ error: parsed.issues.map((i) => i.message).join("; ") || "Invalid plan decision",
51
+ };
52
+ }
53
+ const reduced = reducePlanOnceDecision(input.session, parsed.decision);
54
+ if (!reduced.ok)
55
+ return { ok: false, error: reduced.error };
56
+ if (reduced.session.status === "cancelled") {
57
+ return { ok: true, session: reduced.session, membershipWritten: false };
58
+ }
59
+ if (reduced.session.status !== "confirmed") {
60
+ return { ok: false, error: `Unexpected status after decision: ${reduced.session.status}` };
61
+ }
62
+ if (input.writeMembership) {
63
+ await input.writeMembership(reduced.session.plan, input.cwd);
64
+ }
65
+ else {
66
+ const { buildPlanMembership, writePlanMembership } = await import("#cli/commands/dash/plan-once-membership.js");
67
+ await writePlanMembership(buildPlanMembership(reduced.session.plan), { cwd: input.cwd });
68
+ }
69
+ return { ok: true, session: reduced.session, membershipWritten: true };
70
+ }
71
+ function defaultRegistryPort() {
72
+ // createRun is intentionally not used by apply — only updateRun for plan labels.
73
+ // Import createDashRun only when tests inject a full port; production update path:
74
+ return {
75
+ createRun: async () => {
76
+ throw new Error("plan-once apply must not createDashRun; selectAgent/pane-controller owns create");
77
+ },
78
+ updateRun: updateDashRun,
79
+ };
80
+ }
81
+ function defaultDeps(input) {
82
+ return {
83
+ launch: input.launch,
84
+ registry: input.registry ?? defaultRegistryPort(),
85
+ resolvePermissionMode: input.resolvePermissionMode ?? resolveLaunchPermissionMode,
86
+ registryCwd: input.registryCwd,
87
+ };
88
+ }
89
+ function outcomeRunId(result) {
90
+ const outcome = result.outcome;
91
+ if ("runId" in outcome && typeof outcome.runId === "string" && outcome.runId.length > 0) {
92
+ return outcome.runId;
93
+ }
94
+ return null;
95
+ }
96
+ /**
97
+ * Apply a confirmed plan: sequential fan-out through launch coordinator.
98
+ *
99
+ * Does **not** pre-create registry rows. selectAgent (pane-controller) creates
100
+ * exactly one dash run per host on newRun. On success, plan-once labels that run.
101
+ */
102
+ export async function applyConfirmedMultiHostPlan(input) {
103
+ if (input.session.status !== "confirmed") {
104
+ return {
105
+ ok: false,
106
+ error: `Cannot apply plan in status ${input.session.status} (require confirmed)`,
107
+ };
108
+ }
109
+ const deps = defaultDeps(input);
110
+ const plan = input.session.plan;
111
+ const hostResults = [];
112
+ // Ensure durable membership exists before fan-out (idempotent). Cancel never reaches here.
113
+ try {
114
+ const { buildPlanMembership, writePlanMembership } = await import("#cli/commands/dash/plan-once-membership.js");
115
+ await writePlanMembership(buildPlanMembership(plan), { cwd: deps.registryCwd });
116
+ }
117
+ catch {
118
+ // Membership write failure does not block launch; merge may report incomplete.
119
+ }
120
+ for (const entry of plan.hosts) {
121
+ const permissionMode = deps.resolvePermissionMode(entry.extraArgs ?? [], entry.host);
122
+ const command = buildDashLaunchCommand(entry.host, permissionMode, entry.extraArgs ?? []);
123
+ const label = planOnceRunLabel(plan.planId, entry);
124
+ let launchResult;
125
+ try {
126
+ // newRun:true without runId — selectAgent owns createDashRun.
127
+ // prepared:true skips prepare thrash so sequential fan-out does not
128
+ // contend on coordinator pending Fresh.
129
+ launchResult = await deps.launch.launch(command, {
130
+ cwd: entry.workdir,
131
+ newRun: true,
132
+ prepared: true,
133
+ provider: entry.host,
134
+ });
135
+ }
136
+ catch (error) {
137
+ const message = error instanceof Error ? error.message : "Launch threw";
138
+ hostResults.push({
139
+ host: entry.host,
140
+ workdir: entry.workdir,
141
+ runId: null,
142
+ status: "failed",
143
+ permissionMode,
144
+ command,
145
+ error: message,
146
+ });
147
+ continue;
148
+ }
149
+ if (launchResult.kind === "launched") {
150
+ const runId = outcomeRunId(launchResult);
151
+ if (!runId) {
152
+ hostResults.push({
153
+ host: entry.host,
154
+ workdir: entry.workdir,
155
+ runId: null,
156
+ status: "failed",
157
+ permissionMode,
158
+ command,
159
+ error: "selectAgent launched without dash runId (cannot label or attach)",
160
+ });
161
+ continue;
162
+ }
163
+ // Label the select-created run only — never invent a second registry row.
164
+ // Do not write runId into providerSessionId (that field is provider session).
165
+ // tmuxPaneId is already persisted by pane-controller before select returns.
166
+ try {
167
+ await deps.registry.updateRun(runId, { label, error: null }, { cwd: deps.registryCwd });
168
+ }
169
+ catch (error) {
170
+ const message = error instanceof Error ? error.message : "Failed to label dash run";
171
+ hostResults.push({
172
+ host: entry.host,
173
+ workdir: entry.workdir,
174
+ runId,
175
+ status: "failed",
176
+ permissionMode,
177
+ command,
178
+ error: message,
179
+ });
180
+ continue;
181
+ }
182
+ hostResults.push({
183
+ host: entry.host,
184
+ workdir: entry.workdir,
185
+ runId,
186
+ status: "launched",
187
+ permissionMode,
188
+ command,
189
+ error: null,
190
+ });
191
+ continue;
192
+ }
193
+ if (launchResult.kind === "discarded") {
194
+ hostResults.push({
195
+ host: entry.host,
196
+ workdir: entry.workdir,
197
+ runId: null,
198
+ status: "discarded",
199
+ permissionMode,
200
+ command,
201
+ error: "launch discarded",
202
+ });
203
+ continue;
204
+ }
205
+ if (launchResult.kind === "pending") {
206
+ hostResults.push({
207
+ host: entry.host,
208
+ workdir: entry.workdir,
209
+ runId: null,
210
+ status: "failed",
211
+ permissionMode,
212
+ command,
213
+ error: launchResult.message || "Launch still pending (unexpected in sequential fan-out)",
214
+ });
215
+ continue;
216
+ }
217
+ // failed — selectAgent/pane-controller already marked its own run failed if created
218
+ const message = `${launchResult.message}${launchResult.action ? ` · ${launchResult.action}` : ""}`;
219
+ hostResults.push({
220
+ host: entry.host,
221
+ workdir: entry.workdir,
222
+ runId: null,
223
+ status: "failed",
224
+ permissionMode,
225
+ command,
226
+ error: message,
227
+ });
228
+ }
229
+ const launchedCount = hostResults.filter((h) => h.status === "launched").length;
230
+ const failedCount = hostResults.filter((h) => h.status === "failed").length;
231
+ const discardedCount = hostResults.filter((h) => h.status === "discarded").length;
232
+ let nextStatus;
233
+ if (launchedCount === hostResults.length && hostResults.length > 0) {
234
+ nextStatus = "applied";
235
+ }
236
+ else if (launchedCount === 0) {
237
+ nextStatus = "apply-failed";
238
+ }
239
+ else {
240
+ nextStatus = "apply-partial";
241
+ }
242
+ return {
243
+ ok: true,
244
+ result: {
245
+ planId: plan.planId,
246
+ concurrency: PLAN_ONCE_FANOUT_CONCURRENCY,
247
+ hosts: hostResults,
248
+ launchedCount,
249
+ failedCount,
250
+ discardedCount,
251
+ },
252
+ session: { ...input.session, status: nextStatus },
253
+ };
254
+ }
255
+ /** Propose → parse helper for callers that only have untrusted JSON. */
256
+ export function proposeMultiHostPlan(raw) {
257
+ const parsed = parseMultiHostPlanProposal(raw);
258
+ if (!parsed.ok)
259
+ return parsed;
260
+ return { ok: true, session: createPlanOnceSession(parsed.plan) };
261
+ }
@@ -2,6 +2,15 @@ import { type AttentionItem } from "#cli/commands/dash/attention.js";
2
2
  import type { AgentRunStatus } from "#cli/commands/dash/pane-controller.js";
3
3
  export declare function attentionItemsFromRunStatuses(statuses: readonly AgentRunStatus[], now?: string): readonly AttentionItem[];
4
4
  export declare function formatAttentionLine(items: readonly AttentionItem[]): string | null;
5
+ /**
6
+ * Prefer multi-host merge rollup when the merge line is non-null.
7
+ * Callers should pass a merge line only for non-all-ok rollups
8
+ * (`formatPrimaryMergeAttentionLine`) so all-ok does not hide live failures.
9
+ */
10
+ export declare function selectAttentionLine(input: {
11
+ readonly mergeLine: string | null;
12
+ readonly genericLine: string | null;
13
+ }): string | null;
5
14
  /** Dash-process RSS + active run count (owned tree probe is best-effort elsewhere). */
6
15
  export declare function formatResourceLine(input: {
7
16
  readonly statuses: readonly AgentRunStatus[];
@@ -1,5 +1,6 @@
1
1
  // Project session attention + process resource HUD lines for the pure TUI.
2
2
  // Uses the same normalizeSessionState / projectAttention domain as desktop.
3
+ // Multi-host merge rollup (plan-once membership) can override the generic line.
3
4
  import { normalizeSessionState, projectAttention, } from "#cli/commands/dash/attention.js";
4
5
  import { resourceView } from "#cli/commands/dash/resources.js";
5
6
  export function attentionItemsFromRunStatuses(statuses, now = new Date().toISOString()) {
@@ -35,6 +36,14 @@ export function formatAttentionLine(items) {
35
36
  return null;
36
37
  return `attention · ${top.provider}:${top.state} · ${top.reason} · ${top.action}`;
37
38
  }
39
+ /**
40
+ * Prefer multi-host merge rollup when the merge line is non-null.
41
+ * Callers should pass a merge line only for non-all-ok rollups
42
+ * (`formatPrimaryMergeAttentionLine`) so all-ok does not hide live failures.
43
+ */
44
+ export function selectAttentionLine(input) {
45
+ return input.mergeLine ?? input.genericLine;
46
+ }
38
47
  /** Dash-process RSS + active run count (owned tree probe is best-effort elsewhere). */
39
48
  export function formatResourceLine(input) {
40
49
  const active = input.statuses.filter((status) => status.running || status.active);
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Integrated-chat bridge port: hermetic fake for CI + real adapter seam.
3
+ * Does not create dash runs — uses existing run identity only (no second registry).
4
+ */
5
+ import type { DashProvider } from "#cli/commands/dash/provider-capabilities.js";
6
+ export type ChatRole = "user" | "assistant" | "system";
7
+ export type ChatMessage = {
8
+ readonly role: ChatRole;
9
+ readonly text: string;
10
+ readonly at: string;
11
+ };
12
+ export type ChatSession = {
13
+ readonly runId: string;
14
+ readonly host: DashProvider;
15
+ readonly label: string | null;
16
+ readonly messages: readonly ChatMessage[];
17
+ };
18
+ export type ChatBridgeResult = {
19
+ readonly ok: true;
20
+ readonly session: ChatSession;
21
+ } | {
22
+ readonly ok: false;
23
+ readonly error: string;
24
+ };
25
+ export type ChatBridge = {
26
+ open(input: {
27
+ readonly host: DashProvider;
28
+ readonly runId: string;
29
+ readonly label?: string | null;
30
+ }): Promise<ChatBridgeResult>;
31
+ send(session: ChatSession, text: string): Promise<ChatBridgeResult>;
32
+ };
33
+ /**
34
+ * Hermetic bridge: drives real send/receive entry points without live ACP.
35
+ * Codex is the MVP host (capability matrix); others fail with matrix reasons.
36
+ */
37
+ export declare function createHermeticChatBridge(options?: {
38
+ readonly now?: () => string;
39
+ }): ChatBridge;
40
+ /** Format transcript lines for browser overlay. */
41
+ export declare function formatChatTranscript(session: ChatSession): readonly string[];
@@ -0,0 +1,73 @@
1
+ import { hostAcpCapability } from "#cli/commands/dash/tui/chat-mode.js";
2
+ function nowIso() {
3
+ return new Date().toISOString();
4
+ }
5
+ /**
6
+ * Hermetic bridge: drives real send/receive entry points without live ACP.
7
+ * Codex is the MVP host (capability matrix); others fail with matrix reasons.
8
+ */
9
+ export function createHermeticChatBridge(options = {}) {
10
+ const stamp = options.now ?? nowIso;
11
+ return {
12
+ async open(input) {
13
+ if (!input.runId.trim()) {
14
+ return { ok: false, error: "runId is required (use existing dash run identity)" };
15
+ }
16
+ const cap = hostAcpCapability(input.host);
17
+ if (!cap.acpAvailable) {
18
+ return { ok: false, error: cap.reason };
19
+ }
20
+ const session = {
21
+ runId: input.runId,
22
+ host: input.host,
23
+ label: input.label ?? null,
24
+ messages: [
25
+ {
26
+ role: "system",
27
+ text: `Integrated chat open for ${input.host} (hermetic). Host-pane remains available via mode switch.`,
28
+ at: stamp(),
29
+ },
30
+ ],
31
+ };
32
+ return { ok: true, session };
33
+ },
34
+ async send(session, text) {
35
+ const trimmed = text.trim();
36
+ if (!trimmed) {
37
+ return { ok: false, error: "message text is required" };
38
+ }
39
+ const cap = hostAcpCapability(session.host);
40
+ if (!cap.acpAvailable) {
41
+ return { ok: false, error: cap.reason };
42
+ }
43
+ const userMsg = { role: "user", text: trimmed, at: stamp() };
44
+ // Hermetic assistant echo proves receive path without live LLM.
45
+ const assistantMsg = {
46
+ role: "assistant",
47
+ text: `[hermetic:${session.host}] ${trimmed}`,
48
+ at: stamp(),
49
+ };
50
+ return {
51
+ ok: true,
52
+ session: {
53
+ ...session,
54
+ messages: [...session.messages, userMsg, assistantMsg],
55
+ },
56
+ };
57
+ },
58
+ };
59
+ }
60
+ /** Format transcript lines for browser overlay. */
61
+ export function formatChatTranscript(session) {
62
+ const header = [
63
+ `chat · ${session.host} · run ${session.runId.slice(0, 8)}`,
64
+ session.label ? `label · ${session.label}` : null,
65
+ "scope · integrated-chat (host-pane available via mode switch)",
66
+ "─".repeat(40),
67
+ ].filter((line) => line !== null);
68
+ const body = session.messages.map((msg) => {
69
+ const tag = msg.role === "user" ? "you" : msg.role === "assistant" ? "host" : "sys";
70
+ return `${tag} · ${msg.text}`;
71
+ });
72
+ return [...header, ...body];
73
+ }