@phnx-labs/agents-cli 1.22.59 → 1.22.61

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 (100) hide show
  1. package/CHANGELOG.md +71 -0
  2. package/dist/cli/command-registry.d.ts +1 -0
  3. package/dist/cli/command-registry.js +2 -0
  4. package/dist/commands/browser.js +9 -4
  5. package/dist/commands/doctor.js +1 -1
  6. package/dist/commands/exec.js +35 -1
  7. package/dist/commands/harness-hooks.d.ts +55 -0
  8. package/dist/commands/harness-hooks.js +104 -0
  9. package/dist/commands/harness-wizard.d.ts +33 -14
  10. package/dist/commands/harness-wizard.js +53 -23
  11. package/dist/commands/harness.d.ts +14 -0
  12. package/dist/commands/harness.js +86 -5
  13. package/dist/commands/perf.js +10 -0
  14. package/dist/commands/reminders.d.ts +9 -0
  15. package/dist/commands/reminders.js +49 -0
  16. package/dist/commands/run-account-picker.d.ts +14 -0
  17. package/dist/commands/run-account-picker.js +13 -0
  18. package/dist/commands/sessions-picker.d.ts +13 -0
  19. package/dist/commands/sessions-picker.js +17 -8
  20. package/dist/commands/sessions.js +13 -11
  21. package/dist/commands/teams-picker.js +20 -6
  22. package/dist/commands/teams.d.ts +3 -3
  23. package/dist/commands/teams.js +86 -24
  24. package/dist/index.js +9 -0
  25. package/dist/lib/accounting/rotate.d.ts +63 -0
  26. package/dist/lib/accounting/rotate.js +240 -16
  27. package/dist/lib/accounting/usage-sync.d.ts +12 -2
  28. package/dist/lib/accounting/usage-sync.js +34 -6
  29. package/dist/lib/browser/drivers/local.d.ts +11 -0
  30. package/dist/lib/browser/drivers/local.js +26 -0
  31. package/dist/lib/browser/profiles.js +8 -6
  32. package/dist/lib/browser/service.d.ts +12 -8
  33. package/dist/lib/browser/service.js +38 -10
  34. package/dist/lib/claude-statusline.d.ts +14 -1
  35. package/dist/lib/claude-statusline.js +27 -2
  36. package/dist/lib/daemon/runner.js +17 -2
  37. package/dist/lib/devices/doctor-findings.d.ts +1 -1
  38. package/dist/lib/devices/doctor-findings.js +22 -4
  39. package/dist/lib/doctor-diff.d.ts +21 -5
  40. package/dist/lib/doctor-diff.js +242 -76
  41. package/dist/lib/feed/events.d.ts +1 -1
  42. package/dist/lib/feed/events.js +28 -15
  43. package/dist/lib/github/gh-overload.d.ts +58 -0
  44. package/dist/lib/github/gh-overload.js +246 -0
  45. package/dist/lib/github/rest.d.ts +64 -0
  46. package/dist/lib/github/rest.js +111 -0
  47. package/dist/lib/harness-connection-test.d.ts +57 -0
  48. package/dist/lib/harness-connection-test.js +80 -0
  49. package/dist/lib/heal.js +8 -3
  50. package/dist/lib/installations/shims.d.ts +22 -0
  51. package/dist/lib/installations/shims.js +104 -0
  52. package/dist/lib/linear-project-counts.js +8 -0
  53. package/dist/lib/linear-rate-limit.d.ts +26 -0
  54. package/dist/lib/linear-rate-limit.js +163 -0
  55. package/dist/lib/mcp.d.ts +9 -0
  56. package/dist/lib/mcp.js +37 -1
  57. package/dist/lib/open-url.js +5 -3
  58. package/dist/lib/perf/db.d.ts +1 -1
  59. package/dist/lib/perf/db.js +53 -2
  60. package/dist/lib/perf/types.d.ts +14 -0
  61. package/dist/lib/permissions.d.ts +28 -0
  62. package/dist/lib/permissions.js +156 -1
  63. package/dist/lib/refresh.js +9 -1
  64. package/dist/lib/reminders.d.ts +29 -0
  65. package/dist/lib/reminders.js +88 -0
  66. package/dist/lib/resource-content-diff.d.ts +33 -0
  67. package/dist/lib/resource-content-diff.js +103 -0
  68. package/dist/lib/rules/compile.d.ts +7 -0
  69. package/dist/lib/rules/compile.js +7 -1
  70. package/dist/lib/session/active.d.ts +41 -4
  71. package/dist/lib/session/active.js +58 -7
  72. package/dist/lib/session/host-link.d.ts +22 -0
  73. package/dist/lib/session/host-link.js +40 -4
  74. package/dist/lib/session/live-metadata.js +3 -3
  75. package/dist/lib/session/trajectory.d.ts +42 -0
  76. package/dist/lib/session/trajectory.js +46 -27
  77. package/dist/lib/ssh-exec.d.ts +30 -0
  78. package/dist/lib/ssh-exec.js +37 -5
  79. package/dist/lib/startup/command-registry.js +1 -1
  80. package/dist/lib/subagents-registry.d.ts +18 -0
  81. package/dist/lib/subagents-registry.js +79 -0
  82. package/dist/lib/teams/agents.d.ts +12 -0
  83. package/dist/lib/teams/agents.js +51 -0
  84. package/dist/lib/teams/api.d.ts +8 -0
  85. package/dist/lib/teams/api.js +50 -6
  86. package/dist/lib/teams/delivery.d.ts +14 -4
  87. package/dist/lib/teams/delivery.js +15 -5
  88. package/dist/lib/traces/schema2-build.d.ts +85 -0
  89. package/dist/lib/traces/schema2-build.js +637 -0
  90. package/dist/lib/traces/schema2-danger.d.ts +36 -0
  91. package/dist/lib/traces/schema2-danger.js +185 -0
  92. package/dist/lib/traces/schema2.d.ts +149 -0
  93. package/dist/lib/traces/schema2.js +20 -0
  94. package/dist/lib/traces/sync.d.ts +93 -0
  95. package/dist/lib/traces/sync.js +75 -22
  96. package/dist/lib/traces/worker-template.js +5 -0
  97. package/dist/lib/uninstall.js +10 -1
  98. package/dist/lib/workflows.d.ts +11 -0
  99. package/dist/lib/workflows.js +67 -8
  100. package/package.json +1 -1
