@oh-my-pi/pi-coding-agent 17.0.2 → 17.0.4

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 (80) hide show
  1. package/CHANGELOG.md +80 -20
  2. package/dist/cli.js +4087 -4108
  3. package/dist/types/config/settings-schema.d.ts +7 -3
  4. package/dist/types/eval/agent-bridge.d.ts +7 -19
  5. package/dist/types/extensibility/legacy-pi-coding-agent-shim.d.ts +15 -1
  6. package/dist/types/lsp/client.d.ts +2 -0
  7. package/dist/types/lsp/types.d.ts +3 -0
  8. package/dist/types/mcp/transports/stdio.d.ts +29 -0
  9. package/dist/types/mnemopi/state.d.ts +32 -9
  10. package/dist/types/modes/components/agent-hub.d.ts +15 -0
  11. package/dist/types/registry/persisted-agents.d.ts +3 -0
  12. package/dist/types/sdk.d.ts +14 -3
  13. package/dist/types/session/session-entries.d.ts +6 -1
  14. package/dist/types/session/session-manager.d.ts +5 -0
  15. package/dist/types/task/executor.d.ts +26 -1
  16. package/dist/types/task/index.d.ts +1 -1
  17. package/dist/types/task/parallel.d.ts +14 -0
  18. package/dist/types/task/structured-subagent.d.ts +111 -0
  19. package/dist/types/task/types.d.ts +51 -0
  20. package/dist/types/tools/fetch.d.ts +4 -5
  21. package/dist/types/tools/hub/messaging.d.ts +5 -1
  22. package/dist/types/tools/index.d.ts +16 -1
  23. package/dist/types/tools/todo.d.ts +31 -0
  24. package/dist/types/tui/tree-list.d.ts +7 -0
  25. package/dist/types/utils/markit.d.ts +11 -0
  26. package/package.json +12 -12
  27. package/src/cli/file-processor.ts +1 -2
  28. package/src/config/settings-schema.ts +4 -3
  29. package/src/eval/__tests__/agent-bridge.test.ts +133 -47
  30. package/src/eval/__tests__/prelude-agent.test.ts +29 -0
  31. package/src/eval/agent-bridge.ts +104 -477
  32. package/src/eval/jl/prelude.jl +7 -6
  33. package/src/eval/js/shared/prelude.txt +5 -4
  34. package/src/eval/py/prelude.py +11 -39
  35. package/src/eval/rb/prelude.rb +5 -6
  36. package/src/extensibility/legacy-pi-coding-agent-shim.ts +53 -7
  37. package/src/lsp/client.ts +57 -0
  38. package/src/lsp/index.ts +62 -6
  39. package/src/lsp/types.ts +3 -0
  40. package/src/mcp/oauth-flow.ts +20 -0
  41. package/src/mcp/transports/stdio.test.ts +269 -1
  42. package/src/mcp/transports/stdio.ts +152 -1
  43. package/src/mnemopi/backend.ts +1 -1
  44. package/src/mnemopi/embed-worker.ts +8 -7
  45. package/src/mnemopi/state.ts +45 -18
  46. package/src/modes/components/agent-hub.ts +1 -72
  47. package/src/modes/components/bash-execution.ts +7 -3
  48. package/src/modes/components/eval-execution.ts +3 -1
  49. package/src/modes/components/transcript-container.ts +13 -7
  50. package/src/modes/interactive-mode.ts +30 -8
  51. package/src/prompts/system/system-prompt.md +1 -1
  52. package/src/prompts/tools/eval.md +5 -2
  53. package/src/prompts/tools/read.md +1 -1
  54. package/src/prompts/tools/task.md +12 -8
  55. package/src/prompts/tools/write.md +1 -1
  56. package/src/registry/persisted-agents.ts +74 -0
  57. package/src/sdk.ts +129 -87
  58. package/src/session/agent-session.ts +75 -21
  59. package/src/session/session-entries.ts +6 -1
  60. package/src/session/session-loader.ts +31 -3
  61. package/src/session/session-manager.ts +9 -0
  62. package/src/session/streaming-output.ts +41 -1
  63. package/src/stt/recorder.ts +68 -55
  64. package/src/system-prompt.ts +7 -2
  65. package/src/task/executor.ts +99 -21
  66. package/src/task/index.ts +258 -429
  67. package/src/task/parallel.ts +43 -0
  68. package/src/task/persisted-revive.ts +19 -7
  69. package/src/task/structured-subagent.ts +642 -0
  70. package/src/task/types.ts +61 -0
  71. package/src/tools/__tests__/eval-description.test.ts +1 -1
  72. package/src/tools/fetch.ts +28 -105
  73. package/src/tools/hub/messaging.ts +16 -3
  74. package/src/tools/index.ts +47 -14
  75. package/src/tools/path-utils.ts +1 -0
  76. package/src/tools/read.ts +14 -26
  77. package/src/tools/todo.ts +126 -13
  78. package/src/tui/tree-list.ts +39 -0
  79. package/src/utils/markit.ts +12 -0
  80. package/src/utils/zip.ts +16 -1
