@xingular/harness 0.1.7 → 2.0.0

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 (130) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/README.md +29 -10
  3. package/dist/cli.js +5179 -2976
  4. package/dist/terminal-pi.js +365 -24
  5. package/docs/README.md +3 -0
  6. package/docs/decisions/ADR-0002-native-subagent-lifecycle.md +49 -0
  7. package/docs/decisions/ADR-0003-shared-asynchronous-subagents.md +68 -0
  8. package/docs/decisions/ADR-0004-fullscreen-terminal-layout.md +49 -0
  9. package/docs/decisions/ADR-0005-app-provided-oauth-clients.md +54 -0
  10. package/docs/terminal-tui.md +100 -10
  11. package/package.json +5 -4
  12. package/src/accounts/accounts.test.ts +87 -1
  13. package/src/accounts/accounts.ts +120 -18
  14. package/src/core/bus.test.ts +22 -0
  15. package/src/core/bus.ts +17 -2
  16. package/src/harness.ts +12 -2
  17. package/src/index.ts +7 -2
  18. package/src/mcp/connections.ts +39 -12
  19. package/src/mcp/service.test.ts +49 -3
  20. package/src/mcp/service.ts +14 -3
  21. package/src/mcp/types.ts +10 -0
  22. package/src/memory/curation.test.ts +44 -1
  23. package/src/memory/dispatch.ts +5 -0
  24. package/src/memory/maintenance.test.ts +17 -1
  25. package/src/memory/maintenance.ts +169 -8
  26. package/src/memory/recall.ts +2 -1
  27. package/src/memory/trajectory.test.ts +178 -0
  28. package/src/memory/trajectory.ts +220 -0
  29. package/src/memory/wiki.ts +2 -1
  30. package/src/memory/wikiIndex.ts +3 -2
  31. package/src/plugins/builtin.ts +4 -2
  32. package/src/plugins/catalogue.test.ts +1 -1
  33. package/src/server/methods.ts +6 -0
  34. package/src/server/server.test.ts +20 -1
  35. package/src/sessions/approvals.test.ts +49 -0
  36. package/src/sessions/approvals.ts +26 -4
  37. package/src/sessions/conversation.test.ts +56 -0
  38. package/src/sessions/lineageJoin.ts +1 -1
  39. package/src/sessions/registry.ts +38 -7
  40. package/src/sessions/runStore.ts +14 -0
  41. package/src/sessions/runner.ts +9 -3
  42. package/src/sessions/sessions.test.ts +46 -7
  43. package/src/sessions/sessions.ts +31 -39
  44. package/src/sessions/subagentService.test.ts +159 -0
  45. package/src/sessions/subagentService.ts +236 -0
  46. package/src/sessions/subagents.ts +97 -0
  47. package/src/terminal/controller.ts +73 -10
  48. package/src/terminal/piExtension.ts +42 -6
  49. package/src/terminal/socket.test.ts +23 -0
  50. package/src/terminal/socket.ts +42 -18
  51. package/src/terminal/standalone.ts +12 -0
  52. package/src/terminal/terminal.test.ts +150 -15
  53. package/src/terminal/terminals.ts +113 -5
  54. package/src/terminal/trajectory.test.ts +66 -0
  55. package/src/terminal/trajectory.ts +44 -0
  56. package/src/terminal/tui/application.test.ts +184 -8
  57. package/src/terminal/tui/application.ts +243 -81
  58. package/src/terminal/tui/commands.test.ts +69 -9
  59. package/src/terminal/tui/commands.ts +89 -7
  60. package/src/terminal/tui/editor.test.ts +56 -27
  61. package/src/terminal/tui/editor.ts +49 -19
  62. package/src/terminal/tui/keybindings.test.ts +8 -0
  63. package/src/terminal/tui/keybindings.ts +10 -1
  64. package/src/terminal/tui/planMode.test.ts +10 -0
  65. package/src/terminal/tui/planMode.ts +12 -0
  66. package/src/terminal/tui/runtime.socket.test.ts +77 -3
  67. package/src/terminal/tui/runtime.test.ts +10 -0
  68. package/src/terminal/tui/runtime.ts +24 -9
  69. package/src/terminal/tui/screen.test.ts +143 -0
  70. package/src/terminal/tui/screen.ts +121 -0
  71. package/src/terminal/tui/skillInvocation.test.ts +71 -0
  72. package/src/terminal/tui/skillInvocation.ts +51 -0
  73. package/src/terminal/tui/status.test.ts +24 -46
  74. package/src/terminal/tui/status.ts +42 -69
  75. package/src/terminal/tui/theme.ts +18 -7
  76. package/src/terminal/tui/trajectoryPanel.test.ts +99 -0
  77. package/src/terminal/tui/trajectoryPanel.ts +226 -0
  78. package/src/terminal/tui/transcript.test.ts +14 -0
  79. package/src/terminal/tui/transcript.ts +11 -0
  80. package/src/terminal/tui/ui.ts +2 -2
  81. package/src/terminal/tui/welcome.ts +4 -4
  82. package/src/terminal/tui/workingStatus.test.ts +44 -0
  83. package/src/terminal/tui/workingStatus.ts +238 -0
  84. package/src/tools/definitions.ts +42 -1
  85. package/src/tools/maintenance.ts +6 -1
  86. package/src/tools/spawnSubagent.ts +40 -85
  87. package/src/tools/types.ts +2 -0
  88. package/docs/evidence/terminal-2026-10-17/120x40-completed-transcript.ansi.gz +0 -0
  89. package/docs/evidence/terminal-2026-10-17/40x20-settings-interaction.ansi.gz +0 -0
  90. package/docs/evidence/terminal-2026-10-17/40x20-settings-keyboard.ansi.gz +0 -0
  91. package/docs/evidence/terminal-2026-10-17/40x20-welcome.ansi.gz +0 -0
  92. package/docs/evidence/terminal-2026-10-17/80x24-approval-0.ansi.gz +0 -0
  93. package/docs/evidence/terminal-2026-10-17/80x24-question.ansi.gz +0 -0
  94. package/docs/evidence/terminal-2026-10-17/verification.json +0 -130
  95. package/docs/evidence/terminal-2026-10-17-followup/120x40-completed-transcript.ansi.gz +0 -0
  96. package/docs/evidence/terminal-2026-10-17-followup/40x20-settings-interaction.ansi.gz +0 -0
  97. package/docs/evidence/terminal-2026-10-17-followup/40x20-settings-keyboard.ansi.gz +0 -0
  98. package/docs/evidence/terminal-2026-10-17-followup/40x20-welcome.ansi.gz +0 -0
  99. package/docs/evidence/terminal-2026-10-17-followup/80x24-approval-0.ansi.gz +0 -0
  100. package/docs/evidence/terminal-2026-10-17-followup/80x24-plugin-light.ansi.gz +0 -0
  101. package/docs/evidence/terminal-2026-10-17-followup/80x24-question-batch.ansi.gz +0 -0
  102. package/docs/evidence/terminal-2026-10-17-followup/80x24-question.ansi.gz +0 -0
  103. package/docs/evidence/terminal-2026-10-17-followup/app-bundle/80x24-approval-0.ansi.gz +0 -0
  104. package/docs/evidence/terminal-2026-10-17-followup/app-bundle/80x24-plugin-light.ansi.gz +0 -0
  105. package/docs/evidence/terminal-2026-10-17-followup/app-bundle/80x24-question-batch.ansi.gz +0 -0
  106. package/docs/evidence/terminal-2026-10-17-followup/app-bundle/80x24-question.ansi.gz +0 -0
  107. package/docs/evidence/terminal-2026-10-17-followup/app-bundle/verification.json +0 -95
  108. package/docs/evidence/terminal-2026-10-17-followup/global-install/80x24-approval-0.ansi.gz +0 -0
  109. package/docs/evidence/terminal-2026-10-17-followup/global-install/80x24-plugin-light.ansi.gz +0 -0
  110. package/docs/evidence/terminal-2026-10-17-followup/global-install/80x24-question-batch.ansi.gz +0 -0
  111. package/docs/evidence/terminal-2026-10-17-followup/global-install/80x24-question.ansi.gz +0 -0
  112. package/docs/evidence/terminal-2026-10-17-followup/global-install/README.md +0 -27
  113. package/docs/evidence/terminal-2026-10-17-followup/global-install/shell-command-80x24.ansi.gz +0 -0
  114. package/docs/evidence/terminal-2026-10-17-followup/global-install/shell-command.json +0 -18
  115. package/docs/evidence/terminal-2026-10-17-followup/global-install/verification.json +0 -95
  116. package/docs/evidence/terminal-2026-10-17-followup/harness-reference-80x24.json.gz +0 -0
  117. package/docs/evidence/terminal-2026-10-17-followup/harness-reference-final-80x24.json.gz +0 -0
  118. package/docs/evidence/terminal-2026-10-17-followup/harness-reference-scenario.json +0 -60
  119. package/docs/evidence/terminal-2026-10-17-followup/live-codex/README.md +0 -24
  120. package/docs/evidence/terminal-2026-10-17-followup/live-codex/complete-80x24.ansi.gz +0 -0
  121. package/docs/evidence/terminal-2026-10-17-followup/live-codex/verification.json +0 -57
  122. package/docs/evidence/terminal-2026-10-17-followup/omp-reference-80x24-early.json.gz +0 -0
  123. package/docs/evidence/terminal-2026-10-17-followup/omp-reference-80x24.json.gz +0 -0
  124. package/docs/evidence/terminal-2026-10-17-followup/omp-reference-scenario.json +0 -58
  125. package/docs/evidence/terminal-2026-10-17-followup/omp-supplemental-static.json.gz +0 -0
  126. package/docs/evidence/terminal-2026-10-17-followup/rea-artifacts.json +0 -320
  127. package/docs/evidence/terminal-2026-10-17-followup/reference-comparison.json +0 -30
  128. package/docs/evidence/terminal-2026-10-17-followup/reference-comparison.md +0 -133
  129. package/docs/evidence/terminal-2026-10-17-followup/reference-source-observations.json +0 -20
  130. package/docs/evidence/terminal-2026-10-17-followup/verification.json +0 -207