@@ -101,6 +101,18 @@ export { captureProcessStartTime };
101
101
  * model_reasoning_effort override). Mode (plan/edit/full) is a separate knob.
102
102
  */
103
103
  export type EffortLevel = 'low' | 'medium' | 'high' | 'xhigh' | 'max' | 'auto';
104
+ /**
105
+ * Append {@link TEAMMATE_PR_POLICY} to a teammate prompt for every WRITE-capable
106
+ * mode (all but plan, which is read-only and opens no PR). Exported so the CLOUD
107
+ * dispatch path (`cloudDispatchOptions` in `commands/teams.ts`) applies the SAME
108
+ * boundary this file's `buildRunArgv` applies to LOCAL and REMOTE teammates. A
109
+ * cloud teammate is the case that needs it MOST: it runs in the provider's
110
+ * sandbox, not the shared local version home, so it never inherits the
111
+ * `merge-guard.sh` PreToolUse hook — the prompt policy is then its ONLY
112
+ * self-merge layer. Routing every dispatch surface through one helper keeps that
113
+ * parity from drifting (PHNX-3236).
114
+ */
115
+ export declare function withTeammatePrPolicy(prompt: string, mode: string): string;
104
116
  export declare const VALID_MODES: readonly ["plan", "edit", "auto", "skip", "full"];
105
117
  type Mode = 'plan' | 'edit' | 'auto' | 'skip';
106
118
  /** Resolve a mode string to a validated Mode, falling back to the given default. */