@@ -13,17 +13,15 @@
13
13
  *
14
14
  * Replaces the old SessionObserverOverlayComponent (ctrl+s observer).
15
15
  */
16
- import * as fs from "node:fs";
17
- import * as path from "node:path";
18
16
  import { type AgentTool, ThinkingLevel } from "@oh-my-pi/pi-agent-core";
19
17
  import { Container, Ellipsis, matchesKey, type OverlayHandle, padding, type TUI, visibleWidth } from "@oh-my-pi/pi-tui";
20
18
  import { formatAge, getProjectDir, logger } from "@oh-my-pi/pi-utils";
21
- import { ADVISOR_TRANSCRIPT_FILENAME, isAdvisorTranscriptName } from "../../advisor";
22
19
  import type { KeyId } from "../../config/keybindings";
23
20
  import type { MessageRenderer } from "../../extensibility/extensions/types";
24
21
  import { IrcBus } from "../../irc/bus";
25
22
  import { AgentLifecycleManager } from "../../registry/agent-lifecycle";
26
23
  import { type AgentRef, AgentRegistry, type AgentStatus, MAIN_AGENT_ID } from "../../registry/agent-registry";
24
+ import { registerPersistedSubagents } from "../../registry/persisted-agents";
27
25
  import { USER_INTERRUPT_LABEL } from "../../session/messages";
28
26
  import { parseThinkingLevel } from "../../thinking";
29
27
  import { replaceTabs, TRUNCATE_LENGTHS, truncateToWidth } from "../../tools/render-utils";
@@ -100,75 +98,6 @@ function modelBadge(ref: AgentRef, observed: ObservableSession | undefined): str
100
98
  return formatModelBadge(selector.slice(selector.indexOf("/") + 1), level);
101
99
  }
102
100
 
103
- async function registerPersistedSubagents(
104
- registry: AgentRegistry,
105
- sessionFile: string | null | undefined,
106
- ): Promise<void> {
107
- if (!sessionFile?.endsWith(".jsonl")) return;
108
- const root = sessionFile.slice(0, -6);
109
- await registerPersistedSubagentsFromDir(registry, root, undefined);
110
- }
111
-
112
- async function registerPersistedSubagentsFromDir(
113
- registry: AgentRegistry,
114
- dir: string,
115
- parentId: string | undefined,
116
- ): Promise<void> {
117
- let entries: fs.Dirent[];
118
- try {
119
- entries = await fs.promises.readdir(dir, { withFileTypes: true });
120
- } catch {
121
- return;
122
- }
123
- for (const entry of entries) {
124
- if (!entry.isFile() || !entry.name.endsWith(".jsonl") || entry.name.includes(".bak")) continue;
125
- const sessionFile = path.join(dir, entry.name);
126
- // The advisor transcript is observability-only: register it as a non-peer
127
- // `advisor` kind under its owning session so the Hub can show its read-only
128
- // transcript, but it never joins agent-facing rosters and is not revivable.
129
- if (isAdvisorTranscriptName(entry.name)) {
130
- const owner = parentId ?? MAIN_AGENT_ID;
131
- // `__advisor.jsonl` → the default advisor (no slug); `__advisor.<slug>.jsonl`
132
- // → a named advisor, keyed and labeled by its slug.
133
- const slug =
134
- entry.name === ADVISOR_TRANSCRIPT_FILENAME ? "" : entry.name.slice("__advisor.".length, -".jsonl".length);
135
- const advisorId = slug ? `${owner}/advisor:${slug}` : `${owner}/advisor`;
136
- const displayName = slug ? `advisor:${slug}` : "advisor";
137
- const existing = registry.get(advisorId);
138
- // Never clobber a non-advisor ref that happens to share this id (a freak
139
- // user task literally named `<owner>/advisor`): leave it, skip the advisor.
140
- if (existing && existing.kind !== "advisor") continue;
141
- if (existing?.sessionFile !== sessionFile) {
142
- // The id is reused across `/new`; refresh it to the current session's file.
143
- if (existing) registry.unregister(advisorId);
144
- registry.register({
145
- id: advisorId,
146
- displayName,
147
- kind: "advisor",
148
- parentId: owner,
149
- session: null,
150
- sessionFile,
151
- status: "parked",
152
- });
153
- }
154
- continue;
155
- }
156
- const id = entry.name.slice(0, -6);
157
- if (!registry.get(id)) {
158
- registry.register({
159
- id,
160
- displayName: id,
161
- kind: "sub",
162
- parentId: parentId ?? MAIN_AGENT_ID,
163
- session: null,
164
- sessionFile,
165
- status: "parked",
166
- });
167
- }
168
- await registerPersistedSubagentsFromDir(registry, path.join(dir, id), id);
169
- }
170
- }
171
-
172
101
  /** Result of one host-backed transcript read for the Agent Hub viewer. */