@@ -10,6 +10,7 @@ var SocketError = class extends Error {
10
10
  super(message2);
11
11
  this.code = code;
12
12
  }
13
+ code;
13
14
  };
14
15
  function connectionFromEnv(env = process.env) {
15
16
  const socket = env[SOCKET_ENV];
@@ -105,8 +106,8 @@ async function attempt(ui, action) {
105
106
  return false;
106
107
  }
107
108
  }
108
- function resetIn(resetsAt, now = Date.now()) {
109
- const minutes = Math.round((Date.parse(resetsAt) - now) / 6e4);
109
+ function resetIn(resetsAt, now2 = Date.now()) {
110
+ const minutes = Math.round((Date.parse(resetsAt) - now2) / 6e4);
110
111
  if (Number.isNaN(minutes)) return resetsAt;
111
112
  if (minutes <= 0) return "now";
112
113
  if (minutes < 60) return `in ${minutes}m`;
@@ -123,7 +124,7 @@ function accountRows(overview, session) {
123
124
  var limitLine = (limit) => `${limit.label}: ${limit.usedPercent ?? "?"}% used${limit.resetsAt ? `, resets ${resetIn(limit.resetsAt)}` : ""}`;
124
125
  function accountDetails(account) {
125
126
  const state = account.state;
126
- const lines = [
127
+ const lines2 = [
127
128
  account.label,
128
129
  `Health: ${HEALTH[state.health] ?? HEALTH.unknown}`,
129
130
  `Default for new sessions: ${account.isDefault ? "yes" : "no"}`,
@@ -137,7 +138,7 @@ function accountDetails(account) {
137
138
  state.limits.length ? null : "Usage limits: Pi providers do not report them.",
138
139
  state.capabilities.length ? `Detected: ${state.capabilities.join(", ")}` : null
139
140
  ];
140
- return lines.filter((line) => line !== null).join("\n");
141
+ return lines2.filter((line) => line !== null).join("\n");
141
142
  }
142
143
  function loginStep(step) {
143
144
  if (step.type === "auth_url" && typeof step.url === "string") return `Open this link to sign in:
@@ -233,9 +234,9 @@ async function accountMenu(ui, call, session, accountId) {
233
234
  }
234
235
  async function showActivity(ui, call, accountId, label) {
235
236
  const entries = await call("accounts.audit", { accountId, limit: 30 });
236
- const lines = entries.map((entry) => `${new Date(entry.at).toLocaleString()} \xB7 ${entry.action} \xB7 ${entry.detail}`);
237
+ const lines2 = entries.map((entry) => `${new Date(entry.at).toLocaleString()} \xB7 ${entry.action} \xB7 ${entry.detail}`);
237
238
  await ui.select(`${label}: activity
238
- ${lines.join("\n") || "Nothing has happened here yet."}`, [BACK]);
239
+ ${lines2.join("\n") || "Nothing has happened here yet."}`, [BACK]);
239
240
  }
240
241
  async function signIn(ui, call, account) {
241
242
  const providers = await call("accounts.providers");
@@ -284,7 +285,7 @@ async function followLogin(ui, call, accountId, loginId) {
284
285
  await answerPrompt(ui, call, loginId, snapshot.prompt);
285
286
  continue;
286
287
  }
287
- const step = [...snapshot.events].reverse().map(loginStep).find((text2) => text2 !== null);
288
+ const step = [...snapshot.events].reverse().map(loginStep).find((text3) => text3 !== null);
288
289
  if (step && snapshot.revision !== shownRevision) {
289
290
  shownRevision = snapshot.revision;
290
291
  const choice = await ui.select(`${step}
@@ -313,18 +314,18 @@ function reportLogin(ui, snapshot) {
313
314
  async function answerPrompt(ui, call, loginId, prompt) {
314
315
  const promptId = String(prompt.promptId ?? "");
315
316
  const detail = prompt.prompt ?? {};
316
- const text2 = detail.message ?? "Sign-in needs an answer.";
317
+ const text3 = detail.message ?? "Sign-in needs an answer.";
317
318
  let value;
318
319
  if (detail.type === "select" && Array.isArray(detail.options)) {
319
320
  if (detail.options.every((option) => typeof option === "string")) {
320
- value = await ui.select(text2, detail.options);
321
+ value = await ui.select(text3, detail.options);
321
322
  } else if (detail.options.every((option) => typeof option === "object" && option !== null && "id" in option && typeof option.id === "string" && "label" in option && typeof option.label === "string")) {
322
323
  const options = detail.options;
323
- const selected = await ui.select(text2, options.map((option) => option.label));
324
+ const selected = await ui.select(text3, options.map((option) => option.label));
324
325
  value = options.find((option) => option.label === selected)?.id;
325
326
  }
326
327
  } else {
327
- value = await ui.input(text2, detail.placeholder ?? void 0);
328
+ value = await ui.input(text3, detail.placeholder ?? void 0);
328
329
  }
329
330
  await call("accounts.loginRespond", { loginId, promptId, value: value ?? null });
330
331
  }
@@ -497,11 +498,303 @@ function claimTerminalControl(connection = connectionFromEnv()) {
497
498
  }
498
499
  return claim;
499
500
  }
501
+ async function controlCall(call, params = {}, timeoutMs = 12e4) {
502
+ const connection = connectionFromEnv();
503
+ const control = await claimTerminalControl(connection);
504
+ return socketCall(connection, call, params, timeoutMs, control);
505
+ }
506
+
507
+ // src/sessions/contracts.ts
508
+ import { z } from "zod";
509
+ var runStatusSchema = z.enum(["queued", "running", "waiting_for_approval", "waiting_for_input", "interrupted", "completed", "failed"]);
510
+ var runEventTypeSchema = z.enum([
511
+ "started",
512
+ "context.snapshot",
513
+ "message.delta",
514
+ "message.completed",
515
+ "message.steered",
516
+ "tool.started",
517
+ "tool.completed",
518
+ "retry.started",
519
+ "retry.ended",
520
+ "compaction.started",
521
+ "compaction.ended",
522
+ "approval.requested",
523
+ "approval.resolved",
524
+ "question.requested",
525
+ "question.answered",
526
+ "extension.notice",
527
+ "reaction.added",
528
+ "agent.message",
529
+ "artifact.added",
530
+ "file.changed",
531
+ "usage.updated",
532
+ "account.status",
533
+ "status.changed",
534
+ "failed",
535
+ "completed"
536
+ ]);
537
+ var messageDeltaPayload = z.object({ messageId: z.string(), text: z.string() });
538
+ var messageCompletedPayload = z.object({ messageId: z.string(), role: z.enum(["user", "assistant"]), text: z.string() });
539
+ var messageSteeredPayload = z.object({ text: z.string() });
540
+ var contextSnapshotPayload = z.object({
541
+ systemPrompt: z.string().max(1e5),
542
+ contextUpdate: z.string().max(1e5).optional(),
543
+ tools: z.array(z.object({ name: z.string(), description: z.string().max(2e3), parameters: z.unknown() })).max(120),
544
+ systemPromptTruncated: z.boolean().optional(),
545
+ contextUpdateTruncated: z.boolean().optional(),
546
+ toolsTruncated: z.boolean().optional()
547
+ });
548
+ var toolAttachmentRefSchema = z.object({
549
+ id: z.string().min(1).max(300),
550
+ name: z.string().min(1).max(500),
551
+ mimeType: z.string().min(1).max(100)
552
+ }).strict();
553
+ var toolAttachmentRefsSchema = z.array(toolAttachmentRefSchema).max(16);
554
+ var toolPayload = z.object({
555
+ toolCallId: z.string(),
556
+ name: z.string(),
557
+ summary: z.string().max(200).optional(),
558
+ input: z.string().max(24e3).optional(),
559
+ inputTruncated: z.boolean().optional(),
560
+ output: z.string().max(24e3).optional(),
561
+ outputTruncated: z.boolean().optional(),
562
+ ok: z.boolean().optional(),
563
+ /** On `tool.completed`: how long the call ran, and the size of what it returned to the model. */
564
+ durationMs: z.number().optional(),
565
+ resultChars: z.number().optional(),
566
+ /** Opaque attachment references. Content stays outside the run event stream. */
567
+ attachments: toolAttachmentRefsSchema.optional()
568
+ });
569
+ var retryStartedPayload = z.object({ attempt: z.number(), maxAttempts: z.number(), delayMs: z.number(), error: z.string().max(1e3).optional() });
570
+ var retryEndedPayload = z.object({ attempt: z.number(), success: z.boolean(), error: z.string().max(1e3).optional() });
571
+ var compactionStartedPayload = z.object({ reason: z.string() });
572
+ var compactionEndedPayload = z.object({
573
+ reason: z.string(),
574
+ aborted: z.boolean(),
575
+ willRetry: z.boolean().optional(),
576
+ error: z.string().max(1e3).optional(),
577
+ tokensBefore: z.number().optional(),
578
+ tokensAfter: z.number().optional(),
579
+ summary: z.string().max(24e3).optional(),
580
+ summaryTruncated: z.boolean().optional()
581
+ });
582
+ var approvalAutomaticSchema = z.enum(["always", "remembered", "scope"]);
583
+ var approvalRequestedPayload = z.object({
584
+ approvalId: z.string(),
585
+ target: z.string(),
586
+ action: z.string(),
587
+ screenshotRef: z.string().optional(),
588
+ operation: z.object({ tool: z.string(), args: z.unknown() }).optional(),
589
+ /** A tool the user may choose to always allow for this session. */
590
+ rememberable: z.boolean().optional(),
591
+ /** Set when the request was granted as it was made, so nobody is waiting on it. */
592
+ automatic: approvalAutomaticSchema.optional()
593
+ });
594
+ var approvalResolvedPayload = z.object({ approvalId: z.string(), decision: z.enum(["approved", "denied", "native"]), automatic: approvalAutomaticSchema.optional() });
595
+ var questionOptionSchema = z.object({ label: z.string().min(1), description: z.string().optional() });
596
+ var questionBatchFields = {
597
+ batchId: z.string().min(1).optional(),
598
+ batchIndex: z.number().int().min(1).max(5).optional(),
599
+ batchCount: z.number().int().min(1).max(5).optional()
600
+ };
601
+ var questionBatchRefinement = (value, context) => {
602
+ const fields = [value.batchId, value.batchIndex, value.batchCount];
603
+ if (fields.some((field) => field !== void 0) && fields.some((field) => field === void 0)) {
604
+ context.addIssue({ code: z.ZodIssueCode.custom, message: "Question batch fields must be supplied together." });
605
+ }
606
+ if (value.batchIndex !== void 0 && value.batchCount !== void 0 && value.batchIndex > value.batchCount) {
607
+ context.addIssue({ code: z.ZodIssueCode.custom, message: "Question batch index cannot exceed its count." });
608
+ }
609
+ };
610
+ var questionRequestedPayload = z.object({
611
+ questionId: z.string(),
612
+ question: z.string().min(1),
613
+ summary: z.string().min(1).max(100).optional(),
614
+ options: z.array(questionOptionSchema),
615
+ multiSelect: z.boolean(),
616
+ allowText: z.boolean(),
617
+ userOnly: z.boolean().optional(),
618
+ ...questionBatchFields
619
+ }).superRefine(questionBatchRefinement);
620
+ var questionAnsweredPayload = z.object({
621
+ questionId: z.string(),
622
+ selected: z.array(z.string()),
623
+ text: z.string().optional(),
624
+ notes: z.record(z.string(), z.string().max(1e4)).optional(),
625
+ ...questionBatchFields
626
+ }).superRefine(questionBatchRefinement);
627
+ var reactionAddedPayload = z.object({ emoji: z.string().min(1).refine((text3) => Buffer.byteLength(text3) <= 16 && !/[A-Za-z0-9\s]/.test(text3)) });
628
+ var agentMessagePayload = z.object({ direction: z.enum(["to", "from"]), agentId: z.string(), agentName: z.string(), text: z.string() });
629
+ var extensionNoticePayload = z.object({
630
+ kind: z.enum(["notify", "status", "widget", "title", "error"]),
631
+ level: z.enum(["info", "warning", "error"]).optional(),
632
+ key: z.string().optional(),
633
+ text: z.string(),
634
+ extension: z.string().optional()
635
+ });
636
+ var artifactPayload = z.object({ artifactId: z.string(), name: z.string(), file: z.unknown().optional() });
637
+ var fileChangedPayload = z.object({ path: z.string(), change: z.enum(["added", "modified", "deleted"]) });
638
+ var costSourceSchema = z.enum(["harness", "estimate", "mixed"]);
639
+ var usageRequestSchema = z.object({
640
+ /** Position in the run, from 1. */
641
+ index: z.number(),
642
+ responseId: z.string().optional(),
643
+ model: z.string().optional(),
644
+ provider: z.string().optional(),
645
+ inputTokens: z.number().optional(),
646
+ cacheReadTokens: z.number().optional(),
647
+ cacheWriteTokens: z.number().optional(),
648
+ outputTokens: z.number().optional(),
649
+ reasoningTokens: z.number().optional(),
650
+ costUsd: z.number().optional(),
651
+ costSource: z.enum(["harness", "estimate"]).optional(),
652
+ /** Separate provider-supplied summary; never generic private thinking content. */
653
+ reasoningSummary: z.string().max(4e3).optional(),
654
+ /** From sending the request to the end of the response, and to its first output. */
655
+ durationMs: z.number().optional(),
656
+ ttftMs: z.number().optional(),
657
+ stopReason: z.string().optional()
658
+ });
659
+ var usagePayloadSchema = z.object({
660
+ /** Input tokens that were not read from the provider's cache. */
661
+ inputTokens: z.number().optional(),
662
+ /** Includes reasoning tokens. */
663
+ outputTokens: z.number().optional(),
664
+ cacheReadTokens: z.number().optional(),
665
+ cacheWriteTokens: z.number().optional(),
666
+ reasoningTokens: z.number().optional(),
667
+ costUsd: z.number().optional(),
668
+ costSource: costSourceSchema.optional(),
669
+ /** Model requests made so far. */
670
+ requests: z.number().optional(),
671
+ /** Size of the latest request (not cumulative), and the model's limit. */
672
+ contextTokens: z.number().optional(),
673
+ contextWindow: z.number().optional(),
674
+ model: z.string().optional(),
675
+ provider: z.string().optional(),
676
+ /** The request this update reports. */
677
+ request: usageRequestSchema.optional()
678
+ });
679
+ var accountIssueCategorySchema = z.enum(["auth_expired", "rate_limited", "quota", "network", "unknown"]);
680
+ var accountLimitSchema = z.object({
681
+ id: z.string(),
682
+ label: z.string(),
683
+ usedPercent: z.number().nullable().optional(),
684
+ resetsAt: z.string().nullable().optional(),
685
+ windowMinutes: z.number().nullable().optional(),
686
+ status: z.string().nullable().optional()
687
+ });
688
+ var accountStatusPayload = z.object({
689
+ accountId: z.string(),
690
+ category: accountIssueCategorySchema.optional(),
691
+ message: z.string().optional(),
692
+ resetsAt: z.string().optional(),
693
+ plan: z.string().optional(),
694
+ limits: z.array(accountLimitSchema).optional()
695
+ });
696
+ var statusPayload = z.object({ status: runStatusSchema, reason: z.string().optional() });
697
+ var failedPayload = z.object({ code: z.string(), message: z.string() });
698
+ var emptyPayload = z.object({}).passthrough();
699
+ var runEventPayloadSchemas = {
700
+ started: emptyPayload,
701
+ "context.snapshot": contextSnapshotPayload,
702
+ "message.delta": messageDeltaPayload,
703
+ "message.completed": messageCompletedPayload,
704
+ "message.steered": messageSteeredPayload,
705
+ "tool.started": toolPayload,
706
+ "tool.completed": toolPayload,
707
+ "retry.started": retryStartedPayload,
708
+ "retry.ended": retryEndedPayload,
709
+ "compaction.started": compactionStartedPayload,
710
+ "compaction.ended": compactionEndedPayload,
711
+ "approval.requested": approvalRequestedPayload,
712
+ "approval.resolved": approvalResolvedPayload,
713
+ "question.requested": questionRequestedPayload,
714
+ "question.answered": questionAnsweredPayload,
715
+ "extension.notice": extensionNoticePayload,
716
+ "reaction.added": reactionAddedPayload,
717
+ "agent.message": agentMessagePayload,
718
+ "artifact.added": artifactPayload,
719
+ "file.changed": fileChangedPayload,
720
+ "usage.updated": usagePayloadSchema,
721
+ "account.status": accountStatusPayload,
722
+ "status.changed": statusPayload,
723
+ failed: failedPayload,
724
+ completed: emptyPayload
725
+ };
726
+ var runEventSchema = z.object({
727
+ id: z.string(),
728
+ runId: z.string(),
729
+ sessionId: z.string(),
730
+ sequence: z.number().int().nonnegative(),
731
+ timestamp: z.string(),
732
+ type: runEventTypeSchema,
733
+ payload: z.unknown(),
734
+ /** The native event this one was derived from, kept as evidence. */
735
+ native: z.unknown().optional(),
736
+ /** Set when consecutive text deltas were merged for relay; covers sequences coalescedFrom..sequence. */
737
+ coalescedFrom: z.number().int().nonnegative().optional()
738
+ }).superRefine((event, context) => {
739
+ if (!runEventPayloadSchemas[event.type].safeParse(event.payload).success) {
740
+ context.addIssue({ code: "custom", path: ["payload"], message: `Invalid payload for ${event.type}` });
741
+ }
742
+ });
743
+ var text = z.string().nullish().transform((value) => value ?? null);
744
+ var runRecordSchema = z.object({
745
+ resumedFromRunId: text,
746
+ parentRunId: text,
747
+ relation: text,
748
+ terminalReason: text,
749
+ id: z.string(),
750
+ projectId: text,
751
+ mode: z.string(),
752
+ harness: z.string(),
753
+ sessionType: z.string(),
754
+ sessionId: z.string(),
755
+ nativeSessionId: text,
756
+ workingDirectory: text,
757
+ worktreeId: text,
758
+ goal: text,
759
+ attachmentCount: z.number().int().nonnegative().default(0),
760
+ startedAt: z.string(),
761
+ endedAt: text,
762
+ status: z.string(),
763
+ exitCode: z.number().int().nullish().transform((value) => value ?? null),
764
+ handoffId: text,
765
+ accountId: text,
766
+ agentId: text,
767
+ trigger: text
768
+ });
769
+
770
+ // src/core/index.ts
771
+ import { DatabaseSync } from "node:sqlite";
772
+
773
+ // src/sessions/subagents.ts
774
+ var SUBAGENT_RECEIPT = "xingularSubagents";
775
+ var SUBAGENT_OWNER = "xingular-subagent-owner";
776
+ var SUBAGENT_RESULTS = "xingular-subagent-results";
777
+ function subagentHistory(entries) {
778
+ const owners = [];
779
+ const receipts = [];
780
+ for (const raw of entries) {
781
+ if (!raw || typeof raw !== "object") continue;
782
+ const entry = raw;
783
+ if (entry.type === "custom" && entry.customType === SUBAGENT_OWNER && typeof entry.data?.runId === "string") owners.push(entry.data.runId);
784
+ const message2 = entry.type === "message" ? entry.message : entry;
785
+ const receipt = message2?.details?.[SUBAGENT_RECEIPT];
786
+ if (!receipt || receipt.version !== 1 || typeof receipt.ownerRunId !== "string") continue;
787
+ const strings = (value) => Array.isArray(value) && value.every((id) => typeof id === "string");
788
+ if (receipt.spawned !== void 0 && !strings(receipt.spawned) || receipt.delivered !== void 0 && !strings(receipt.delivered)) continue;
789
+ receipts.push(receipt);
790
+ }
791
+ return { owners, receipts };
792
+ }
500
793
 
501
794
  // src/tools/askUser.ts
502
795
  var ASK_USER = "ask_user";
503
796
  var MAX_OPTIONS = 6;
504
- var text = (value) => typeof value === "string" ? value.trim() : "";
797
+ var text2 = (value) => typeof value === "string" ? value.trim() : "";
505
798
  function describeAnswer(answer) {
506
799
  const parts = [];
507
800
  if (answer.selected.length) parts.push(`The user chose: ${answer.selected.join(", ")}.`);
@@ -509,7 +802,7 @@ function describeAnswer(answer) {
509
802
  for (const [choice, note] of Object.entries(answer.notes ?? {})) parts.push(`Their note for ${choice}: ${note}`);
510
803
  return parts.join(" ") || "The user did not answer.";
511
804
  }
512
- function askUserTool(run) {
805
+ function askUserTool(run2) {
513
806
  return {
514
807
  name: ASK_USER,
515
808
  label: "Ask the user",
@@ -531,15 +824,15 @@ function askUserTool(run) {
531
824
  if (!Array.isArray(input.questions) || input.questions.length < 1 || input.questions.length > 5) return "Ask between one and five questions.";
532
825
  const questions = input.questions.map((raw) => {
533
826
  const item = raw;
534
- const summary = text(item.summary);
535
- const question = text(item.question);
536
- const options = (Array.isArray(item.options) ? item.options : []).map((option) => ({ label: text(option.label), description: text(option.description) })).filter((option) => option.label).slice(0, MAX_OPTIONS);
827
+ const summary = text2(item.summary);
828
+ const question = text2(item.question);
829
+ const options = (Array.isArray(item.options) ? item.options : []).map((option) => ({ label: text2(option.label), description: text2(option.description) })).filter((option) => option.label).slice(0, MAX_OPTIONS);
537
830
  return { summary, question, options, multiSelect: options.length > 1 && item.multiSelect === true, allowText: true };
538
831
  });
539
832
  if (questions.some((question) => !question.summary || question.summary.length > 100)) return "Each question needs a short summary of no more than 100 characters.";
540
833
  if (questions.some((question) => !question.question)) return "Every question needs a question sentence.";
541
834
  if (questions.some((question) => question.options.some((option) => !option.description))) return "Every choice needs a short description. Aim for 25 words or fewer.";
542
- const answers = await run.requestAnswers(questions);
835
+ const answers = await run2.requestAnswers(questions);
543
836
  return questions.map((question, index) => `${question.summary}: ${describeAnswer(answers[index])}`).join("\n");
544
837
  }
545
838
  };
@@ -551,9 +844,24 @@ function resultSize(result) {
551
844
  if (!Array.isArray(content)) return typeof result === "string" ? result.length : null;
552
845
  return content.reduce((total, part) => total + (part?.type === "text" && typeof part.text === "string" ? part.text.length : 0), 0);
553
846
  }
847
+ async function withParentCancellation(signal, work) {
848
+ const stop = () => {
849
+ void controlCall("terminal.interrupt").catch(() => {
850
+ });
851
+ };
852
+ if (signal?.aborted) stop();
853
+ signal?.addEventListener("abort", stop, { once: true });
854
+ try {
855
+ return await work();
856
+ } finally {
857
+ signal?.removeEventListener("abort", stop);
858
+ }
859
+ }
554
860
  async function piExtension_default(pi) {
555
861
  await terminalCall("startup", { phase: "Pi extension loaded" });
556
862
  let context = null;
863
+ let nativeSubagents = false;
864
+ let subagentParent = false;
557
865
  let timer = null;
558
866
  let polling = false;
559
867
  let failure;
@@ -624,6 +932,18 @@ async function piExtension_default(pi) {
624
932
  }
625
933
  });
626
934
  }
935
+ const native = await controlCall("terminal.subagents.tools");
936
+ nativeSubagents = native.tools.length > 0;
937
+ for (const tool of native.tools) pi.registerTool({
938
+ name: tool.name,
939
+ label: tool.label,
940
+ description: tool.description,
941
+ parameters: Type.Unsafe(tool.parameters),
942
+ execute: async (_id, input, signal) => {
943
+ const result = await withParentCancellation(signal, () => controlCall("terminal.subagents.call", { name: tool.name, input }, 2147483647));
944
+ return { content: [{ type: "text", text: result.text }], details: result.details ?? {} };
945
+ }
946
+ });
627
947
  } catch (error) {
628
948
  await terminalCall("startupFailed", { reason: `${stage} failed: ${error instanceof Error ? error.message : "Unavailable"}` }).catch(() => {
629
949
  });
@@ -656,13 +976,26 @@ async function piExtension_default(pi) {
656
976
  usage = emptyUsage();
657
977
  reasoningTokens = void 0;
658
978
  costReported = false;
659
- await terminalCall("begin", { text: event.messageType === "user" ? event.prompt : "" });
979
+ const begun = await terminalCall("begin", { text: event.messageType === "user" ? event.prompt : "" });
980
+ if (nativeSubagents) {
981
+ pi.appendEntry(SUBAGENT_OWNER, { runId: begun.runId });
982
+ await controlCall("terminal.subagents.register", { tools: pi.getActiveTools(), model: ctx.model ? `${ctx.model.provider}/${ctx.model.id}` : null, effort: pi.getThinkingLevel(), history: subagentHistory(ctx.sessionManager.getBranch()) });
983
+ subagentParent = true;
984
+ }
660
985
  });
986
+ const deliverChildren = async (event, ctx, settle) => {
987
+ if (!nativeSubagents || settle && event.outcome !== "completed" || ctx.signal?.aborted) return;
988
+ const result = await withParentCancellation(ctx.signal, () => controlCall("terminal.subagents.delivery", { history: subagentHistory([...ctx.sessionManager.getBranch(), ...event.entries]), settle }, 2147483647));
989
+ if (!result) return;
990
+ return { entries: [...event.entries, { type: "custom_message", customType: SUBAGENT_RESULTS, content: result.text, display: false, details: { [SUBAGENT_RECEIPT]: result.receipt } }], continue: true };
991
+ };
992
+ pi.on("turn_end", (event, ctx) => deliverChildren(event, ctx, false));
993
+ pi.on("agent_before_settle", (event, ctx) => deliverChildren(event, ctx, true));
661
994
  pi.on("message_end", async (event) => {
662
995
  if (event.message.role !== "assistant") return;
663
996
  if (event.message.stopReason === "error" || event.message.stopReason === "aborted") failure = event.message.errorMessage ?? `Pi stopped: ${event.message.stopReason}`;
664
- const text2 = event.message.content.filter((part) => part.type === "text").map((part) => part.text).join("\n");
665
- if (text2) await terminalCall("message", { text: text2 });
997
+ const text3 = event.message.content.filter((part) => part.type === "text").map((part) => part.text).join("\n");
998
+ if (text3) await terminalCall("message", { text: text3 });
666
999
  if (event.message.usage) {
667
1000
  const reported = event.message.usage;
668
1001
  usage.inputTokens += reported.input ?? 0;
@@ -707,9 +1040,9 @@ async function piExtension_default(pi) {
707
1040
  });
708
1041
  pi.on("tool_call", async (event) => {
709
1042
  try {
710
- const policy = await terminalCall("policy", { tool: event.toolName, args: event.input });
1043
+ const policy = await controlCall("terminal.policy", { tool: event.toolName, args: event.input });
711
1044
  if (policy.requiresApproval) {
712
- const approval = await terminalCall("approval", { requestId: event.toolCallId, target: event.toolName, action: JSON.stringify(event.input), tool: event.toolName, args: event.input });
1045
+ const approval = await controlCall("terminal.approval", { requestId: event.toolCallId, tool: event.toolName, args: event.input });
713
1046
  if (approval.decision === "approved") return;
714
1047
  return { block: true, reason: "The user denied this action. Ask how they would like to proceed." };
715
1048
  }
@@ -732,8 +1065,16 @@ async function piExtension_default(pi) {
732
1065
  fileTools.delete(event.toolCallId);
733
1066
  if (!event.isError && path) await terminalCall("file", { path, change: "modified" });
734
1067
  });
735
- pi.on("agent_settled", async () => {
736
- await terminalCall("settled", { error: failure });
1068
+ pi.on("agent_settled", async (event, ctx) => {
1069
+ if (subagentParent && !event.aborted && !failure) {
1070
+ try {
1071
+ await controlCall("terminal.subagents.verify", { history: subagentHistory(ctx.sessionManager.getBranch()) }, 2147483647);
1072
+ } catch (error) {
1073
+ failure = error instanceof Error ? error.message : String(error);
1074
+ }
1075
+ }
1076
+ subagentParent = false;
1077
+ await terminalCall("settled", { error: failure, aborted: event.aborted });
737
1078
  });
738
1079
  pi.on("user_bash", async () => {
739
1080
  throw new Error("Use the bash tool so this action can be checked.");
package/docs/README.md CHANGED
@@ -5,6 +5,9 @@ Current implementation: [Harness terminal frontend](terminal-tui.md). The packag
5
5
 
6
6
  | ID | Record | Type | Status | Related |
7
7
  |---|---|---|---|---|
8
+ | ADR-0004 | [Fullscreen terminal layout with a fixed dock](decisions/ADR-0004-fullscreen-terminal-layout.md) | Decision | Accepted; working tree implementation | ADR-0001, [Terminal architecture and evidence](terminal-tui.md) |
9
+ | ADR-0003 | [Shared asynchronous subagents with native-history delivery receipts](decisions/ADR-0003-shared-asynchronous-subagents.md) | Decision | Accepted; working tree implementation | ADR-0002, [Embedding API](../README.md) |
10
+ | ADR-0002 | [Native subagent lifecycle in Harness sessions](decisions/ADR-0002-native-subagent-lifecycle.md) | Decision | Superseded by ADR-0003 | [Embedding API](../README.md) |
8
11
  | ADR-0001 | [Own the terminal frontend and keep Pi as the session runtime](decisions/ADR-0001-owned-terminal.md) | Decision | Accepted; working tree implementation | [Terminal architecture and evidence](terminal-tui.md) |
9
12
 
10
13
  Living reference pages describe checked source behavior. Accepted decisions record the choice;
@@ -0,0 +1,49 @@
1
+ ---
2
+ id: ADR-0002
3
+ title: Native subagent lifecycle in Harness sessions
4
+ status: superseded
5
+ superseded_by: ADR-0003
6
+ created: 2026-10-18
7
+ updated: 2026-10-18
8
+ ---
9
+ # ADR-0002 — Native subagent lifecycle in Harness sessions
10
+
11
+ Superseded by [ADR-0003](ADR-0003-shared-asynchronous-subagents.md), which expands the native
12
+ service to asynchronous execution, reliable delivery, and terminal parity. The earlier decision
13
+ and its rationale are retained below.
14
+
15
+ ## Context
16
+
17
+ Subagents already used Harness sessions, capability snapshots and run lineage, but their tool
18
+ owned launching and polled run files for completion. A host inferred ownership from session
19
+ names, coupling it to an internal identifier format.
20
+
21
+ ## Decision
22
+
23
+ `Sessions.spawnSubagent` owns child creation. A live parent's captured launch supplies caller,
24
+ account, model selection, folder, policy context, prompt and capability ceiling. The host or
25
+ model supplies only a complete brief and an optional tool subset. Current capability availability
26
+ and the captured nesting limit further restrict each child. Empty tool subsets stay empty;
27
+ spawning permission is never added back after narrowing. Children start with fresh history,
28
+ without inherited image attachments or request preamble. Existing tool gating applies.
29
+
30
+ The model tool is an adapter to the same native API exposed over RPC. `RunRegistry.wait` resolves
31
+ on settlement, including failed startup and interruption, without polling or per-call listeners.
32
+ The Harness Subagents plugin attributes the model-visible tool. Opt-in remains a session option;
33
+ `{}` selects the Harness default depth of one. Application organization delegation remains a host
34
+ concern. Consumers resolve ownership from run lineage rather than parsing session IDs.
35
+
36
+ ## Alternatives and consequences
37
+
38
+ Keeping orchestration in the tool would leave hosts without the same lifecycle contract.
39
+ Moving application organizations into Harness would violate the embedding boundary. A new
40
+ background job manager would duplicate existing run tracking, approvals and interruption.
41
+ The selected change reuses these layers and preserves the synchronous model tool behavior.
42
+ The native API starts children only for live parents in the same Harness instance; it does not
43
+ resume a stopped parent's launch or implement cross-process child management.
44
+
45
+ ## Evidence
46
+
47
+ Regression coverage is in `src/sessions/sessions.test.ts` and `src/server/server.test.ts`.
48
+ Application host and run-family tests cover nested lineage with opaque session IDs. Verification
49
+ results belong to the implementation handoff; no release or live-provider behavior is claimed.
@@ -0,0 +1,68 @@
1
+ ---
2
+ id: ADR-0003
3
+ title: Shared asynchronous subagents with native-history delivery receipts
4
+ status: accepted
5
+ created: 2026-10-18
6
+ updated: 2026-10-18
7
+ supersedes: ADR-0002
8
+ ---
9
+ # ADR-0003 — Shared asynchronous subagents with native-history delivery receipts
10
+
11
+ ## Context and decision
12
+
13
+ The owner selected reliable delivery, asynchronous spawn/wait, and one SDK/terminal/App API after
14
+ studying OMP. They chose attached children joined before the parent finishes, async spawning by
15
+ default, availability for every interactive caller within its capability ceiling, and recovery
16
+ only when the user explicitly resumes. This supersedes the blocking tool contract in ADR-0002.
17
+
18
+ `harness.subagents` owns spawning, selective waits and results. SDK and terminal adapters register
19
+ live, frozen parent context with it; no model argument supplies caller, account or replacement
20
+ launch metadata. The terminal adapter uses its private control key. Children use normal Harness
21
+ sessions, isolated accounts, fresh history, existing gates and capability intersection. The old
22
+ `sessions.spawnSubagent` entry point forwards to the service for compatibility.
23
+
24
+ A parent reserves each child in `runs/<parent>/subagents.json` through the run store before startup.
25
+ A native branch owner marker is persisted before a real parent accepts spawns. Results are delivered
26
+ at turn boundaries or through explicit blocking tools. Native messages and tool results carry typed
27
+ receipts; only receipts on the selected branch acknowledge delivery. Constructing a delivery draft
28
+ never acknowledges it. Large batches acknowledge only the included children. The final settlement
29
+ hook joins children and injects remaining results; an adapter verification guard refuses successful
30
+ completion if persisted native history still lacks a result. The stable system prefix is unchanged.
31
+
32
+ On explicit resume, matching owner markers, caller/account/folder/native identity and run lineage
33
+ restore undelivered results. A startup reservation that never opened becomes an interrupted run.
34
+ Existing startup recovery interrupts orphaned active runs. Neither recovery path restarts model
35
+ work. Cancellation settles approvals and questions before waiting for SDK abort, preventing blocked
36
+ child interactions from deadlocking parent shutdown.
37
+
38
+ Interactive sessions enable the tools by default, `null` disables them, and explicit capability
39
+ sets can exclude them. Unattended callers remain off unless the host explicitly enables them.
40
+ Default nesting remains one, with a maximum of three. There is no new scheduler, reusable-worker
41
+ lifecycle, model routing, workspace isolation system, concurrency cap or family budget here.
42
+
43
+ ## Alternatives and consequences
44
+
45
+ A tool-owned launcher leaves terminal and SDK behavior divergent. An in-memory delivery flag loses
46
+ results across restart; searching prose for run IDs confuses mentions with delivery. Native-history
47
+ receipts identify persisted context without adding a second acknowledgement store. Run records
48
+ remain the outcome source of truth, while the parent file preserves startup reservations.
49
+
50
+ The App owns organization delegation and resolves native ancestry for thread routing, tools,
51
+ isolation and connections. Its existing work-budget accounting includes native descendants. Harness
52
+ learns no organizational role or application concept. Native completion records execution outcome;
53
+ it does not certify the child's findings or artifacts.
54
+
55
+ ## Evidence and limits
56
+
57
+ `subagentService.test.ts` covers selective waits, foreign handles, cancellation, bounded batches and
58
+ recovery windows. Real scripted Pi tests in `conversation.test.ts` cover async delivery, inline
59
+ receipts and interruption during a child question. `runtime.socket.test.ts` exercises the real
60
+ terminal socket, private controls, native execution and child decision routing. RPC and App host
61
+ regressions cover protocol shapes and ancestry. Tests use disposable storage without external
62
+ model requests. These are working-tree changes, not a publication or live-provider acceptance claim.
63
+
64
+ Verification on 2026-10-18: Harness typecheck, 455 tests (two skipped), unchanged golden fixtures,
65
+ build and standalone proof passed. App typecheck, layer/UI checks, sidecar build, 30 sidecar tests
66
+ and 217 Rust tests passed (one ignored). The renderer suite passed 491 tests and retained its
67
+ pre-existing `PendingRequestDock.test.ts` heading-text assertion failure. No live account or
68
+ user-data-root testing was performed.