@@ -255,6 +255,49 @@ When you're done, provide a brief summary of:
255
255
  const CLAUDE_PLAN_MODE_PREFIX = `You are running in HEADLESS PLAN MODE. This mode works like normal plan mode with one exception: you cannot write to ~/.claude/plans/ directory. Instead of writing a plan file, output your complete plan/response as your final message.
256
256
 
257
257
  `;
258
+ // PHNX-3236: the teammate self-merge boundary, injected as a DISPATCH DEFAULT.
259
+ // A write-capable teammate has `gh pr merge` and authenticates as the repo owner,
260
+ // so it can merge its OWN PR past the required non-author-review gate — which is
261
+ // exactly what happened in the RUSH-2988 wave-1 dispatch (PR #1817, #1820). The
262
+ // root cause was that the boundary lived in per-brief wording: one teammate in the
263
+ // batch was told "open the PR, don't merge" and held off; the others weren't and
264
+ // self-merged. Making it a default the runner appends to every non-plan teammate
265
+ // gives one HARNESS-INDEPENDENT layer instead of relying on each dispatch prompt
266
+ // remembering to say it. The HARD enforcement is merge-guard.sh — a PreToolUse hook
267
+ // the teammate inherits from the shared version home, whose self-authored-verdict
268
+ // exclusion was closed in the same ticket (.agents-system #395) so a verdict a
269
+ // teammate posts on its own PR no longer clears the gate. The two layers do NOT
270
+ // overlap everywhere: hook-capable local/remote teammates get both, but cloud
271
+ // teammates (provider sandbox, no inherited hook) and hook-incapable harnesses
272
+ // (Warp/oz — no hook surface, no allowlist) get ONLY this prompt, so for them it is
273
+ // a soft control. That residual is documented in cli/AGENTS.md §6; server-side
274
+ // branch protection is the client-independent way to close it. This is the
275
+ // harness-independent layer plus the operator hand-off contract, not a replacement
276
+ // for the hard block where the hard block can run.
277
+ const TEAMMATE_PR_POLICY = `
278
+
279
+ Teammate PR policy (agents teams): when your work opens a pull request, open it and
280
+ hand it off — do NOT merge your OWN PR unless a NON-AUTHOR review verdict has been
281
+ posted on that same PR. You authenticate as the repo owner and share that one
282
+ GitHub identity with every other teammate, so an APPROVE you post on your own PR
283
+ does not count as a non-author review. \`gh pr merge\` on your own PR is blocked by
284
+ merge-guard until a genuine non-author verdict exists on it; never pass --admin or
285
+ otherwise route around that guard. Report the PR as open and let the orchestrator
286
+ or a separate reviewer take it to merge.`;
287
+ /**
288
+ * Append {@link TEAMMATE_PR_POLICY} to a teammate prompt for every WRITE-capable
289
+ * mode (all but plan, which is read-only and opens no PR). Exported so the CLOUD
290
+ * dispatch path (`cloudDispatchOptions` in `commands/teams.ts`) applies the SAME
291
+ * boundary this file's `buildRunArgv` applies to LOCAL and REMOTE teammates. A
292
+ * cloud teammate is the case that needs it MOST: it runs in the provider's
293
+ * sandbox, not the shared local version home, so it never inherits the
294
+ * `merge-guard.sh` PreToolUse hook — the prompt policy is then its ONLY
295
+ * self-merge layer. Routing every dispatch surface through one helper keeps that
296
+ * parity from drifting (PHNX-3236).
297
+ */
298
+ export function withTeammatePrPolicy(prompt, mode) {
299
+ return mode === 'plan' ? prompt : prompt + TEAMMATE_PR_POLICY;
300
+ }
258
301
  // Canonical modes plus the historical `full` alias (rewritten to `skip` by
259
302
  // normalizeModeValue). Keep `full` listed so user-typed CLI flags and stored
260
303
  // metadata that pre-date the rename continue to parse.
