pi-pier 0.3.0 → 0.3.1

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 (96) hide show
  1. package/README.md +12 -11
  2. package/package.json +2 -2
  3. package/schemas/role-manifest.schema.json +7 -1
  4. package/skills/pi-herdr-workflow/SKILL.md +4 -5
  5. package/src/ansi-text.ts +62 -11
  6. package/src/ask-dialog.ts +308 -0
  7. package/src/ask-user.ts +113 -112
  8. package/src/bootstrap.ts +8 -12
  9. package/src/compact-coordinator.ts +278 -261
  10. package/src/compact-economics-core.ts +77 -145
  11. package/src/config-catalog-core.ts +77 -197
  12. package/src/config-command.ts +271 -64
  13. package/src/dashboard-command.ts +38 -19
  14. package/src/efficiency-config-core.ts +287 -484
  15. package/src/efficiency-store.ts +16 -29
  16. package/src/focus-poller.ts +14 -35
  17. package/src/gc-core.ts +10 -36
  18. package/src/herdr-client.ts +82 -208
  19. package/src/history-store.ts +36 -23
  20. package/src/index-gates.ts +124 -0
  21. package/src/index-locks.ts +2 -7
  22. package/src/index-master.ts +24 -18
  23. package/src/index-notices.ts +28 -15
  24. package/src/index-pipe.ts +17 -12
  25. package/src/index-roles.ts +306 -0
  26. package/src/index-runtime.ts +2 -6
  27. package/src/index-worker.ts +4 -8
  28. package/src/index.ts +305 -564
  29. package/src/jev-client.ts +12 -17
  30. package/src/jev-core.ts +137 -87
  31. package/src/ledger.ts +13 -21
  32. package/src/lock-core.ts +28 -56
  33. package/src/manifest-compose.ts +35 -20
  34. package/src/observation-core.ts +52 -100
  35. package/src/pane-title.ts +19 -38
  36. package/src/pi-surface.ts +34 -22
  37. package/src/pier-options.ts +54 -50
  38. package/src/pipe-channel.ts +27 -59
  39. package/src/platform-paths.ts +19 -52
  40. package/src/{core → plugins}/grid-shape.ts +8 -10
  41. package/src/{core → plugins}/heat-plan.ts +20 -22
  42. package/src/plugins/observation.ts +475 -0
  43. package/src/plugins/subagent.ts +728 -0
  44. package/src/{core → plugins}/terminal.ts +167 -127
  45. package/src/{core → plugins}/todo.ts +61 -100
  46. package/src/progress-core.ts +16 -53
  47. package/src/reconcile-core.ts +24 -33
  48. package/src/reducer-core.ts +24 -60
  49. package/src/reducer-invoker.ts +266 -239
  50. package/src/renderers.ts +76 -87
  51. package/src/role-loader.ts +33 -28
  52. package/src/role-manifest.ts +23 -28
  53. package/src/role-state.ts +119 -0
  54. package/src/routing-telemetry.ts +136 -0
  55. package/src/runtime-policy.ts +25 -40
  56. package/src/session-tail.ts +65 -41
  57. package/src/settle-wake-core.ts +52 -21
  58. package/src/slim-frame.ts +25 -122
  59. package/src/stale-core.ts +17 -24
  60. package/src/storage-layout.ts +14 -43
  61. package/src/subagent-core.ts +253 -118
  62. package/src/subagent-gc.ts +109 -67
  63. package/src/subagent-poller.ts +367 -24
  64. package/src/subagent-session.ts +516 -0
  65. package/src/subagent-spawn.ts +530 -52
  66. package/src/swallow.ts +7 -17
  67. package/src/terminal-core.ts +125 -106
  68. package/src/todo-core.ts +57 -109
  69. package/src/todo-read-hook.ts +23 -39
  70. package/src/todo-reminder-core.ts +26 -51
  71. package/src/todo-window.ts +11 -12
  72. package/src/todos-service.ts +15 -35
  73. package/src/tool-error.ts +6 -13
  74. package/src/tool-gate.ts +38 -21
  75. package/src/vocab.ts +52 -25
  76. package/src/ask-multi.ts +0 -260
  77. package/src/config-guide.ts +0 -361
  78. package/src/core/observation.ts +0 -555
  79. package/src/core/subagent.ts +0 -633
  80. package/src/efficiency-config-io.ts +0 -53
  81. package/src/git-adapter.ts +0 -113
  82. package/src/notice-buffer.ts +0 -23
  83. package/src/subagent-execute.ts +0 -328
  84. package/src/subagent-git-io.ts +0 -80
  85. package/src/subagent-launch.ts +0 -114
  86. package/src/subagent-list-action.ts +0 -69
  87. package/src/subagent-output-action.ts +0 -104
  88. package/src/subagent-output-adapter.ts +0 -27
  89. package/src/subagent-output-core.ts +0 -263
  90. package/src/subagent-poll-loop.ts +0 -305
  91. package/src/subagent-port.ts +0 -27
  92. package/src/subagent-ready.ts +0 -89
  93. package/src/subagent-registry.ts +0 -70
  94. package/src/subagent-resolution.ts +0 -41
  95. package/src/subagent-scope.ts +0 -41
  96. package/src/subagent-session-io.ts +0 -230
