pi-ui-extend 1.0.41 → 1.0.42

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 (124) hide show
  1. package/README.md +10 -2
  2. package/dist/app/app.d.ts +3 -0
  3. package/dist/app/app.js +26 -0
  4. package/dist/app/commands/command-host.d.ts +1 -0
  5. package/dist/app/commands/command-model-actions.js +6 -1
  6. package/dist/app/commands/command-registry.js +1 -1
  7. package/dist/app/commands/command-session-actions.js +9 -3
  8. package/dist/app/commands/reload-context-inventory.d.ts +11 -0
  9. package/dist/app/commands/reload-context-inventory.js +59 -0
  10. package/dist/app/extensions/extension-actions-controller.d.ts +1 -0
  11. package/dist/app/extensions/extension-actions-controller.js +6 -0
  12. package/dist/app/extensions/subagent-catalog-state.d.ts +9 -0
  13. package/dist/app/extensions/subagent-catalog-state.js +23 -0
  14. package/dist/app/rendering/editor-panels.js +10 -4
  15. package/dist/app/session/session-lifecycle-controller.d.ts +1 -0
  16. package/dist/app/session/session-lifecycle-controller.js +6 -0
  17. package/dist/app/session/tabs-controller.d.ts +1 -0
  18. package/dist/app/session/tabs-controller.js +19 -1
  19. package/dist/app/subagents/subagents-files.js +60 -1
  20. package/dist/app/subagents/subagents-model.d.ts +1 -0
  21. package/dist/app/subagents/subagents-model.js +18 -2
  22. package/dist/app/types.d.ts +5 -0
  23. package/dist/default-pix-config.js +10 -0
  24. package/dist/schemas/index.d.ts +1 -0
  25. package/dist/schemas/index.js +1 -0
  26. package/dist/schemas/pi-tools-suite-schema.d.ts +80 -58
  27. package/dist/schemas/pi-tools-suite-schema.js +43 -78
  28. package/dist/schemas/pix-schema.d.ts +7 -0
  29. package/dist/schemas/pix-schema.js +14 -0
  30. package/dist/schemas/tasks-schema.d.ts +18 -0
  31. package/dist/schemas/tasks-schema.js +41 -0
  32. package/docs/concurrency.md +6 -0
  33. package/docs/desktop-mvp.md +12 -1
  34. package/docs/desktop-task-manager.md +93 -82
  35. package/external/pi-tools-suite/README.md +37 -50
  36. package/external/pi-tools-suite/docs/browser-qa-subagent.md +19 -0
  37. package/external/pi-tools-suite/docs/evals.md +1 -1
  38. package/external/pi-tools-suite/docs/session-recovery.md +40 -14
  39. package/external/pi-tools-suite/docs/subagent-model-pools.md +38 -27
  40. package/external/pi-tools-suite/src/async-subagents/agents/frontier-review.md +23 -0
  41. package/external/pi-tools-suite/src/async-subagents/agents/presets.jsonc +16 -0
  42. package/external/pi-tools-suite/src/async-subagents/agents/research.md +5 -3
  43. package/external/pi-tools-suite/src/async-subagents/commands.ts +14 -70
  44. package/external/pi-tools-suite/src/async-subagents/core/activity.ts +33 -0
  45. package/external/pi-tools-suite/src/async-subagents/core/agent-catalog.ts +5 -4
  46. package/external/pi-tools-suite/src/async-subagents/core/agent-strategy.ts +1 -1
  47. package/external/pi-tools-suite/src/async-subagents/core/agents-dir.ts +8 -3
  48. package/external/pi-tools-suite/src/async-subagents/core/config.ts +122 -312
  49. package/external/pi-tools-suite/src/async-subagents/core/routing.ts +15 -2
  50. package/external/pi-tools-suite/src/async-subagents/core/spawn.ts +6 -2
  51. package/external/pi-tools-suite/src/async-subagents/core/state.ts +40 -0
  52. package/external/pi-tools-suite/src/async-subagents/core/types.ts +7 -0
  53. package/external/pi-tools-suite/src/async-subagents/index.ts +69 -5
  54. package/external/pi-tools-suite/src/async-subagents/lib.ts +7 -8
  55. package/external/pi-tools-suite/src/async-subagents/tools/spawn.ts +8 -4
  56. package/external/pi-tools-suite/src/async-subagents/tools/subagents.ts +3 -2
  57. package/external/pi-tools-suite/src/async-subagents/types.ts +1 -0
  58. package/external/pi-tools-suite/src/config.ts +41 -1
  59. package/external/pi-tools-suite/src/context-gateway/config.ts +11 -2
  60. package/external/pi-tools-suite/src/context-gateway/enforcement.ts +102 -0
  61. package/external/pi-tools-suite/src/context-gateway/index.ts +73 -19
  62. package/external/pi-tools-suite/src/context-gateway/storeless-capabilities.ts +3 -3
  63. package/external/pi-tools-suite/src/context-gateway/telemetry.ts +101 -28
  64. package/external/pi-tools-suite/src/context-gateway/types.ts +9 -3
  65. package/external/pi-tools-suite/src/context-inventory.ts +99 -0
  66. package/external/pi-tools-suite/src/dcp/auto-compress.ts +96 -44
  67. package/external/pi-tools-suite/src/dcp/commands.ts +18 -0
  68. package/external/pi-tools-suite/src/dcp/compress-tool.ts +170 -45
  69. package/external/pi-tools-suite/src/dcp/compression-blocks.ts +6 -58
  70. package/external/pi-tools-suite/src/dcp/compression-preview.ts +9 -0
  71. package/external/pi-tools-suite/src/dcp/config.ts +56 -13
  72. package/external/pi-tools-suite/src/dcp/prompts.ts +83 -192
  73. package/external/pi-tools-suite/src/dcp/protected-continuity.ts +175 -0
  74. package/external/pi-tools-suite/src/dcp/pruner-candidates.ts +1 -1
  75. package/external/pi-tools-suite/src/dcp/pruner-message-ids.ts +6 -10
  76. package/external/pi-tools-suite/src/dcp/pruner-tools.ts +6 -3
  77. package/external/pi-tools-suite/src/default-pi-tools-suite-config.ts +13 -9
  78. package/external/pi-tools-suite/src/index.ts +7 -1
  79. package/external/pi-tools-suite/src/resource-registry/index.ts +2290 -0
  80. package/external/pi-tools-suite/src/shell-command-policy.ts +219 -0
  81. package/external/pi-tools-suite/src/tool-descriptions.ts +3 -4
  82. package/package.json +4 -2
  83. package/schemas/pi-tools-suite.json +427 -280
  84. package/schemas/pix.json +24 -0
  85. package/schemas/tasks.json +132 -0
  86. package/skills/spec-wiki/SKILL.md +55 -0
  87. package/skills/spec-wiki/evals/evals.json +132 -0
  88. package/skills/spec-wiki/evals/files/bootstrap/architecture/rfc-007.md +14 -0
  89. package/skills/spec-wiki/evals/files/bootstrap/docs/how-to-pay.md +4 -0
  90. package/skills/spec-wiki/evals/files/bootstrap/docs/overview.md +9 -0
  91. package/skills/spec-wiki/evals/files/bootstrap/requirements/payment-behavior.md +17 -0
  92. package/skills/spec-wiki/evals/files/bootstrap/src/payments.ts +12 -0
  93. package/skills/spec-wiki/evals/files/bootstrap/tests/payments.test.ts +11 -0
  94. package/skills/spec-wiki/evals/files/focused/src/session.ts +5 -0
  95. package/skills/spec-wiki/evals/files/focused/tests/session.test.ts +17 -0
  96. package/skills/spec-wiki/evals/files/maintenance/low-signal/incoming/capture.txt +2 -0
  97. package/skills/spec-wiki/evals/files/maintenance/low-signal/src/capture.ts +3 -0
  98. package/skills/spec-wiki/evals/files/maintenance/move/docs/old-job-contract.md +18 -0
  99. package/skills/spec-wiki/evals/files/maintenance/move/src/job.ts +5 -0
  100. package/skills/spec-wiki/evals/files/maintenance/new-relation/docs/session-refresh.md +24 -0
  101. package/skills/spec-wiki/evals/files/maintenance/new-relation/incoming/refresh-worker.ts +7 -0
  102. package/skills/spec-wiki/evals/files/maintenance/new-relation/src/session.ts +7 -0
  103. package/skills/spec-wiki/evals/files/maintenance/no-impact/docs/auth-contract.md +17 -0
  104. package/skills/spec-wiki/evals/files/maintenance/no-impact/incoming/telemetry-format.ts +3 -0
  105. package/skills/spec-wiki/evals/files/maintenance/no-impact/src/auth.ts +3 -0
  106. package/skills/spec-wiki/evals/files/query/docs/how-to-run-jobs.md +4 -0
  107. package/skills/spec-wiki/evals/files/query/docs/spec-overview.md +9 -0
  108. package/skills/spec-wiki/evals/files/query/features/jobs/cancellation-contract.md +15 -0
  109. package/skills/spec-wiki/evals/files/query/features/jobs/retry-spec.md +6 -0
  110. package/skills/spec-wiki/evals/maintenance_regression.py +311 -0
  111. package/skills/spec-wiki/evals/retrieval-eval.md +37 -0
  112. package/skills/spec-wiki/evals/retrieval-evals.json +37 -0
  113. package/skills/spec-wiki/evals/trigger-evals.json +94 -0
  114. package/skills/spec-wiki/references/bootstrap.md +100 -0
  115. package/skills/spec-wiki/references/focused-spec.md +117 -0
  116. package/skills/spec-wiki/references/index-schema.md +330 -0
  117. package/skills/spec-wiki/references/maintenance.md +134 -0
  118. package/skills/spec-wiki/references/query.md +74 -0
  119. package/skills/spec-wiki/scripts/spec_wiki.py +2076 -0
  120. package/docs/desktop-markdown-media.md +0 -77
  121. package/external/pi-tools-suite/docs/dcp-emergency-current-turn.md +0 -102
  122. package/external/pi-tools-suite/src/async-subagents/async-subagents.sample.jsonc +0 -54
  123. package/external/pi-tools-suite/src/skill-installer/index.ts +0 -333
  124. package/skills/spec-lite/SKILL.md +0 -140