173
102
  export interface AgentHubRemoteTranscript {
174
103
  text: string;
@@ -146,14 +146,18 @@ export class BashExecutionComponent extends Container {
146
146
  #updateDisplay(): void {
147
147
  const availableLines = this.#outputLines;
148
148
 
149
- // Apply preview truncation based on expanded state
149
+ // Full output is shown when expanded or when sixel passthrough renders
150
+ // the raw payload; the collapsed preview shows only the tail window.
150
151
  const previewLogicalLines = availableLines.slice(-PREVIEW_LINES);
151
- const hiddenLineCount = availableLines.length - previewLogicalLines.length;
152
152
  const sixelLineMask =
153
153
  TERMINAL.imageProtocol === ImageProtocol.Sixel && isSixelPassthroughEnabled()
154
154
  ? getSixelLineMask(availableLines)
155
155
  : undefined;
156
156
  const hasSixelOutput = sixelLineMask?.some(Boolean) ?? false;
157
+ const showingAllLines = this.#expanded || hasSixelOutput;
158
+ // Only the collapsed preview hides lines; when the full output is shown
159
+ // the footer must not keep advertising hidden lines / ctrl+o.
160
+ const hiddenLineCount = showingAllLines ? 0 : availableLines.length - previewLogicalLines.length;
157
161
 
158
162
  // Rebuild content container
159
163
  this.#contentContainer.clear();
@@ -163,7 +167,7 @@ export class BashExecutionComponent extends Container {
163
167
 
164
168
  // Output
165
169
  if (availableLines.length > 0) {
166
- if (this.#expanded || hasSixelOutput) {
170
+ if (showingAllLines) {
167
171
  const displayText = availableLines
168
172
  .map((line, index) => (sixelLineMask?.[index] ? line : theme.fg("muted", line)))
169
173
  .join("\n");
@@ -114,7 +114,9 @@ export class EvalExecutionComponent extends Container {
114
114
  #updateDisplay(): void {
115
115
  const availableLines = this.#outputLines;
116
116
  const previewLogicalLines = availableLines.slice(-PREVIEW_LINES);
117
- const hiddenLineCount = availableLines.length - previewLogicalLines.length;
117
+ // Only the collapsed preview hides lines; when expanded the footer must
118
+ // not keep advertising hidden lines / ctrl+o.
119
+ const hiddenLineCount = this.#expanded ? 0 : availableLines.length - previewLogicalLines.length;
118
120
 
119
121
  this.#contentContainer.clear();
120
122
 
@@ -19,11 +19,17 @@ interface FinalizableBlock {
19
19
  /**
20
20
  * Monotonic content version for blocks that can still mutate *after*
21
21
  * reporting finalized (e.g. `AssistantMessageComponent`: the inline error
22
- * restored at the next turn's `agent_start`, late tool-result images). The
23
- * committed-scrollback render bypass only replays a block's previous rows
24
- * when the version is unchanged; without this signal a post-finalize
25
- * mutation would stay invisible until a global invalidation. Blocks that
26
- * never mutate post-finalize simply omit the method.
22
+ * restored at the next turn's `agent_start`, late tool-result images). While
23
+ * the block's rows are still on screen (not yet fully committed), the
24
+ * committed-scrollback render bypass replays its previous rows only when the
25
+ * version is unchanged; a bump forces a real render so the TUI's
26
+ * committed-prefix audit can observe and re-anchor the change. Once the rows
27
+ * fully commit to native scrollback they are dropped from the local frame
28
+ * (compacted) — a later mutation no longer recommits on an ordinary frame
29
+ * (immutable history the terminal owns; recommitting would duplicate it) and
30
+ * instead rehydrates on the next destructive full replay
31
+ * ({@link "@oh-my-pi/pi-tui".NativeScrollbackReplay}). Blocks that never
32
+ * mutate post-finalize simply omit the method.
27
33
  */
28
34
  getTranscriptBlockVersion?(): number;
29
35
  /**
@@ -124,7 +130,7 @@ interface BlockSegment {
124
130
  sep: number;
125
131
  /** Whether the block reported finalized when this segment was rendered. */
126
132
  finalized: boolean;
127
- /** Safe to drop after commit: produced while finalized, without post-finalize version tracking. */
133
+ /** Safe to drop from the local frame once its rows fully commit to native scrollback: produced while finalized. */
128
134
  compactable: boolean;
129
135
  /** Block version observed when this segment was rendered (see {@link FinalizableBlock}). */
130
136
  version: number | undefined;
@@ -453,7 +459,7 @@ export class TranscriptContainer
453
459
  previous.width === width &&
454
460
  previous.generation === this.#generation);
455
461
  const contribution = reusable ? previous.contribution : stripPlainBlankEdges(raw);
456
- const compactable = finalized && version === undefined && previous?.finalized !== false;
462
+ const compactable = finalized && previous?.finalized !== false;
457
463
 
458
464
  // Empty (or stripped-to-nothing) children contribute nothing and never
459
465
  // affect spacing. An empty still-live child still gates the commit
@@ -123,7 +123,12 @@ import type { LspStartupServerInfo } from "../tools";
123
123
  import { normalizeLocalScheme } from "../tools/path-utils";
124
124
  import { replaceTabs, TRUNCATE_LENGTHS, truncateToWidth } from "../tools/render-utils";
125
125
  import { setAutoQaConsentHandler } from "../tools/report-tool-issue";
126
- import { formatPhaseDisplayName, todoMatchesAnyDescription } from "../tools/todo";
126
+ import {
127
+ formatPhaseDisplayName,
128
+ selectCollapsedTodos,
129
+ setActiveTodoDescriptionsProvider,
130
+ todoMatchesAnyDescription,
131
+ } from "../tools/todo";
127
132
  import { ToolError } from "../tools/tool-errors";
128
133
  import { vocalizer } from "../tts/vocalizer";
129
134
  import { renderTreeList } from "../tui/tree-list";
@@ -951,6 +956,9 @@ export class InteractiveMode implements InteractiveModeContext {
951
956
  this.#observerRegistry.onChange(kind => {
952
957
  this.#scheduleObserverUiSync(kind);
953
958
  });
959
+ // Let the transient todo tool result light up pending todos executed by a
960
+ // live subagent, matching the sticky HUD's active set (#5873).
961
+ setActiveTodoDescriptionsProvider(() => this.#getActiveSubagentDescriptions());
954
962
 
955
963
  // Load initial todos
956
964
  await this.#loadTodoList();
@@ -1895,15 +1903,29 @@ export class InteractiveMode implements InteractiveModeContext {
1895
1903
  const isMatched = (todo: TodoItem): boolean =>
1896
1904
  activeDescs.length > 0 && todoMatchesAnyDescription(todo.content, activeDescs);
1897
1905
 
1898
- // Task subtree for a phase. Collapsed previews the first open tasks — the
1899
- // stage's `done/total` makes the hidden count obvious, so there is no
1900
- // "… more" row; expanded lists every task.
1906
+ // Task subtree for a phase. Collapsed runs the shared walking-viewport
1907
+ // policy (completed/abandoned omitted, active work pulled to the head,
1908
+ // then following pending tasks) so the HUD and the transient tool result
1909
+ // can never disagree about the current work (#5873). Expanded lists all.
1901
1910
  const renderTasks = (phase: TodoPhase): string[] => {
1902
- const open = phase.tasks.filter(t => t.status === "pending" || t.status === "in_progress");
1903
- const base = expanded ? phase.tasks : open.length > 0 ? open : phase.tasks;
1904
- const items = expanded ? base : base.slice(0, activeTaskCap);
1911
+ if (expanded) {
1912
+ return renderTreeList(
1913
+ {
1914
+ items: phase.tasks,
1915
+ expanded: true,
1916
+ renderItem: todo => this.#formatTodoLine(todo, "", isMatched(todo)),
1917
+ },
1918
+ theme,
1919
+ );
1920
+ }
1921
+ const selection = selectCollapsedTodos(phase.tasks, isMatched, activeTaskCap);
1905
1922
  return renderTreeList(
1906
- { items, expanded: true, renderItem: todo => this.#formatTodoLine(todo, "", isMatched(todo)) },
1923
+ {
1924
+ items: selection.items,
1925
+ itemType: "task",
1926
+ trailingSummary: selection.summary,
1927
+ renderItem: todo => this.#formatTodoLine(todo, "", isMatched(todo)),
1928
+ },
1907
1929
  theme,
1908
1930
  );
1909
1931
  };
@@ -57,7 +57,7 @@ Special URLs for internal resources; with most FS/bash tools they auto-resolve t
57
57
  - `memory://root`: project memory summary
58
58
  {{/if}}
59
59
  - `agent://<id>`: agent output artifact; `/<child>` reads a nested subagent's output, else `/<path>` extracts a JSON field
60
- - `history://<id>`: read-only markdown transcript of an agent (live, parked, or released); bare `history://` lists all agents. Serves any agent whose session file persists on disk, not just registered peers.
60
+ - `history://<id>`: read-only markdown transcript of an agent (live, parked, or released); bare `history://` lists all agents. Serves registered agents process-wide plus persisted subagents discoverable from their artifact trees; does not discover unregistered top-level sessions solely from their persisted session files.
61
61
  - `artifact://<id>`: artifact content
62
62
  - `local://<name>.md`: plan artifacts or shared content for subagents
63
63
  {{#if hasObsidian}}
@@ -17,9 +17,12 @@ write(path, content) → str
17
17
  env(key?=None, value?=None) → str | None | dict
18
18
  output(*ids, format?="raw", query?=None, offset?=None, limit?=None) → str | dict | list[dict]
19
19
  tool.<name>(args) → unknown
20
+ Invoke any session tool; `args` = its parameter object.
20
21
  completion(prompt, model?="default"|"smol"|"slow", system?=None, schema?=None) → str | dict
21
- {{#if spawns}}agent(prompt, agent?="{{spawnDefaultAgent}}", model?=None, schema?=None, handle?=False) str | dict{{#if spawnAllowedAgentsText}} Allowed: {{spawnAllowedAgentsText}}.{{/if}}
22
- {{#if js}} JS: agent(prompt, { agent, schema, handle }).{{/if}}
22
+ Oneshot, stateless (no history/tools). `model`: "smol" fast | "default" session | "slow" most capable. `schema` (JSON-Schema) → parsed object.
23
+ {{#if spawns}}agent(prompt, agent?="{{spawnDefaultAgent}}", model?=None, label?=None, schema?=None, schema{{#if js}}Mode{{else}}_mode{{/if}}?="permissive", isolated?=None, apply?=None, merge?=None, handle?=False) → str | dict
24
+ Run a subagent → final output. `agent` selects a discovered agent; omit it to use `{{spawnDefaultAgent}}`.{{#if spawnAllowedAgentsText}} Allowed agents: {{spawnAllowedAgentsText}}.{{/if}} `schema` overrides agent/session schemas; `schemaMode`/`schema_mode`: "permissive" | "strict". Effective schemas return parsed data. `isolated` requests a worktree; `apply`/`merge` control its changes. Background via `local://` files named in the prompt. `handle` → { text, output, handle: "agent://<id>", id, agent }, parsed `data` when structured.
25
+ {{#if js}} JS: ONE trailing object — agent(prompt, { agent, model, label, schema, schemaMode, isolated, apply, merge, handle }).{{/if}}
23
26
  {{/if}}
24
27
  parallel(thunks) → list pipeline(items, ...stages) → list
25
28
  log(message) → None phase(title) → None
@@ -15,7 +15,7 @@ Read files, directories, archives, SQLite, images, documents, internal resources
15
15
  - {{#if IS_HL_MODE}}File + selector → `[foo.ts#1A2B]` snapshot header + numbered lines. Copy `[FILENAME#TAG]` for anchored edits; NEVER fabricate the tag.{{/if}}
16
16
  - Directory → depth-limited dirent listing.
17
17
  - SQLite (`.sqlite`, `.sqlite3`, `.db`, `.db3`): `file.db` (tables), `file.db:table` (schema+rows), `file.db:table:key` (by PK), `?limit=`/`?where=`/`?q=SELECT`.
18
- - Archives (`.tar`, `.tar.gz`, `.tgz`, `.zip`): `archive.ext:path/inside/archive` reads a member.
18
+ - Archives (`.tar`, `.tar.gz`, `.tgz`, `.zip`, plus ZIP-based `.jar`/`.war`/`.ear`/`.apk`): `archive.ext:path/inside/archive` reads a member.
19
19
  - Documents → extracted text. Notebooks → editable cells. Images → {{#if INSPECT_IMAGE_ENABLED}}metadata; call `inspect_image`{{else}}decoded inline{{/if}}. `:raw` bypasses converters.
20
20
  - URLs → reader-mode clean text/markdown; `:raw` → untouched HTML. Bare `host:port` needs trailing slash.
21
21
  - Internal URIs — all schemes take selectors. `artifact://<id>` recovers spilled output; page with `:N-M`/`:raw:N-M`.
@@ -10,18 +10,22 @@ Agents marked BLOCKING run inline — results return in this call; non-blocking
10
10
 
11
11
  # Inputs
12
12
  {{#if batchEnabled}}
13
- - `context`: Shared project state for the entire batch don't duplicate into individual tasks.
14
- - `tasks[]`: Subagents to spawn.
15
- - `name`: CamelCase ≤32 chars (auto-generated if omitted).
16
- - `agent`: specialist type (optional).
17
- - `task`: Complete, self-contained instructions — no one-liners, no missing acceptance criteria.
13
+ - `context`: Shared project state, constraints, and contracts. Applies to the entire batch; do not duplicate this background into individual tasks.
14
+ - `tasks[]`: Array of subagents to spawn.
15
+ - `name`: A stable CamelCase identifier (≤32 chars), used to address the agent (IRC, job ids). Generated automatically if omitted.
16
+ - `agent`: The agent type running this item (e.g. `scout`, `reviewer`). Omitting it gives you the general-purpose worker (`{{defaultAgent}}`) — NEVER pass that name explicitly. Only omit it after checking the agent list below and finding no specialist that fits.{{#if allowedAgentsText}} Current spawn policy allows: {{allowedAgentsText}}.{{/if}}
17
+ - `task`: Complete, self-contained instructions. One-liners or missing acceptance criteria are PROHIBITED.
18
+ - `outputSchema`: Invocation-specific JSON Schema. Overrides the selected agent and parent-session schemas.
19
+ - `schemaMode`: `"permissive"` (default) accepts a retry-exhausted invalid result with a warning; `"strict"` fails it.
18
20
  {{#if isolationEnabled}}
19
21
  - `isolated`: Run in dedicated worktree, return patches. Destroyed on completion, cannot be addressed afterward.
20
22
  {{/if}}
21
23
  {{else}}
22
- - `name`: CamelCase ≤32 chars (auto-generated if omitted).
23
- - `agent`: specialist type (optional).
24
- - `task`: Complete, self-contained instructions — no one-liners, no missing acceptance criteria.
24
+ - `name`: A stable CamelCase identifier (≤32 chars), used to address the agent (IRC, job ids). Generated automatically if omitted.
25
+ - `agent`: The agent type to spawn (e.g. `scout`, `reviewer`). Omitting it gives you the general-purpose worker (`{{defaultAgent}}`) — NEVER pass that name explicitly. Only omit it after checking the agent list below and finding no specialist that fits.{{#if allowedAgentsText}} Current spawn policy allows: {{allowedAgentsText}}.{{/if}}
26
+ - `task`: Complete, self-contained instructions. One-liners or missing acceptance criteria are PROHIBITED.
27
+ - `outputSchema`: Invocation-specific JSON Schema. Overrides the selected agent and parent-session schemas.
28
+ - `schemaMode`: `"permissive"` (default) accepts a retry-exhausted invalid result with a warning; `"strict"` fails it.
25
29
  {{#if isolationEnabled}}
26
30
  - `isolated`: Run in dedicated worktree, return patches.
27
31
  {{/if}}
@@ -3,7 +3,7 @@ Creates or overwrites file at specified path.
3
3
  <conditions>
4
4
  - Creating new files explicitly required by task
5
5
  - Replacing entire file contents when editing would be more complex
6
- - Supports `.tar`, `.tar.gz`, `.tgz`, and `.zip` archive entries via `archive.ext:path/inside/archive`
6
+ - Supports `.tar`, `.tar.gz`, `.tgz`, `.zip`, and ZIP-based `.jar`/`.war`/`.ear`/`.apk` archive entries via `archive.ext:path/inside/archive`
7
7
  - Supports SQLite row operations via `db.sqlite:table` (insert), `db.sqlite:table:key` (update with JSON content, delete with empty content)
8
8
  </conditions>
9
9
 
@@ -0,0 +1,74 @@
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+ import { ADVISOR_TRANSCRIPT_FILENAME, isAdvisorTranscriptName } from "../advisor/transcript-recorder";
4
+ import { type AgentRegistry, MAIN_AGENT_ID } from "./agent-registry";
5
+
6
+ /** Register persisted subagent and advisor transcripts as parked registry refs. */
7
+ export async function registerPersistedSubagents(
8
+ registry: AgentRegistry,
9
+ sessionFile: string | null | undefined,
10
+ ): Promise<void> {
11
+ if (!sessionFile?.endsWith(".jsonl")) return;
12
+ const root = sessionFile.slice(0, -6);
13
+ await registerPersistedSubagentsFromDir(registry, root, undefined);
14
+ }
15
+
16
+ async function registerPersistedSubagentsFromDir(
17
+ registry: AgentRegistry,
18
+ dir: string,
19
+ parentId: string | undefined,
20
+ ): Promise<void> {
21
+ let entries: fs.Dirent[];
22
+ try {
23
+ entries = await fs.promises.readdir(dir, { withFileTypes: true });
24
+ } catch {
25
+ return;
26
+ }
27
+ for (const entry of entries) {
28
+ if (!entry.isFile() || !entry.name.endsWith(".jsonl") || entry.name.includes(".bak")) continue;
29
+ const sessionFile = path.join(dir, entry.name);
30
+ // The advisor transcript is observability-only: register it as a non-peer
31
+ // `advisor` kind under its owning session so the Hub can show its read-only
32
+ // transcript, but it never joins agent-facing rosters and is not revivable.
33
+ if (isAdvisorTranscriptName(entry.name)) {
34
+ const owner = parentId ?? MAIN_AGENT_ID;
35
+ // `__advisor.jsonl` → the default advisor (no slug); `__advisor.<slug>.jsonl`
36
+ // → a named advisor, keyed and labeled by its slug.
37
+ const slug =
38
+ entry.name === ADVISOR_TRANSCRIPT_FILENAME ? "" : entry.name.slice("__advisor.".length, -".jsonl".length);
39
+ const advisorId = slug ? `${owner}/advisor:${slug}` : `${owner}/advisor`;
40
+ const displayName = slug ? `advisor:${slug}` : "advisor";
41
+ const existing = registry.get(advisorId);
42
+ // Never clobber a non-advisor ref that happens to share this id (a freak
43
+ // user task literally named `<owner>/advisor`): leave it, skip the advisor.
44
+ if (existing && existing.kind !== "advisor") continue;
45
+ if (existing?.sessionFile !== sessionFile) {
46
+ // The id is reused across `/new`; refresh it to the current session's file.
47
+ if (existing) registry.unregister(advisorId);
48
+ registry.register({
49
+ id: advisorId,
50
+ displayName,
51
+ kind: "advisor",
52
+ parentId: owner,
53
+ session: null,
54
+ sessionFile,
55
+ status: "parked",
56
+ });
57
+ }
58
+ continue;
59
+ }
60
+ const id = entry.name.slice(0, -6);
61
+ if (!registry.get(id)) {
62
+ registry.register({
63
+ id,
64
+ displayName: id,
65
+ kind: "sub",
66
+ parentId: parentId ?? MAIN_AGENT_ID,
67
+ session: null,
68
+ sessionFile,
69
+ status: "parked",
70
+ });
71
+ }
72
+ await registerPersistedSubagentsFromDir(registry, path.join(dir, id), id);
73
+ }
74
+ }