@@ -2530,6 +2573,14 @@ export class AgentManager {
2530
2573
  fullPrompt = CLAUDE_PLAN_MODE_PREFIX + fullPrompt;
2531
2574
  }
2532
2575
  }
2576
+ // PHNX-3236: append the self-merge boundary to every WRITE-capable teammate,
2577
+ // fresh or resumed. A plan-mode teammate produces no PR (read-only), so it is
2578
+ // skipped to keep its prompt clean; every other mode can open — and could
2579
+ // self-merge — a PR, so the policy rides along regardless of harness. The
2580
+ // hard block is still merge-guard.sh (inherited hook); see TEAMMATE_PR_POLICY.
2581
+ // The cloud dispatch path applies the SAME helper (withTeammatePrPolicy) so
2582
+ // local, remote, and cloud teammates never diverge on this boundary.
2583
+ fullPrompt = withTeammatePrPolicy(fullPrompt, mode);
2533
2584
  // Profile target takes precedence — `agents run <profile>` resolves the
2534
2585
  // host harness, version pin, and env injection in one place. Plain
2535
2586
  // version pins only apply when no profile is selected.
@@ -62,6 +62,8 @@ export interface AgentStatusDetail {
62
62
  task_type?: TaskType | null;
63
63
  /** Device name the teammate runs on for a distributed (--on) teammate; null for local. */
64
64
  host?: string | null;
65
+ /** Absolute path to the teammate's worktree, when known. */
66
+ workspace_dir?: string | null;
65
67
  /** Sanitized evidence observed at the lifecycle boundary that failed. */
66
68
  failure?: TeammateFailure | null;
67
69
  }
@@ -73,6 +75,7 @@ export interface TaskStatusResult {
73
75
  pending: number;
74
76
  running: number;
75
77
  completed: number;
78
+ stranded: number;
76
79
  failed: number;
77
80
  stopped: number;
78
81
  };
@@ -126,6 +129,8 @@ export interface AgentStatusSummary {
126
129
  /** Device name for a distributed (--on) teammate; null for local. */
127
130
  host: string | null;
128
131
  failure: TeammateFailure | null;
132
+ /** Absolute path to the teammate's worktree, when known. */
133
+ workspace_dir?: string | null;
129
134
  /** ISO timestamp — feed back via --since for delta polling. */
130
135
  cursor: string;
131
136
  }
@@ -137,6 +142,7 @@ export interface TaskStatusSummaryResult {
137
142
  pending: number;
138
143
  running: number;
139
144
  completed: number;
145
+ stranded: number;
140
146
  failed: number;
141
147
  stopped: number;
142
148
  };