@@ -1,114 +0,0 @@
1
- /**
2
- * Subagent `execute` planners (validation + foreground wait).
3
- *
4
- * Why: execute mixed param checks, isolate git I/O, spawn, and wait-loop
5
- * decisions. The decisions are pure and must stay testable without a pane.
6
- */
7
- import { normalizeEntryKind } from './history-store.ts';
8
-
9
- export const FOREGROUND_POLL_MS = 2_000;
10
-
11
- export type LaunchParams = {
12
- description?: unknown;
13
- prompt?: unknown;
14
- run_in_background?: unknown;
15
- cwd?: unknown;
16
- isolate?: unknown;
17
- role?: unknown;
18
- allowed_tools?: unknown;
19
- tab?: unknown;
20
- };
21
-
22
- import type { SubagentSpec } from './subagent-core.ts';
23
-
24
- export type LaunchValidation =
25
- | { kind: 'error'; text: string }
26
- | {
27
- kind: 'ok';
28
- spec: SubagentSpec;
29
- background: boolean;
30
- isolate: boolean;
31
- cwdParam: string | null;
32
- roleKind: string;
33
- suggested: string[];
34
- tab: string | null;
35
- /** Role manifest supplied by the caller (drives tool gating); null when unset. */
36
- manifestRole: string | null;
37
- };
38
-
39
- export function planLaunchValidation(
40
- params: LaunchParams | null | undefined,
41
- herdrAvailable: boolean,
42
- ): LaunchValidation {
43
- if (!herdrAvailable) {
44
- return {
45
- kind: 'error',
46
- text: 'Error: subagent requires pi to run inside a herdr-managed pane (HERDR_ENV not set).',
47
- };
48
- }
49
- const spec: SubagentSpec = {
50
- description: String(params?.description ?? 'subagent'),
51
- prompt: String(params?.prompt ?? ''),
52
- };
53
- if (!spec.prompt.trim()) {
54
- return { kind: 'error', text: 'Error: `prompt` must be a non-empty string' };
55
- }
56
- const cwdParam = typeof params?.cwd === 'string' && params.cwd.trim() ? params.cwd.trim() : null;
57
- const isolate = params?.isolate === true;
58
- if (isolate && cwdParam) {
59
- return {
60
- kind: 'error',
61
- text: 'Error: `isolate` and `cwd` are mutually exclusive — isolate creates a new worktree, cwd delegates into an existing one',
62
- };
63
- }
64
- const suggested = Array.isArray(params?.allowed_tools)
65
- ? params.allowed_tools.filter((t): t is string => typeof t === 'string' && t.trim() !== '')
66
- : [];
67
- const manifestRole =
68
- typeof params?.role === 'string' && params.role.trim() ? params.role.trim() : 'worker-default';
69
- const tab = typeof params?.tab === 'string' ? params.tab : null;
70
- return {
71
- kind: 'ok',
72
- spec,
73
- background: params?.run_in_background === true,
74
- isolate,
75
- cwdParam,
76
- roleKind: normalizeEntryKind(typeof params?.role === 'string' ? params.role.trim() : undefined),
77
- suggested,
78
- manifestRole,
79
- tab,
80
- };
81
- }
82
-
83
- export function planIsolateRepoGuard(baseSha: string | null): { kind: 'error'; text: string } | { kind: 'ok'; sha: string } {
84
- if (!baseSha) {
85
- return { kind: 'error', text: 'Error: isolate requires a git repository with at least one commit' };
86
- }
87
- return { kind: 'ok', sha: baseSha };
88
- }
89
-
90
- export type ForegroundTickPlan =
91
- | { kind: 'blocked' }
92
- | { kind: 'settled'; text: string }
93
- | { kind: 'wait'; delayMs: number }
94
- | { kind: 'collect-final' }
95
- | { kind: 'continue' };
96
-
97
- export function planForegroundTick(input: {
98
- state: string | null;
99
- session: { text: string | null; pendingTool: boolean; activity: boolean };
100
- }): ForegroundTickPlan {
101
- if (input.state === 'blocked') return { kind: 'blocked' };
102
- if (input.state === 'idle' || input.state === 'done') {
103
- if (input.session.text) return { kind: 'settled', text: input.session.text };
104
- if (input.session.pendingTool || !input.session.activity) {
105
- return { kind: 'wait', delayMs: FOREGROUND_POLL_MS };
106
- }
107
- return { kind: 'collect-final' };
108
- }
109
- return { kind: 'continue' };
110
- }
111
-
112
- export function planPatienceExpiry(alive: boolean): 'move-to-background' | 'timeout' {
113
- return alive ? 'move-to-background' : 'timeout';
114
- }
@@ -1,69 +0,0 @@
1
- import { basename } from 'node:path';
2
- import { agoText, type AliveProbe, type SubEntry } from './subagent-core.ts';
3
-
4
- export interface ListActionResult {
5
- content: Array<{ type: 'text'; text: string }>;
6
- details: Record<string, unknown>;
7
- }
8
-
9
- export interface ListActionDeps {
10
- subs: () => Iterable<SubEntry>;
11
- probeAlive: (paneId: string, cwd: string) => Promise<AliveProbe>;
12
- readAskFlag: (paneId: string) => Promise<string | null>;
13
- now?: () => number;
14
- }
15
-
16
- /** Render the live background registry without coupling it to Cordis or the tool registration. */
17
- export async function executeSubagentList(deps: ListActionDeps): Promise<ListActionResult> {
18
- const listed = [...deps.subs()].filter((sub) => sub.background);
19
- if (listed.length === 0) {
20
- return {
21
- content: [{ type: 'text', text: 'No background subagents started (from this session branch).' }],
22
- details: {},
23
- };
24
- }
25
-
26
- const byTask = new Map<string, SubEntry>();
27
- for (const sub of listed) {
28
- const previous = byTask.get(sub.taskId);
29
- if (!previous || sub.createdAt >= previous.createdAt) byTask.set(sub.taskId, sub);
30
- }
31
-
32
- const probes = new Map<string, AliveProbe>();
33
- await Promise.all([...byTask.values()].map(async (sub) => {
34
- if (sub.status === 'closed') return;
35
- try {
36
- probes.set(sub.paneId, await deps.probeAlive(sub.paneId, sub.cwd));
37
- } catch {
38
- /* Best effort for display. */
39
- }
40
- }));
41
-
42
- const now = deps.now ?? Date.now;
43
- const lines: string[] = [];
44
- for (const sub of byTask.values()) {
45
- const tabTag = sub.tabName ? ` [tab: ${sub.tabName}]` : '';
46
- const wtTag = sub.isolate ? ` [wt: ${sub.isolate.branch}]` : '';
47
- if (sub.status === 'closed') {
48
- lines.push(`${sub.taskId.slice(0, 8)} [idle] (${sub.kind}, closed; action send revives)${tabTag}${wtTag} ${sub.description}`);
49
- continue;
50
- }
51
-
52
- const state = sub.status === 'settled' ? 'idle' : 'running';
53
- const takeoverMark = sub.userTakeover ? ', user-controlled' : '';
54
- const probe = probes.get(sub.paneId);
55
- const statusTag = probe?.agentStatus ? ` ${probe.agentStatus}` : '';
56
- const activityTag = probe?.lastActivityMs != null ? `, active ${agoText(probe.lastActivityMs, now())}` : '';
57
- let gateTag = '';
58
- if (probe?.agentStatus === 'blocked') {
59
- const question = await deps.readAskFlag(sub.paneId);
60
- gateTag = question ? ` — AWAITING HUMAN: "${question}"` : ' — AWAITING HUMAN decision';
61
- }
62
- const cwdTag = probe?.foregroundCwd && probe.foregroundCwd !== sub.cwd
63
- ? ` [cwd: ${basename(probe.foregroundCwd) || probe.foregroundCwd}]`
64
- : '';
65
- lines.push(`${sub.paneId} [${state}${takeoverMark}${statusTag}${activityTag}${gateTag}] (${sub.kind})${tabTag}${wtTag}${cwdTag} ${sub.description}`);
66
- }
67
-
68
- return { content: [{ type: 'text', text: lines.join('\n') }], details: {} };
69
- }
@@ -1,104 +0,0 @@
1
- import type { HerdrAgentState, HerdrClientLike } from './herdr-client.ts';
2
- import type { SubEntry } from './subagent-core.ts';
3
- import {
4
- computeSubagentOutputDelta,
5
- formatSubagentOutput,
6
- resolveSubagentStatus,
7
- type SubagentOutputCursor,
8
- } from './subagent-output-core.ts';
9
- import { readSubagentOutput } from './subagent-output-adapter.ts';
10
- import { toolError } from './tool-error.ts';
11
-
12
- export interface OutputActionParams {
13
- agentId?: unknown;
14
- taskId?: unknown;
15
- max_chars?: unknown;
16
- maxChars?: unknown;
17
- }
18
-
19
- export interface OutputActionResult {
20
- content: Array<{ type: 'text'; text: string }>;
21
- details: Record<string, unknown>;
22
- }
23
-
24
- export interface OutputActionDeps {
25
- client: HerdrClientLike;
26
- resolveEntry: (rawId: string, cwd: string) => { entry: SubEntry } | { error: string };
27
- readAskFlag: (paneId: string) => Promise<string | null>;
28
- outputCursors: Map<string, SubagentOutputCursor>;
29
- getCwd: (toolCtx: unknown) => string;
30
- }
31
-
32
- export async function executeSubagentOutput(
33
- params: OutputActionParams | undefined,
34
- toolCtx: unknown,
35
- deps: OutputActionDeps,
36
- ): Promise<OutputActionResult> {
37
- const rawId = String(params?.agentId ?? params?.taskId ?? '').trim();
38
- if (!rawId) return toolError('Error: missing agentId for output (see action list)') as OutputActionResult;
39
-
40
- const cwd = deps.getCwd(toolCtx);
41
- const resolved = deps.resolveEntry(rawId, cwd);
42
- if ('error' in resolved) return toolError(resolved.error) as OutputActionResult;
43
-
44
- const entry = resolved.entry;
45
- let agentState: HerdrAgentState | null = null;
46
- let askFlag: string | null = null;
47
- try {
48
- const agents = await deps.client.listAgents();
49
- const agent = agents.find((candidate) => candidate.paneId === entry.paneId);
50
- agentState = agent?.status ?? null;
51
- askFlag = agent?.tokens?.['pi-ask'] ?? null;
52
- } catch {
53
- /* Best effort: output remains useful without live status. */
54
- }
55
- if (!askFlag && agentState === 'blocked') {
56
- try {
57
- askFlag = await deps.readAskFlag(entry.paneId);
58
- } catch {
59
- /* Best effort. */
60
- }
61
- }
62
-
63
- const status = resolveSubagentStatus({
64
- localStatus: entry.status,
65
- herdrStatus: agentState,
66
- hasAskFlag: Boolean(askFlag),
67
- });
68
-
69
- let rawRead: { text: string; revision: number; truncated: boolean };
70
- try {
71
- rawRead = await readSubagentOutput(deps.client, entry.paneId);
72
- } catch (err) {
73
- const message = err instanceof Error ? err.message : String(err);
74
- return toolError(`Error: failed to read output for subagent ${entry.paneId}: ${message}`) as OutputActionResult;
75
- }
76
-
77
- const prevCursor = deps.outputCursors.get(entry.paneId);
78
- const maxChars = typeof params?.max_chars === 'number'
79
- ? params.max_chars
80
- : typeof params?.maxChars === 'number' ? params.maxChars : undefined;
81
- const deltaResult = computeSubagentOutputDelta(prevCursor, rawRead.text, { maxChars });
82
- deps.outputCursors.set(entry.paneId, deltaResult.nextCursor);
83
-
84
- const formatted = formatSubagentOutput({
85
- paneId: entry.paneId,
86
- status,
87
- revision: rawRead.revision,
88
- bufferTruncated: rawRead.truncated,
89
- deltaResult,
90
- askQuestion: askFlag,
91
- });
92
-
93
- return {
94
- content: [{ type: 'text', text: formatted }],
95
- details: {
96
- paneId: entry.paneId,
97
- status,
98
- revision: rawRead.revision,
99
- truncated: rawRead.truncated || deltaResult.truncated,
100
- restart: deltaResult.restart,
101
- deltaLength: deltaResult.delta.length,
102
- },
103
- };
104
- }
@@ -1,27 +0,0 @@
1
- /**
2
- * Herdr output-read adapter for subagents.
3
- *
4
- * Newer herdr exposes agent.read; older versions only expose pane.read. Prefer the semantic agent
5
- * endpoint and fall back to the pane endpoint so output observation stays version-compatible.
6
- */
7
- import type { HerdrClientLike } from './herdr-client.ts';
8
-
9
- export interface SubagentOutputRead {
10
- text: string;
11
- revision: number;
12
- truncated: boolean;
13
- }
14
-
15
- export async function readSubagentOutput(
16
- client: HerdrClientLike,
17
- paneId: string,
18
- ): Promise<SubagentOutputRead> {
19
- if (typeof client.readAgent === 'function') {
20
- try {
21
- return await client.readAgent(paneId, { source: 'recent', stripAnsi: true });
22
- } catch {
23
- return await client.readPane(paneId, { source: 'recent', stripAnsi: true });
24
- }
25
- }
26
- return await client.readPane(paneId, { source: 'recent', stripAnsi: true });
27
- }
@@ -1,263 +0,0 @@
1
- /**
2
- * subagent-output-core.ts
3
- *
4
- * Pure functions for subagent incremental output extraction:
5
- * - Cursor management (in-memory per-pane tail tracking)
6
- * - Longest common suffix/prefix delta computation
7
- * - Boundary handling for scrolling, clear-screen, repeated text (graceful degradation with restart: true)
8
- * - Bounded character budgeting and metadata formatting
9
- */
10
- import type { HerdrAgentState } from './herdr-client.ts';
11
- import type { SubEntry } from './subagent-core.ts';
12
-
13
- export const OUTPUT_DEFAULT_MAX_CHARS = 6000;
14
- export const OUTPUT_HARD_CAP_CHARS = 16000;
15
- export const OUTPUT_MIN_OVERLAP = 16;
16
- export const OUTPUT_TAIL_CHARS = 500;
17
-
18
- export type SubagentStatus = 'running' | 'idle' | 'blocked' | 'settled';
19
-
20
- export interface SubagentOutputCursor {
21
- /** Suffix of previous full text used for boundary matching */
22
- tail: string;
23
- /** Length of previous full text */
24
- fullLength: number;
25
- }
26
-
27
- export interface SubagentOutputDeltaOptions {
28
- /** Max characters of delta to return (default: 6000) */
29
- maxChars?: number;
30
- /** Minimum overlap length required for partial scroll matching (default: 16) */
31
- minOverlap?: number;
32
- /** Length of tail to retain in cursor (default: 500) */
33
- tailChars?: number;
34
- }
35
-
36
- export interface SubagentOutputDeltaResult {
37
- /** The new output text (or full bounded text on initial/restart) */
38
- delta: string;
39
- /** Whether the increment could not be determined reliably (scrolled away, cleared, ambiguous repeat) */
40
- restart: boolean;
41
- /** Whether delta was truncated to maxChars */
42
- truncated: boolean;
43
- /** New cursor to save for next call */
44
- nextCursor: SubagentOutputCursor;
45
- }
46
-
47
- /**
48
- * Extract tail characters from a text buffer for cursor matching.
49
- */
50
- export function extractTail(text: string, tailChars = OUTPUT_TAIL_CHARS): string {
51
- if (text.length <= tailChars) return text;
52
- return text.slice(text.length - tailChars);
53
- }
54
-
55
- /**
56
- * Bound text to a maximum character budget, retaining the tail (most recent output).
57
- */
58
- export function boundText(text: string, maxChars: number): { text: string; truncated: boolean } {
59
- if (text.length <= maxChars) {
60
- return { text, truncated: false };
61
- }
62
- return {
63
- text: text.slice(text.length - maxChars),
64
- truncated: true,
65
- };
66
- }
67
-
68
- /**
69
- * Pure function: Given previous cursor (tail + fullLength) and current full buffer text,
70
- * compute the incremental delta since last read.
71
- *
72
- * Boundary handling:
73
- * 1. Initial read (prevCursor is null/empty): returns full bounded text, restart=false.
74
- * 2. No change: fullLength and tail match, delta is empty, restart=false.
75
- * 3. Normal append: prevTail appears uniquely in currText -> delta is text after prevTail.
76
- * 4. Repeated text: prevTail appears multiple times -> ambiguous, returns full bounded text with restart=true.
77
- * 5. Partial buffer scroll: prevTail partially scrolled off top -> longest suffix of prevTail
78
- * matching prefix of currText (>= minOverlap) identifies boundary.
79
- * 6. Full scroll / clear screen: no valid overlap found -> returns full bounded text with restart=true.
80
- */
81
- export function computeSubagentOutputDelta(
82
- prevCursor: SubagentOutputCursor | null | undefined,
83
- currText: string,
84
- options?: SubagentOutputDeltaOptions,
85
- ): SubagentOutputDeltaResult {
86
- const rawMaxChars = options?.maxChars ?? OUTPUT_DEFAULT_MAX_CHARS;
87
- const maxChars = Math.max(1, Math.min(rawMaxChars, OUTPUT_HARD_CAP_CHARS));
88
- const minOverlap = options?.minOverlap ?? OUTPUT_MIN_OVERLAP;
89
- const tailChars = options?.tailChars ?? OUTPUT_TAIL_CHARS;
90
-
91
- // Case 1: First call (no previous cursor)
92
- if (!prevCursor || !prevCursor.tail) {
93
- const bounded = boundText(currText, maxChars);
94
- return {
95
- delta: bounded.text,
96
- restart: false,
97
- truncated: bounded.truncated,
98
- nextCursor: {
99
- tail: extractTail(currText, tailChars),
100
- fullLength: currText.length,
101
- },
102
- };
103
- }
104
-
105
- const prevTail = prevCursor.tail;
106
-
107
- // Case 2: Exact same content (no new output)
108
- if (currText.length === prevCursor.fullLength && currText.endsWith(prevTail)) {
109
- return {
110
- delta: '',
111
- restart: false,
112
- truncated: false,
113
- nextCursor: prevCursor,
114
- };
115
- }
116
-
117
- // Case 3: prevTail appears in currText
118
- const firstIdx = currText.indexOf(prevTail);
119
- const lastIdx = currText.lastIndexOf(prevTail);
120
-
121
- if (firstIdx !== -1) {
122
- // 3a: Ambiguous duplicate occurrences in current text
123
- if (firstIdx !== lastIdx) {
124
- const bounded = boundText(currText, maxChars);
125
- return {
126
- delta: bounded.text,
127
- restart: true,
128
- truncated: bounded.truncated,
129
- nextCursor: {
130
- tail: extractTail(currText, tailChars),
131
- fullLength: currText.length,
132
- },
133
- };
134
- }
135
-
136
- // 3b: Unique match
137
- const rawDelta = currText.slice(firstIdx + prevTail.length);
138
- if (rawDelta.length === 0) {
139
- return {
140
- delta: '',
141
- restart: false,
142
- truncated: false,
143
- nextCursor: prevCursor,
144
- };
145
- }
146
-
147
- const bounded = boundText(rawDelta, maxChars);
148
- return {
149
- delta: bounded.text,
150
- restart: false,
151
- truncated: bounded.truncated,
152
- nextCursor: {
153
- tail: extractTail(currText, tailChars),
154
- fullLength: currText.length,
155
- },
156
- };
157
- }
158
-
159
- // Case 4: prevTail is not found in full. Check if a suffix of prevTail matches a prefix of currText
160
- // (terminal buffer scrolled lines off the top).
161
- let longestOverlap = 0;
162
- const maxPossibleOverlap = Math.min(prevTail.length - 1, currText.length);
163
- const effectiveMinOverlap = Math.min(minOverlap, prevTail.length);
164
-
165
- for (let k = maxPossibleOverlap; k >= effectiveMinOverlap; k--) {
166
- const suffix = prevTail.slice(prevTail.length - k);
167
- if (currText.startsWith(suffix)) {
168
- longestOverlap = k;
169
- break;
170
- }
171
- }
172
-
173
- if (longestOverlap >= effectiveMinOverlap) {
174
- const rawDelta = currText.slice(longestOverlap);
175
- const bounded = boundText(rawDelta, maxChars);
176
- return {
177
- delta: bounded.text,
178
- restart: false,
179
- truncated: bounded.truncated,
180
- nextCursor: {
181
- tail: extractTail(currText, tailChars),
182
- fullLength: currText.length,
183
- },
184
- };
185
- }
186
-
187
- // Case 5: Clear screen or full scroll out -> degraded reset
188
- const bounded = boundText(currText, maxChars);
189
- return {
190
- delta: bounded.text,
191
- restart: true,
192
- truncated: bounded.truncated,
193
- nextCursor: {
194
- tail: extractTail(currText, tailChars),
195
- fullLength: currText.length,
196
- },
197
- };
198
- }
199
-
200
- /**
201
- * Resolve subagent semantic status from local subs registry and herdr agent state.
202
- */
203
- export function resolveSubagentStatus(opts: {
204
- localStatus: SubEntry['status'];
205
- herdrStatus?: HerdrAgentState | null;
206
- hasAskFlag?: boolean;
207
- }): SubagentStatus {
208
- if (opts.hasAskFlag || opts.herdrStatus === 'blocked') {
209
- return 'blocked';
210
- }
211
- if (opts.herdrStatus === 'working') {
212
- return 'running';
213
- }
214
- if (opts.localStatus === 'settled' || opts.localStatus === 'closed' || opts.herdrStatus === 'done') {
215
- return 'settled';
216
- }
217
- if (opts.herdrStatus === 'idle') {
218
- return 'idle';
219
- }
220
- if (opts.localStatus === 'running') {
221
- return 'running';
222
- }
223
- return 'running';
224
- }
225
-
226
- export interface FormatSubagentOutputOpts {
227
- paneId: string;
228
- status: SubagentStatus;
229
- revision: number;
230
- bufferTruncated: boolean;
231
- deltaResult: SubagentOutputDeltaResult;
232
- askQuestion?: string | null;
233
- }
234
-
235
- /**
236
- * Format subagent output for display to the master model.
237
- */
238
- export function formatSubagentOutput(opts: FormatSubagentOutputOpts): string {
239
- const parts: string[] = [];
240
- const metaItems: string[] = [
241
- `pane: ${opts.paneId}`,
242
- `status: ${opts.status}${opts.status === 'blocked' && opts.askQuestion ? ` (question: "${opts.askQuestion}")` : ''}`,
243
- `revision: ${opts.revision}`,
244
- ];
245
-
246
- if (opts.deltaResult.restart) {
247
- metaItems.push('restart: true (buffer reset or scrolled)');
248
- }
249
- if (opts.bufferTruncated || opts.deltaResult.truncated) {
250
- metaItems.push('truncated: true');
251
- }
252
-
253
- const header = `[Subagent Output | ${metaItems.join(' | ')}]`;
254
- parts.push(header);
255
-
256
- if (opts.deltaResult.delta.length === 0) {
257
- parts.push('(no new output since last read)');
258
- } else {
259
- parts.push(opts.deltaResult.delta);
260
- }
261
-
262
- return parts.join('\n');
263
- }