@@ -1,10 +1,12 @@
1
1
  import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
+ import { activityFromProgressRecord } from "./activity.js";
3
4
  import { hasLaunchedAgentPrompt, isDir } from "./paths.js";
4
5
  import { readStructuredResult } from "./structured-result.js";
5
6
  import type { AgentResult, AgentState, RpcEventRecord, RunState } from "./types.js";
6
7
 
7
8
  const MAX_RPC_EVENT_LINE_CHARS = 1024 * 1024;
9
+ const LAST_ACTIVITY_TAIL_BYTES = 64 * 1024;
8
10
 
9
11
  interface AgentStateReadOptions {
10
12
  includeLineCounts?: boolean;
@@ -67,6 +69,9 @@ export function getAgentState(
67
69
  state.finishedAt = fs.readFileSync(finishedAtFile, "utf-8").trim();
68
70
  }
69
71
 
72
+ const lastActivity = readLastActivity(path.join(agentDir, "progress.jsonl"));
73
+ if (lastActivity) state.lastActivity = lastActivity;
74
+
70
75
  const retryPendingFile = path.join(agentDir, "retry_pending");
71
76
  const stopRequestedFile = path.join(agentDir, "stop_requested");
72
77
  if (fs.existsSync(retryPendingFile) && !fs.existsSync(stopRequestedFile) && state.status !== "running" && state.status !== "done") {
@@ -109,6 +114,41 @@ function readPid(pidFile: string): number | undefined {
109
114
  return isNaN(pid) ? undefined : pid;
110
115
  }
111
116
 
117
+ function readLastActivity(filePath: string): AgentState["lastActivity"] | undefined {
118
+ if (!fs.existsSync(filePath)) return undefined;
119
+ let fd: number | undefined;
120
+ try {
121
+ fd = fs.openSync(filePath, "r");
122
+ const size = fs.fstatSync(fd).size;
123
+ if (size <= 0) return undefined;
124
+ const start = Math.max(0, size - LAST_ACTIVITY_TAIL_BYTES);
125
+ const buffer = Buffer.allocUnsafe(size - start);
126
+ const bytesRead = fs.readSync(fd, buffer, 0, buffer.length, start);
127
+ if (bytesRead <= 0) return undefined;
128
+ const lines = buffer.toString("utf-8", 0, bytesRead).split(/\r?\n/);
129
+ if (start > 0) {
130
+ const previousByte = Buffer.allocUnsafe(1);
131
+ const previousBytesRead = fs.readSync(fd, previousByte, 0, 1, start - 1);
132
+ if (previousBytesRead === 1 && previousByte[0] !== 0x0a) lines.shift();
133
+ }
134
+ for (let index = lines.length - 1; index >= 0; index--) {
135
+ const line = lines[index]?.trim();
136
+ if (!line) continue;
137
+ try {
138
+ const activity = activityFromProgressRecord(JSON.parse(line) as unknown);
139
+ if (activity) return activity;
140
+ } catch {
141
+ // Ignore malformed or partially written tail lines and keep scanning.
142
+ }
143
+ }
144
+ } catch {
145
+ return undefined;
146
+ } finally {
147
+ if (fd !== undefined) fs.closeSync(fd);
148
+ }
149
+ return undefined;
150
+ }
151
+
112
152
  function hasRpcPromptFailure(eventsFile: string): boolean {
113
153
  if (!fs.existsSync(eventsFile)) return false;
114
154
  let fd: number | undefined;
@@ -39,6 +39,13 @@ export interface AgentState {
39
39
  eventLines?: number;
40
40
  /** How many times this agent has been retried (0 = first attempt). */
41
41
  retryCount?: number;
42
+ /** Most recent user-visible activity observed from the sub-agent RPC stream. */
43
+ lastActivity?: AgentActivity;
44
+ }
45
+
46
+ export interface AgentActivity {
47
+ label: string;
48
+ at: string;
42
49
  }
43
50
 
44
51
  /** Retry configuration for failed agents. */
@@ -7,6 +7,7 @@ import {
7
7
  getRunState,
8
8
  getRunRoot,
9
9
  getSubagentRegistryPath,
10
+ filterSubagentConfigForParentModel,
10
11
  isBlindModelRef,
11
12
  loadSubagentConfig,
12
13
  listSubagentSessionRecords,
@@ -14,9 +15,11 @@ import {
14
15
  removeSubagentRunsFromRegistry,
15
16
  stopAgents,
16
17
  type AgentCompletionHandler,
18
+ type RpcEventRecord,
17
19
  type StopSignal,
18
20
  type SubagentSessionRecord,
19
21
  } from "./lib.js";
22
+ import { activityFromRpcEvent } from "./core/activity.js";
20
23
  import { buildUltraworkPrompt, isUltraworkEnvEnabled, registerCommands } from "./commands.js";
21
24
  import { agentStrategyPrompt, appendAgentStrategyPrompt } from "./core/agent-strategy.js";
22
25
  import { buildSubagentCatalogPrompt } from "./core/agent-catalog.js";
@@ -41,6 +44,7 @@ function isTerminalAgentStatus(status: AgentState["status"]): boolean {
41
44
 
42
45
  const SUBAGENTS_LIVE_COUNT_EVENT = "pi-tools-suite:async-subagents:live-count";
43
46
  const SUBAGENTS_LIVE_STATE_EVENT = "pi-tools-suite:async-subagents:live-state";
47
+ export const SUBAGENTS_CATALOG_STATE_EVENT = "pi-tools-suite:async-subagents:catalog";
44
48
  const SESSION_SHUTDOWN_KILL_GRACE_MS = 500;
45
49
  const COMPLETION_WATCH_INTERVAL_MS = 2_000;
46
50
 
@@ -54,6 +58,14 @@ interface ShutdownPlan {
54
58
  runDirsToDelete: string[];
55
59
  }
56
60
 
61
+ interface SubagentCatalogStateEvent {
62
+ version: 1;
63
+ sessionId?: string;
64
+ sessionFile?: string;
65
+ model?: string;
66
+ types: string[];
67
+ }
68
+
57
69
  function createLiveStatePayload(
58
70
  liveAgents: Map<string, Map<string, LiveAgent>>,
59
71
  sessionFile: string | undefined,
@@ -65,7 +77,13 @@ function createLiveStatePayload(
65
77
  if (matchingLiveAgents.length === 0) continue;
66
78
  const agentIds = matchingLiveAgents.map((agent) => agent.agentId);
67
79
  const state = getRunState(runDir, agentIds, { includeLineCounts: false, checkRpcPromptFailure: false });
68
- const activeAgents = state.agents.filter((agent) => !isTerminalAgentStatus(agent.status));
80
+ const liveAgentsById = new Map(matchingLiveAgents.map((agent) => [agent.agentId, agent]));
81
+ const activeAgents = state.agents
82
+ .filter((agent) => !isTerminalAgentStatus(agent.status))
83
+ .map((agent) => {
84
+ const lastActivity = liveAgentsById.get(agent.id)?.lastActivity;
85
+ return lastActivity ? { ...agent, lastActivity } : agent;
86
+ });
69
87
  if (activeAgents.length === 0) continue;
70
88
  count += activeAgents.length;
71
89
  const tasks = matchingLiveAgents.map((agent) => agent.preview).filter((preview): preview is NonNullable<typeof preview> => Boolean(preview));
@@ -84,6 +102,25 @@ function createLiveStatePayload(
84
102
  };
85
103
  }
86
104
 
105
+ function createSubagentCatalogState(ctx: unknown): SubagentCatalogStateEvent {
106
+ const cwd = (ctx as { cwd?: string } | undefined)?.cwd ?? process.cwd();
107
+ const model = modelRefFromContext(ctx);
108
+ const config = safeLoadSubagentConfig(cwd);
109
+ const effective = config ? filterSubagentConfigForParentModel(config, model) : undefined;
110
+ const sessionManager = (ctx as {
111
+ sessionManager?: { getSessionId?: () => string; getSessionFile?: () => string | undefined };
112
+ } | undefined)?.sessionManager;
113
+ const sessionId = typeof sessionManager?.getSessionId === "function" ? sessionManager.getSessionId() : undefined;
114
+ const sessionFile = typeof sessionManager?.getSessionFile === "function" ? sessionManager.getSessionFile() : undefined;
115
+ return {
116
+ version: 1,
117
+ ...(sessionId ? { sessionId } : {}),
118
+ ...(sessionFile ? { sessionFile } : {}),
119
+ ...(model ? { model } : {}),
120
+ types: Object.keys(effective?.types ?? {}).sort(),
121
+ };
122
+ }
123
+
87
124
  function agentMatchesSession(agent: LiveAgent, sessionFile: string | undefined): boolean {
88
125
  if (!sessionFile) return true;
89
126
  return agent.parentSession !== undefined && pathsEqual(sessionFile, agent.parentSession);
@@ -106,6 +143,12 @@ export default function (pi: ExtensionAPI) {
106
143
  let completionWatchTimer: ReturnType<typeof setInterval> | undefined;
107
144
  publishSubagentPresetsStartupSection();
108
145
 
146
+ function publishSubagentCatalogState(ctx: unknown): void {
147
+ const state = createSubagentCatalogState(ctx);
148
+ pi.events?.emit?.(SUBAGENTS_CATALOG_STATE_EVENT, state);
149
+ publishRpcSessionState(ctx as Parameters<typeof publishRpcSessionState>[0], SUBAGENTS_CATALOG_STATE_EVENT, state);
150
+ }
151
+
109
152
  function refreshSubagentOverlay(): void {
110
153
  try {
111
154
  reconcileLiveAgentCompletions();
@@ -165,7 +208,16 @@ export default function (pi: ExtensionAPI) {
165
208
  refreshSubagentOverlay();
166
209
  };
167
210
 
168
- registerSubagentsTool(pi, liveAgents, handleAgentCompletion, refreshSubagentOverlay);
211
+ function handleAgentRpcEvent(runDir: string, agentId: string, event: RpcEventRecord): void {
212
+ const activity = activityFromRpcEvent(event);
213
+ if (!activity) return;
214
+ const liveAgent = liveAgents.get(runDir)?.get(agentId);
215
+ if (!liveAgent) return;
216
+ liveAgent.lastActivity = activity;
217
+ refreshSubagentOverlay();
218
+ }
219
+
220
+ registerSubagentsTool(pi, liveAgents, handleAgentCompletion, refreshSubagentOverlay, handleAgentRpcEvent);
169
221
  registerCommands(pi);
170
222
 
171
223
  pi.on("session_start", async (_event, ctx) => {
@@ -173,6 +225,7 @@ export default function (pi: ExtensionAPI) {
173
225
  sawAutoUltraworkCandidate = false;
174
226
  currentSessionFile = sessionFileFromContext(ctx);
175
227
  currentSessionStateContext = ctx;
228
+ publishSubagentCatalogState(ctx);
176
229
  subagentOverlay.restoreRunningAgents(ctx.cwd, currentSessionFile);
177
230
  refreshSubagentOverlay();
178
231
  } catch (error) {
@@ -180,6 +233,14 @@ export default function (pi: ExtensionAPI) {
180
233
  }
181
234
  });
182
235
 
236
+ pi.on("model_select", (_event, ctx) => {
237
+ try {
238
+ publishSubagentCatalogState(ctx);
239
+ } catch (error) {
240
+ ignoreStaleExtensionContextError(error);
241
+ }
242
+ });
243
+
183
244
  pi.on("tool_execution_end", async (event) => {
184
245
  if (event.toolName !== "subagents" && !event.toolName.startsWith("async_subagents_")) return;
185
246
  refreshSubagentOverlay();
@@ -192,7 +253,10 @@ export default function (pi: ExtensionAPI) {
192
253
  });
193
254
  const visionPrompt = visionCapabilityPrompt(event, ctx);
194
255
  const catalogPrompt = selectedToolsInclude(event, "subagents")
195
- ? subagentCatalogPrompt((ctx as { cwd?: string } | undefined)?.cwd ?? process.cwd())
256
+ ? subagentCatalogPrompt(
257
+ (ctx as { cwd?: string } | undefined)?.cwd ?? process.cwd(),
258
+ modelRefFromContext(ctx),
259
+ )
196
260
  : undefined;
197
261
  if (!strategyPrompt && !visionPrompt && !catalogPrompt) return undefined;
198
262
  let systemPrompt = event.systemPrompt ?? "";
@@ -286,9 +350,9 @@ function safeLoadSubagentConfig(cwd: string) {
286
350
  }
287
351
  }
288
352
 
289
- function subagentCatalogPrompt(cwd: string): string | undefined {
353
+ function subagentCatalogPrompt(cwd: string, parentModelRef?: string): string | undefined {
290
354
  const config = safeLoadSubagentConfig(cwd);
291
- return config ? buildSubagentCatalogPrompt(config) : undefined;
355
+ return config ? buildSubagentCatalogPrompt(config, parentModelRef) : undefined;
292
356
  }
293
357
 
294
358
  function startupSubagentPresetList(cwd = process.cwd()): string {
@@ -1,4 +1,5 @@
1
1
  export type {
2
+ AgentActivity,
2
3
  AgentCompletionHandler,
3
4
  AgentResult,
4
5
  AgentState,
@@ -12,21 +13,19 @@ export type {
12
13
  } from "./core/types.js";
13
14
 
14
15
  export { createRunDir, getRunRoot, hasAgentPrompt, hasLaunchedAgentPrompt, hasQueuedAgentPrompt, resolveRunDir, validateBasename } from "./core/paths.js";
15
- export type { CopySubagentConfigSampleResult, ModelByParentEntry, ResolvedAgentTaskConfig, ResolvedSubagentRoutingConfig, ResolveAgentTaskOptions, SubagentConfig, SubagentPreset, SubagentRoutingConfig, SubagentTypeConfig, SubagentVisionConfig } from "./core/config.js";
16
+ export type { ModelByParentEntry, ResolvedAgentTaskConfig, ResolvedSubagentRoutingConfig, ResolveAgentTaskOptions, SubagentConfig, SubagentPreset, SubagentRoutingConfig, SubagentTypeConfig, SubagentVisionConfig } from "./core/config.js";
16
17
  export {
17
- configFiles,
18
- copySubagentConfigSample,
19
18
  currentModelRef,
20
19
  defaultSubagentType,
21
20
  DEFAULT_MAX_CONCURRENT,
22
21
  DEFAULT_RETRY_CONFIG,
23
22
  DEFAULT_ROUTING_CONFIG,
24
- existingSubagentConfigFiles,
25
- getDefaultSubagentConfigPath,
23
+ filterSubagentConfigForParentModel,
26
24
  getBuiltinSubagentDefinitionsDir,
27
- getSubagentConfigInitTargetPath,
28
- getSubagentConfigSamplePath,
25
+ getBuiltinSubagentPresetsPath,
26
+ getProjectSubagentPresetsPath,
29
27
  isBlindModelRef,
28
+ isSubagentTypeAvailableForParent,
30
29
  loadSubagentConfig,
31
30
  resolveAgentTaskConfig,
32
31
  resolveRetryConfig,
@@ -37,7 +36,7 @@ export {
37
36
  export { routeSubagentTasks, SubagentRoutingError } from "./core/routing.js";
38
37
  export { BROWSER_QA_RUNNER_ENV, getBrowserQaRunnerPath } from "./core/browser-qa.js";
39
38
  export type { RoutedSubagentTasks, SubagentRoutingContext } from "./core/routing.js";
40
- export { agentDefinitionFiles, agentNameFromFilename, parseAgentMarkdown, projectAgentDefinitionFiles, readAgentDefinitionsFromDir, readProjectAgentDefinitions } from "./core/agents-dir.js";
39
+ export { agentDefinitionFiles, agentNameFromFilename, parseAgentMarkdown, projectAgentDefinitionFiles, projectAgentsDir, readAgentDefinitionsFromDir, readProjectAgentDefinitions } from "./core/agents-dir.js";
41
40
  export type { AgentDefinition, ProjectAgentDefinition } from "./core/agents-dir.js";
42
41
  export { buildSubagentCatalogPrompt } from "./core/agent-catalog.js";
43
42
  export type { SubagentPresetSelectionState } from "./core/presets.js";
@@ -7,7 +7,7 @@ import { ASYNC_SUBAGENT_TOOL_DESCRIPTIONS } from "../../tool-descriptions.js";
7
7
  import { SUBAGENT_TYPE_SELECTION_GUIDANCE } from "../core/agent-catalog.js";
8
8
  import { selectAvailableAgentModels } from "../core/model-selection.js";
9
9
  import { SubagentModelSelectionError } from "../core/config.js";
10
- import type { AgentCompletionHandler, AgentTask, ResolvedAgentTaskConfig, Semaphore, SpawnedAgent } from "../lib.js";
10
+ import type { AgentCompletionHandler, AgentTask, ResolvedAgentTaskConfig, RpcEventRecord, Semaphore, SpawnedAgent } from "../lib.js";
11
11
  import {
12
12
  createRunDir,
13
13
  createSemaphore,
@@ -52,6 +52,7 @@ interface LaunchQueuedAgentOptions {
52
52
  onCancelled: (reason: string) => void;
53
53
  onLaunchError: (error: unknown) => void;
54
54
  onUpdate: () => void;
55
+ onRpcEvent?: (event: RpcEventRecord) => void;
55
56
  }
56
57
 
57
58
  const PROJECT_SEMAPHORES = new Map<string, Semaphore>();
@@ -71,7 +72,7 @@ function normalizedLimit(limit: number): number {
71
72
  }
72
73
 
73
74
  async function launchQueuedAgent(options: LaunchQueuedAgentOptions): Promise<void> {
74
- const { resolved, runDir, cwd, parentSession, semaphore, signal, onResult, onComplete, onCancelled, onLaunchError, onUpdate } = options;
75
+ const { resolved, runDir, cwd, parentSession, semaphore, signal, onResult, onComplete, onCancelled, onLaunchError, onUpdate, onRpcEvent } = options;
75
76
  let slotAcquired = false;
76
77
  try {
77
78
  await semaphore.acquire(signal);
@@ -111,13 +112,14 @@ async function launchQueuedAgent(options: LaunchQueuedAgentOptions): Promise<voi
111
112
  extraArgs: resolved.extraArgs,
112
113
  fallbackModels: resolved.fallbackModels,
113
114
  signal,
115
+ onRpcEvent,
114
116
  ...spawnOptions,
115
117
  },
116
118
  );
117
119
  onResult(retryResult.initial);
118
120
  retryResult.done.catch(onLaunchError);
119
121
  } else {
120
- const result = spawnAgent(runDir, resolved.task, cwd, resolved.extraArgs, undefined, completionHandler, spawnOptions);
122
+ const result = spawnAgent(runDir, resolved.task, cwd, resolved.extraArgs, onRpcEvent, completionHandler, spawnOptions);
121
123
  onResult(result);
122
124
  }
123
125
  onUpdate();
@@ -195,6 +197,7 @@ export function registerSpawnTool(
195
197
  liveAgents: Map<string, Map<string, LiveAgent>>,
196
198
  handleAgentCompletion: AgentCompletionHandler,
197
199
  onLiveAgentsChange?: () => void,
200
+ onAgentRpcEvent?: (runDir: string, agentId: string, event: RpcEventRecord) => void,
198
201
  ): void {
199
202
  pi.registerTool({
200
203
  ...ASYNC_SUBAGENT_TOOL_DESCRIPTIONS.spawnAction,
@@ -226,7 +229,7 @@ export function registerSpawnTool(
226
229
  const activePreset = activePresetName ? config.presets?.[activePresetName] : undefined;
227
230
  if (getSessionSubagentPresetOverride() && !activePreset) {
228
231
  return {
229
- content: [{ type: "text", text: `AGENTS_PRESET=${activePresetName} does not match any preset in asyncSubagents config.` }],
232
+ content: [{ type: "text", text: `AGENTS_PRESET=${activePresetName} does not match any available sub-agent preset.` }],
230
233
  details: {},
231
234
  isError: true,
232
235
  };
@@ -342,6 +345,7 @@ export function registerSpawnTool(
342
345
  details: partialDetails,
343
346
  });
344
347
  },
348
+ onRpcEvent: (event) => onAgentRpcEvent?.(runDir, task.id, event),
345
349
  });
346
350
  }
347
351
 
@@ -4,7 +4,7 @@ import { Text } from "@earendil-works/pi-tui";
4
4
  import { asyncSubagentToolDescriptions } from "../../tool-descriptions.js";
5
5
  import { SUBAGENT_TYPE_SELECTION_GUIDANCE } from "../core/agent-catalog.js";
6
6
  import { hasIndexedProjectRoot } from "../../lib/project.js";
7
- import type { AgentCompletionHandler } from "../lib.js";
7
+ import type { AgentCompletionHandler, RpcEventRecord } from "../lib.js";
8
8
  import { DEFAULT_SPAWN_WATCH_SECONDS, INLINE_RENDERING } from "../constants.js";
9
9
  import { renderSubagentSpawnPrompts } from "../render.js";
10
10
  import { emptyToolSlot } from "../ui.js";
@@ -73,10 +73,11 @@ export function registerSubagentsTool(
73
73
  liveAgents: Map<string, Map<string, LiveAgent>>,
74
74
  handleAgentCompletion: AgentCompletionHandler,
75
75
  onLiveAgentsChange?: () => void,
76
+ onAgentRpcEvent?: (runDir: string, agentId: string, event: RpcEventRecord) => void,
76
77
  ): void {
77
78
  const toolDescriptions = asyncSubagentToolDescriptions(hasIndexedProjectRoot());
78
79
  const collector = new ToolCollector();
79
- registerSpawnTool(collector as any, liveAgents, handleAgentCompletion, onLiveAgentsChange);
80
+ registerSpawnTool(collector as any, liveAgents, handleAgentCompletion, onLiveAgentsChange, onAgentRpcEvent);
80
81
  registerStatusTool(collector as any);
81
82
  registerStopTool(collector as any, liveAgents, onLiveAgentsChange);
82
83
  registerWaitTool(collector as any);
@@ -6,6 +6,7 @@ export interface LiveAgent {
6
6
  preview?: AgentTaskPreview;
7
7
  /** Parent pi session that spawned/adopted this sub-agent, when known. */
8
8
  parentSession?: string;
9
+ lastActivity?: AgentState["lastActivity"];
9
10
  completed: Promise<void>;
10
11
  }
11
12
 
@@ -12,6 +12,16 @@ export interface PiToolsSuiteConfig {
12
12
  todoThinkingOverrides: Record<string, TodoThinkingLevel>;
13
13
  /** Vision-capable model used by the coding-discipline lookup tool; unset disables lookup. */
14
14
  lookupModel?: string;
15
+ resourceRegistry: ResourceRegistryConfig;
16
+ }
17
+
18
+ export interface ResourceRegistryConfig {
19
+ /** Git remote containing reusable skills/agents and project-scoped state under projects/. */
20
+ remote?: string;
21
+ /** Branch used for installs, updates, and pushes. */
22
+ branch: string;
23
+ /** Optional explicit project key for project-scoped registry state when Git remote discovery is unavailable or should be overridden. */
24
+ projectKey?: string;
15
25
  }
16
26
 
17
27
  type MutableConfig = {
@@ -20,6 +30,7 @@ type MutableConfig = {
20
30
  todoThinking: boolean;
21
31
  todoThinkingOverrides: Map<string, TodoThinkingLevel>;
22
32
  lookupModel: string | undefined;
33
+ resourceRegistry: ResourceRegistryConfig;
23
34
  };
24
35
 
25
36
  const TODO_THINKING_OVERRIDE_LEVELS = ["off", "minimal", "low", "medium", "high", "xhigh", "max"] as const;
@@ -35,8 +46,9 @@ const ENABLED_LIST_KEYS = ["enabledModules", "enabledExtensions"];
35
46
  const MODULE_MAP_KEYS = ["modules", "extensions"];
36
47
  const DEFAULT_DISABLED_MODULES = new Set<string>(["credential-firewall", "truncation-metadata-normalizer"]);
37
48
  const DEFAULT_TODO_THINKING_OVERRIDES = new Map<string, TodoThinkingLevel>([["zai/glm-5.3", "max"]]);
49
+ const DEFAULT_RESOURCE_REGISTRY_BRANCH = "main";
38
50
 
39
- export function getPiToolsSuiteUserConfigPath(homeDir = homedir()): string {
51
+ export function getPiToolsSuiteUserConfigPath(homeDir = process.env.HOME?.trim() || homedir()): string {
40
52
  return join(homeDir, ".config", "pi", "pi-tools-suite.jsonc");
41
53
  }
42
54
 
@@ -68,6 +80,24 @@ function normalizeLookupModel(raw: unknown): string | undefined {
68
80
  return trimmed ? trimmed : undefined;
69
81
  }
70
82
 
83
+ function normalizeNonEmptyString(raw: unknown): string | undefined {
84
+ if (typeof raw !== "string") return undefined;
85
+ const trimmed = raw.trim();
86
+ return trimmed ? trimmed : undefined;
87
+ }
88
+
89
+ function mergeResourceRegistry(config: MutableConfig, raw: unknown): void {
90
+ if (!isRecord(raw)) return;
91
+ if (Object.prototype.hasOwnProperty.call(raw, "remote")) {
92
+ config.resourceRegistry.remote = normalizeNonEmptyString(raw.remote);
93
+ }
94
+ const branch = normalizeNonEmptyString(raw.branch);
95
+ if (branch) config.resourceRegistry.branch = branch;
96
+ if (Object.prototype.hasOwnProperty.call(raw, "projectKey")) {
97
+ config.resourceRegistry.projectKey = normalizeNonEmptyString(raw.projectKey);
98
+ }
99
+ }
100
+
71
101
  function isTodoThinkingLevel(raw: unknown): raw is TodoThinkingLevel {
72
102
  return TODO_THINKING_OVERRIDE_LEVELS.includes(raw as TodoThinkingLevel);
73
103
  }
@@ -145,6 +175,7 @@ function mergeConfigLayer(config: MutableConfig, raw: Record<string, unknown>, k
145
175
  if (typeof raw.todoThinking === "boolean") config.todoThinking = raw.todoThinking;
146
176
  mergeTodoThinkingOverrides(config, raw.todoThinkingOverrides);
147
177
  if (Object.prototype.hasOwnProperty.call(raw, "lookupModel")) config.lookupModel = normalizeLookupModel(raw.lookupModel);
178
+ mergeResourceRegistry(config, raw.resourceRegistry);
148
179
 
149
180
  for (const key of DISABLED_LIST_KEYS) addDisabled(config, raw[key], knownModules);
150
181
  for (const key of ENABLED_LIST_KEYS) removeDisabled(config, raw[key], knownModules);
@@ -191,6 +222,13 @@ function applyEnv(config: MutableConfig, env: Env, knownModules: ReadonlySet<str
191
222
  const todoThinking = boolFromEnv(env.PI_TOOLS_SUITE_TODO_THINKING);
192
223
  if (todoThinking !== undefined) config.todoThinking = todoThinking;
193
224
 
225
+ const registryRemote = normalizeNonEmptyString(env.PI_RESOURCE_REGISTRY_REMOTE);
226
+ if (registryRemote) config.resourceRegistry.remote = registryRemote;
227
+ const registryBranch = normalizeNonEmptyString(env.PI_RESOURCE_REGISTRY_BRANCH);
228
+ if (registryBranch) config.resourceRegistry.branch = registryBranch;
229
+ const registryProjectKey = normalizeNonEmptyString(env.PI_RESOURCE_REGISTRY_PROJECT_KEY);
230
+ if (registryProjectKey) config.resourceRegistry.projectKey = registryProjectKey;
231
+
194
232
  return config;
195
233
  }
196
234
 
@@ -203,6 +241,7 @@ export function loadPiToolsSuiteConfig(moduleNames: readonly string[], options:
203
241
  todoThinking: false,
204
242
  todoThinkingOverrides: new Map(DEFAULT_TODO_THINKING_OVERRIDES),
205
243
  lookupModel: undefined,
244
+ resourceRegistry: { branch: DEFAULT_RESOURCE_REGISTRY_BRANCH },
206
245
  };
207
246
  const userConfigPath = getPiToolsSuiteUserConfigPath(options.homeDir);
208
247
 
@@ -223,5 +262,6 @@ export function loadPiToolsSuiteConfig(moduleNames: readonly string[], options:
223
262
  todoThinking: config.todoThinking,
224
263
  todoThinkingOverrides: Object.fromEntries(config.todoThinkingOverrides),
225
264
  ...(config.lookupModel ? { lookupModel: config.lookupModel } : {}),
265
+ resourceRegistry: { ...config.resourceRegistry },
226
266
  };
227
267
  }
@@ -7,6 +7,7 @@ import type {
7
7
  ContextGatewayBudgets,
8
8
  ContextGatewayMode,
9
9
  ContextGatewayResolvedConfig,
10
+ ContextGatewayToolClass,
10
11
  } from "./types.js";
11
12
 
12
13
  export const DEFAULT_CONTEXT_GATEWAY_BUDGETS: Readonly<ContextGatewayBudgets> = Object.freeze({
@@ -17,8 +18,16 @@ export const DEFAULT_CONTEXT_GATEWAY_BUDGETS: Readonly<ContextGatewayBudgets> =
17
18
  maxSearchMatches: 12,
18
19
  });
19
20
 
20
- export const CONTEXT_GATEWAY_ENFORCE_UNAVAILABLE =
21
- "Context Gateway enforce is unavailable in P01; mode unchanged until capture/store/result-stage integration is implemented.";
21
+ export function contextGatewayBudgetForClass(
22
+ toolClass: ContextGatewayToolClass,
23
+ budgets: ContextGatewayBudgets,
24
+ ): number {
25
+ if (toolClass === "code-read") return budgets.maxExactReadBytes;
26
+ if (["repo-search", "repo-ast", "repo-structure", "ast-grep"].includes(toolClass)) {
27
+ return budgets.maxSearchBytes;
28
+ }
29
+ return budgets.maxResultBytes;
30
+ }
22
31
 
23
32
  const MODES = new Set<ContextGatewayMode>(["off", "observe", "enforce"]);
24
33
  const MAX_BYTE_BUDGET = 64 * 1024 * 1024;
@@ -0,0 +1,102 @@
1
+ import type { ShellCommandClassification } from "../shell-command-policy.js";
2
+ import { parseTestBuildOutput, planProspectiveTestOutputDelivery } from "./test-output-parser.js";
3
+ import type { ContextGatewayToolClass } from "./types.js";
4
+
5
+ type ResultContent = unknown;
6
+
7
+ export interface ContextGatewayEnforcementPlan {
8
+ representation: "passthrough" | "test-build-compact";
9
+ content: ResultContent;
10
+ sourceContentBytes: number;
11
+ contentBytes: number;
12
+ textBytes: number;
13
+ reason: string;
14
+ }
15
+
16
+ function byteLengthJson(value: unknown): number {
17
+ if (value === undefined) return 0;
18
+ try {
19
+ return new TextEncoder().encode(JSON.stringify(value)).byteLength;
20
+ } catch {
21
+ return 0;
22
+ }
23
+ }
24
+
25
+ function contentText(content: unknown): string {
26
+ if (typeof content === "string") return content;
27
+ if (!Array.isArray(content)) return "";
28
+ return content
29
+ .filter((part) => part && typeof part === "object" && !Array.isArray(part)
30
+ && (part as Record<string, unknown>).type === "text"
31
+ && typeof (part as Record<string, unknown>).text === "string")
32
+ .map((part) => (part as Record<string, unknown>).text as string)
33
+ .join("\n");
34
+ }
35
+
36
+ function textOnlyContent(content: unknown): boolean {
37
+ return Array.isArray(content)
38
+ && content.every((part) => part && typeof part === "object" && !Array.isArray(part)
39
+ && (part as Record<string, unknown>).type === "text"
40
+ && typeof (part as Record<string, unknown>).text === "string");
41
+ }
42
+
43
+ function upstreamTruncated(details: unknown): boolean {
44
+ if (!details || typeof details !== "object" || Array.isArray(details)) return false;
45
+ const record = details as Record<string, unknown>;
46
+ if (record.truncated === true) return true;
47
+ const truncation = record.truncation;
48
+ return !!truncation && typeof truncation === "object" && !Array.isArray(truncation)
49
+ && (truncation as Record<string, unknown>).truncated === true;
50
+ }
51
+
52
+ /**
53
+ * Selective storeless enforcement. It never performs generic truncation.
54
+ * Only recognised, complete, simple test/build output is compacted; every
55
+ * other result stays byte-for-byte passthrough until a recovery contract exists.
56
+ */
57
+ export function planContextGatewayEnforcement(input: {
58
+ event: { content?: unknown; details?: unknown; isError?: boolean };
59
+ toolClass: ContextGatewayToolClass;
60
+ shell: ShellCommandClassification;
61
+ budgetBytes: number;
62
+ maxInlineBytes: number;
63
+ }): ContextGatewayEnforcementPlan {
64
+ const sourceText = contentText(input.event.content);
65
+ const sourceContentBytes = byteLengthJson(input.event.content);
66
+ const passthrough = (reason: string): ContextGatewayEnforcementPlan => ({
67
+ representation: "passthrough",
68
+ content: input.event.content,
69
+ sourceContentBytes,
70
+ contentBytes: sourceContentBytes,
71
+ textBytes: new TextEncoder().encode(sourceText).byteLength,
72
+ reason,
73
+ });
74
+
75
+ if (input.toolClass !== "shell") return passthrough("class-not-enforced");
76
+ if (input.shell.scope !== "simple" || input.shell.kind !== "test-build") {
77
+ return passthrough("shell-not-proven-test-build");
78
+ }
79
+ if (!textOnlyContent(input.event.content)) return passthrough("non-text-content");
80
+ if (sourceContentBytes <= input.budgetBytes) return passthrough("within-class-budget");
81
+
82
+ const parsed = parseTestBuildOutput({
83
+ text: sourceText,
84
+ hostOutcome: input.event.isError === true ? "error" : "success",
85
+ upstreamTruncated: upstreamTruncated(input.event.details),
86
+ });
87
+ const compact = planProspectiveTestOutputDelivery(parsed, input.maxInlineBytes, {
88
+ commandScope: input.shell.scope,
89
+ });
90
+ if (compact.decision !== "compact-candidate" || !compact.text) {
91
+ return passthrough(compact.reason);
92
+ }
93
+ const content = [{ type: "text" as const, text: compact.text }];
94
+ return {
95
+ representation: "test-build-compact",
96
+ content,
97
+ sourceContentBytes,
98
+ contentBytes: byteLengthJson(content),
99
+ textBytes: new TextEncoder().encode(compact.text).byteLength,
100
+ reason: compact.reason,
101
+ };
102
+ }