@@ -164,6 +170,8 @@ export interface TaskInfo {
164
170
  pending: number;
165
171
  running: number;
166
172
  completed: number;
173
+ /** Completed teammates with uncommitted work and no PR (PHNX-2951). */
174
+ stranded: number;
167
175
  failed: number;
168
176
  stopped: number;
169
177
  workspace_dir: string | null;
@@ -3,6 +3,7 @@ import { getDelta } from './summarizer.js';
3
3
  import { debug } from './debug.js';
4
4
  import { buildClaudeLabelMap } from '../session/discover.js';
5
5
  import { resolveTeammateDelivery } from './delivery.js';
6
+ import { hasUncommittedChanges } from './worktree.js';
6
7
  /**
7
8
  * Truncate a bash command for status output.
8
9
  * Handles heredocs specially - shows the redirect target instead of contents.
@@ -126,6 +127,7 @@ export function toAgentStatusSummary(detail) {
126
127
  .map((m) => trimMessage(m)),
127
128
  host: detail.host ?? null,
128
129
  failure: detail.failure ?? null,
130
+ workspace_dir: detail.workspace_dir ?? null,
129
131
  cursor: detail.cursor,
130
132
  };
131
133
  }
@@ -207,7 +209,7 @@ parentSessionId) {
207
209
  ? allAgents
208
210
  : allAgents.filter((a) => a.status === effectiveFilter);
209
211
  const agentStatuses = [];
210
- const counts = { pending: 0, running: 0, completed: 0, failed: 0, stopped: 0 };
212
+ const counts = { pending: 0, running: 0, completed: 0, stranded: 0, failed: 0, stopped: 0 };
211
213
  // Count ALL agents for summary (not just filtered)
212
214
  for (const agent of allAgents) {
213
215
  if (agent.status === AgentStatus.PENDING)
@@ -221,6 +223,30 @@ parentSessionId) {
221
223
  else if (agent.status === AgentStatus.STOPPED)
222
224
  counts.stopped++;
223
225
  }
226
+ // Stranded count is computed over ALL agents, not the filter-narrowed view,
227
+ // so a `--filter running` status still reports teammates that completed dirty.
228
+ // Cache the probe result so we can reuse it when building details for the
229
+ // filtered subset without probing the worktree twice.
230
+ const uncommittedCache = new Map();
231
+ for (const agent of allAgents) {
232
+ if (agent.status !== AgentStatus.COMPLETED)
233
+ continue;
234
+ const shouldProbeWorktree = !agent.prUrl?.trim() &&
235
+ !agent.hostName &&
236
+ Boolean(agent.workspaceDir);
237
+ const hasUncommitted = shouldProbeWorktree
238
+ ? await hasUncommittedChanges(agent.workspaceDir)
239
+ : false;
240
+ uncommittedCache.set(agent.agentId, hasUncommitted);
241
+ const delivery = resolveTeammateDelivery({
242
+ status: agent.status,
243
+ prUrl: agent.prUrl,
244
+ hasUncommittedChanges: hasUncommitted,
245
+ });
246
+ if (delivery === 'stranded') {
247
+ counts.stranded++;
248
+ }
249
+ }
224
250
  // Build details only for filtered agents
225
251
  let maxTimestamp = since || new Date(0).toISOString(); // Track max timestamp for cursor
226
252
  const claudeLabels = allAgents.some((agent) => agent.agentType === 'claude')
@@ -237,6 +263,12 @@ parentSessionId) {
237
263
  if (agentTimestamp > maxTimestamp) {
238
264
  maxTimestamp = agentTimestamp;
239
265
  }
266
+ const hasUncommitted = uncommittedCache.get(agent.agentId) ?? false;
267
+ const delivery = resolveTeammateDelivery({
268
+ status: agent.status,
269
+ prUrl: agent.prUrl,
270
+ hasUncommittedChanges: hasUncommitted,
271
+ });
240
272
  const detail = {
241
273
  agent_id: agent.agentId,
242
274
  agent_type: agent.agentType,
@@ -254,15 +286,13 @@ parentSessionId) {
254
286
  after: agent.after,
255
287
  task_type: agent.taskType,
256
288
  host: agent.hostName,
289
+ workspace_dir: agent.workspaceDir,
257
290
  failure: agent.failure,
258
291
  mode: agent.mode,
259
292
  cloud_session_id: agent.cloudSessionId,
260
293
  cloud_provider: agent.cloudProvider,
261
294
  pr_url: agent.prUrl,
262
- delivery: resolveTeammateDelivery({
263
- status: agent.status,
264
- prUrl: agent.prUrl,
265
- }),
295
+ delivery,
266
296
  files_created: delta.new_files_created,
267
297
  files_modified: delta.new_files_modified,
268
298
  files_read: delta.new_files_read,
@@ -297,7 +327,7 @@ export async function handleTasks(manager, limit = 10) {
297
327
  }
298
328
  const tasks = [];
299
329
  for (const [taskName, agents] of taskMap) {
300
- let pending = 0, running = 0, completed = 0, failed = 0, stopped = 0;
330
+ let pending = 0, running = 0, completed = 0, stranded = 0, failed = 0, stopped = 0;
301
331
  let earliestStart = null;
302
332
  let latestActivity = null;
303
333
  let workspaceDir = null;
@@ -313,6 +343,19 @@ export async function handleTasks(manager, limit = 10) {
313
343
  failed++;
314
344
  else if (agent.status === AgentStatus.STOPPED)
315
345
  stopped++;
346
+ // Stranded = completed, no PR, local worktree still dirty. Probes the real
347
+ // worktree so `teams tasks` and `teams list --status` don't classify lost
348
+ // work as done (PHNX-2951).
349
+ if (agent.status === AgentStatus.COMPLETED && !agent.prUrl?.trim() && !agent.hostName && agent.workspaceDir) {
350
+ const delivery = resolveTeammateDelivery({
351
+ status: agent.status,
352
+ prUrl: agent.prUrl,
353
+ hasUncommittedChanges: await hasUncommittedChanges(agent.workspaceDir),
354
+ });
355
+ if (delivery === 'stranded') {
356
+ stranded++;
357
+ }
358
+ }
316
359
  // Track earliest start (created_at)
317
360
  if (!earliestStart || agent.startedAt < earliestStart) {
318
361
  earliestStart = agent.startedAt;
@@ -336,6 +379,7 @@ export async function handleTasks(manager, limit = 10) {
336
379
  pending,
337
380
  running,
338
381
  completed,
382
+ stranded,
339
383
  failed,
340
384
  stopped,
341
385
  workspace_dir: workspaceDir,
@@ -12,13 +12,17 @@
12
12
  */
13
13
  import { AgentStatus } from './agents.js';
14
14
  /** Postcondition-facing delivery of a teammate's work. */
15
- export type TeammateDelivery = 'pending' | 'in_progress' | 'pr_open' | 'pr_merged' | 'no_pr' | 'failed' | 'stopped';
15
+ export type TeammateDelivery = 'pending' | 'in_progress' | 'pr_open' | 'pr_merged' | 'no_pr' | 'stranded' | 'failed' | 'stopped';
16
16
  /**
17
17
  * Derive delivery from process status and PR state.
18
18
  *
19
19
  * When the process completed with a `prUrl` and merge is unknown or false,
20
20
  * delivery is `pr_open` — pessimistic: assume open until proven merged so an
21
21
  * orchestrator never mistakes "agent stopped" for "work on main".
22
+ *
23
+ * When the process completed with no PR and uncommitted changes remain in the
24
+ * worktree, delivery is `stranded` — the work exists only locally and will be
25
+ * lost if the worktree is cleaned up (PHNX-2951).
22
26
  */
23
27
  export declare function resolveTeammateDelivery(opts: {
24
28
  status: AgentStatus | string;
@@ -28,14 +32,20 @@ export declare function resolveTeammateDelivery(opts: {
28
32
  * `null`/`undefined` = unknown; with a `prUrl` that means `pr_open`.
29
33
  */
30
34
  prMerged?: boolean | null;
35
+ /**
36
+ * Whether the teammate's worktree has uncommitted changes. Only consulted
37
+ * when the process completed without a PR URL.
38
+ */
39
+ hasUncommittedChanges?: boolean | null;
31
40
  }): TeammateDelivery;
32
41
  /**
33
42
  * Human label for `teams status` rows. Replaces bare COMPLETED with PR OPEN
34
- * when delivery is still pending merge.
43
+ * when delivery is still pending merge, and with STRANDED when uncommitted
44
+ * work is stranded in the worktree.
35
45
  */
36
46
  export declare function deliveryDisplayLabel(delivery: TeammateDelivery, processStatus: AgentStatus | string): string;
37
47
  /**
38
- * Color key for statusColor-style switches. `pr_open` is its own key so the
39
- * row is visually distinct from green COMPLETED.
48
+ * Color key for statusColor-style switches. `pr_open` and `stranded` get their
49
+ * own keys so the rows are visually distinct from green COMPLETED.
40
50
  */
41
51
  export declare function deliveryColorKey(delivery: TeammateDelivery, processStatus: string): string;
@@ -17,6 +17,10 @@ import { AgentStatus } from './agents.js';
17
17
  * When the process completed with a `prUrl` and merge is unknown or false,
18
18
  * delivery is `pr_open` — pessimistic: assume open until proven merged so an
19
19
  * orchestrator never mistakes "agent stopped" for "work on main".
20
+ *
21
+ * When the process completed with no PR and uncommitted changes remain in the
22
+ * worktree, delivery is `stranded` — the work exists only locally and will be
23
+ * lost if the worktree is cleaned up (PHNX-2951).
20
24
  */
21
25
  export function resolveTeammateDelivery(opts) {
22
26
  const status = String(opts.status);
@@ -30,8 +34,9 @@ export function resolveTeammateDelivery(opts) {
30
34
  return 'stopped';
31
35
  if (status === AgentStatus.COMPLETED || status === 'completed') {
32
36
  const prUrl = opts.prUrl?.trim();
33
- if (!prUrl)
34
- return 'no_pr';
37
+ if (!prUrl) {
38
+ return opts.hasUncommittedChanges ? 'stranded' : 'no_pr';
39
+ }
35
40
  if (opts.prMerged === true)
36
41
  return 'pr_merged';
37
42
  return 'pr_open';
@@ -40,21 +45,26 @@ export function resolveTeammateDelivery(opts) {
40
45
  }
41
46
  /**
42
47
  * Human label for `teams status` rows. Replaces bare COMPLETED with PR OPEN
43
- * when delivery is still pending merge.
48
+ * when delivery is still pending merge, and with STRANDED when uncommitted
49
+ * work is stranded in the worktree.
44
50
  */
45
51
  export function deliveryDisplayLabel(delivery, processStatus) {
46
52
  if (delivery === 'pr_open')
47
53
  return 'PR OPEN';
48
54
  if (delivery === 'pr_merged')
49
55
  return 'COMPLETED';
56
+ if (delivery === 'stranded')
57
+ return 'STRANDED';
50
58
  return String(processStatus).toUpperCase();
51
59
  }
52
60
  /**
53
- * Color key for statusColor-style switches. `pr_open` is its own key so the
54
- * row is visually distinct from green COMPLETED.
61
+ * Color key for statusColor-style switches. `pr_open` and `stranded` get their
62
+ * own keys so the rows are visually distinct from green COMPLETED.
55
63
  */
56
64
  export function deliveryColorKey(delivery, processStatus) {
57
65
  if (delivery === 'pr_open')
58
66
  return 'pr_open';
67
+ if (delivery === 'stranded')
68
+ return 'stranded';
59
69
  return String(processStatus);
60
70
  }
@@ -0,0 +1,85 @@
1
+ /**
2
+ * schema2-build — the PRODUCER's per-tool mappers + `buildSessionDetailV2`
3
+ * (PHNX-3442 step 2, increments 2-4).
4
+ *
5
+ * Populates the `SessionStepV2` discriminated union (schema2.ts) from the parsed
6
+ * session events, reusing the SAME infrastructure the schema-1 path already uses:
7
+ *
8
+ * - the callId pairing loop (`pairSteps` in session/trajectory.ts) — so a step's
9
+ * (use event, result event) triple is recovered without a duplicate loop;
10
+ * - bash unwrap/tokenize/classify (`session/bash-command.ts`) + the effective
11
+ * program resolver (`effectiveProgram`);
12
+ * - the meta / whereItWentWrong / surfacedToolFailures / active-time helpers
13
+ * factored out of sync.ts (`buildDetailMeta`, `buildWhereItWentWrong`, …).
14
+ *
15
+ * The command/patch/output PARSING lives here; the worker stores the shard
16
+ * opaquely and the console reads the union directly and never reparses (spec §5).
17
+ *
18
+ * category / risk / categoryMetrics are DELIBERATELY omitted from the schema-2
19
+ * detail: the shipped consumer (`decodeSessionDetail` → coerceCategory/Risk/Metrics)
20
+ * backfills them to the same neutral defaults it uses for schema-1, so computing
21
+ * them here would be inventing session-level signal this step does not own.
22
+ */
23
+ import { type BashCategory as ClassifierCategory } from '../session/bash-command.js';
24
+ import { type SessionTrajectory } from '../session/trajectory.js';
25
+ import type { SessionEvent } from '../session/types.js';
26
+ import type { BashAction, BashCategory, SessionStepV2 } from './schema2.js';
27
+ import { activeMsFromTrajectory, type SessionDetail } from './sync.js';
28
+ /**
29
+ * SessionDetailV2 — the schema-2 shard this producer emits.
30
+ *
31
+ * `category` / `risk` / `categoryMetrics` are DELIBERATELY OMITTED. The consumer's
32
+ * `SessionDetailV2` declares them, but its `decodeSchema2` backfills neutral
33
+ * defaults via `coerceCategory`/`coerceRisk`/`coerceMetrics` (never throws) — the
34
+ * same defaulting it applies to schema-1 shards today. This producer does not yet
35
+ * author those fields (their provenance is the prix/api PHNX-3351 hosted backend,
36
+ * not agents-cli), so emitting them here would fabricate classification. Omission
37
+ * is the honest choice and is asserted as a tested contract in
38
+ * `schema2-fixture.test.ts`. Wire real category/risk here once its source is settled.
39
+ */
40
+ export interface SessionDetailV2 {
41
+ schema: 2;
42
+ id: string;
43
+ meta: SessionDetail['meta'];
44
+ steps: SessionStepV2[];
45
+ gaps: SessionTrajectory['gaps'];
46
+ truncatedSteps: number;
47
+ whereItWentWrong: string | null;
48
+ surfacedToolFailures: Array<{
49
+ tool?: string;
50
+ label: string;
51
+ detail?: string;
52
+ }>;
53
+ }
54
+ /**
55
+ * `unwrapCommand` (bash-command.ts) strips VAR=/sudo/cd&&/npx/loops/subshells but
56
+ * NOT an interpreter wrapper like `/bin/zsh -lc "…"`, `bash -lc '…'`, or `sh -c …`
57
+ * — the exact shape the managed runner wraps every command in. Peel that first,
58
+ * then hand the inner payload to the existing unwrapper so all the wrappers it DOES
59
+ * know still apply. One extra rule, at the source, not a fork of unwrapCommand.
60
+ */
61
+ export declare function unwrapShellExec(command: string): string;
62
+ /**
63
+ * Map a classifier `BashCategory` (the rich vcs|build-test|install|… taxonomy) to
64
+ * the coarse schema-2 `BashCategory` (build|test|git|network|other). `build-test`
65
+ * needs the argv/subcommand to decide build vs test — `bun test` is test, `bun
66
+ * build` is build — so this takes the tokenized argv too.
67
+ */
68
+ export declare function mapBashCategory(cat: ClassifierCategory, argv: string[]): BashCategory;
69
+ /** Build the per-segment BashAction list for a bash command. */
70
+ export declare function buildBashActions(unwrapped: string): BashAction[];
71
+ export interface BuildDetailV2Options {
72
+ redact?: boolean;
73
+ knownSecrets?: readonly string[];
74
+ }
75
+ /**
76
+ * Build the schema-2 per-session detail from a pre-built trajectory and its raw
77
+ * events. The trajectory supplies meta/gaps/whereItWentWrong/surfacedToolFailures
78
+ * (via the shared sync.ts helpers) and the truncation count; the raw events supply
79
+ * the per-tool detail the schema-1 flat step could not carry.
80
+ *
81
+ * Re-pairs the events with `pairSteps` (the SAME loop buildTrajectory ran) to
82
+ * recover each step's (use event, result event) triple, then dispatches per tool.
83
+ */
84
+ export declare function buildSessionDetailV2(traj: SessionTrajectory, events: SessionEvent[], options?: BuildDetailV2Options): SessionDetailV2;
85
+ export { activeMsFromTrajectory };