@xingular/harness 1.0.0 → 1.2.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.
- package/CHANGELOG.md +46 -5
- package/README.md +75 -21
- package/dist/cli.js +17421 -8711
- package/dist/sdk/accounts/accounts.d.ts +27 -3
- package/dist/sdk/accounts/accounts.js +78 -24
- package/dist/sdk/accounts/secrets.d.ts +12 -0
- package/dist/sdk/accounts/secrets.js +114 -7
- package/dist/sdk/accounts/store.d.ts +26 -1
- package/dist/sdk/accounts/store.js +1 -1
- package/dist/sdk/agents/builtin.d.ts +2 -0
- package/dist/sdk/agents/builtin.js +48 -0
- package/dist/sdk/agents/narrow.d.ts +13 -0
- package/dist/sdk/agents/narrow.js +29 -0
- package/dist/sdk/agents/planResearch.d.ts +5 -0
- package/dist/sdk/agents/planResearch.js +19 -0
- package/dist/sdk/agents/profiles.d.ts +81 -0
- package/dist/sdk/agents/profiles.js +226 -0
- package/dist/sdk/annotations/bridge.d.ts +42 -0
- package/dist/sdk/annotations/bridge.js +264 -0
- package/dist/sdk/annotations/capture.d.ts +116 -0
- package/dist/sdk/annotations/capture.js +308 -0
- package/dist/sdk/annotations/delivery.d.ts +72 -0
- package/dist/sdk/annotations/delivery.js +130 -0
- package/dist/sdk/annotations/inspect.d.ts +10 -0
- package/dist/sdk/annotations/inspect.js +33 -0
- package/dist/sdk/annotations/isolated.d.ts +6 -0
- package/dist/sdk/annotations/isolated.js +51 -0
- package/dist/sdk/annotations/pageScript.d.ts +28 -0
- package/dist/sdk/annotations/pageScript.js +871 -0
- package/dist/sdk/annotations/part.d.ts +10 -0
- package/dist/sdk/annotations/part.js +155 -0
- package/dist/sdk/annotations/queue.d.ts +4 -0
- package/dist/sdk/annotations/queue.js +40 -0
- package/dist/sdk/annotations/serverMethods.d.ts +3 -0
- package/dist/sdk/annotations/serverMethods.js +62 -0
- package/dist/sdk/annotations/store.d.ts +167 -0
- package/dist/sdk/annotations/store.js +394 -0
- package/dist/sdk/browser/backend.d.ts +16 -0
- package/dist/sdk/browser/backend.js +17 -0
- package/dist/sdk/browser/contracts.d.ts +19 -0
- package/dist/sdk/browser/contracts.js +116 -0
- package/dist/sdk/browser/index.d.ts +14 -3
- package/dist/sdk/browser/index.js +11 -4
- package/dist/sdk/browser/serverMethods.d.ts +3 -0
- package/dist/sdk/browser/serverMethods.js +7 -0
- package/dist/sdk/browser/session.d.ts +1 -0
- package/dist/sdk/browser/session.js +45 -9
- package/dist/sdk/browser/tools.d.ts +1 -4
- package/dist/sdk/browser/tools.js +14 -91
- package/dist/sdk/cli.js +24 -15
- package/dist/sdk/commands/list.d.ts +3 -0
- package/dist/sdk/commands/list.js +75 -0
- package/dist/sdk/commands/registry.d.ts +250 -0
- package/dist/sdk/commands/registry.js +47 -0
- package/dist/sdk/commands/routes.d.ts +16 -0
- package/dist/sdk/commands/routes.js +33 -0
- package/dist/sdk/core/bus.js +1 -1
- package/dist/sdk/core/commandBudget.d.ts +23 -0
- package/dist/sdk/core/commandBudget.js +57 -0
- package/dist/sdk/core/env.d.ts +1 -1
- package/dist/sdk/core/env.js +1 -1
- package/dist/sdk/core/index.js +17 -1
- package/dist/sdk/core/interactiveProfile.d.ts +46 -0
- package/dist/sdk/core/interactiveProfile.js +143 -0
- package/dist/sdk/core/workspace.d.ts +13 -0
- package/dist/sdk/core/workspace.js +88 -0
- package/dist/sdk/core/workspaceBashTool.d.ts +4 -0
- package/dist/sdk/core/workspaceBashTool.js +36 -0
- package/dist/sdk/core/workspaceSandbox.d.ts +31 -0
- package/dist/sdk/core/workspaceSandbox.js +320 -0
- package/dist/sdk/core/workspaceWriteOperations.d.ts +22 -0
- package/dist/sdk/core/workspaceWriteOperations.js +206 -0
- package/dist/sdk/doctor/serverMethods.d.ts +3 -0
- package/dist/sdk/doctor/serverMethods.js +8 -0
- package/dist/sdk/doctor/types.d.ts +2 -2
- package/dist/sdk/harness.d.ts +22 -0
- package/dist/sdk/harness.js +84 -5
- package/dist/sdk/history/recall.js +8 -8
- package/dist/sdk/history/serverMethods.d.ts +3 -0
- package/dist/sdk/history/serverMethods.js +8 -0
- package/dist/sdk/hub/cli.d.ts +1 -1
- package/dist/sdk/hub/cli.js +1 -1
- package/dist/sdk/hub/schema.d.ts +1 -1
- package/dist/sdk/hub/serverMethods.d.ts +3 -0
- package/dist/sdk/hub/serverMethods.js +11 -0
- package/dist/sdk/index.d.ts +16 -1
- package/dist/sdk/index.js +10 -0
- package/dist/sdk/mcp/connections.js +14 -7
- package/dist/sdk/mcp/serverMethods.d.ts +3 -0
- package/dist/sdk/mcp/serverMethods.js +22 -0
- package/dist/sdk/pi/runtime.d.ts +2 -1
- package/dist/sdk/pi/runtime.js +2 -2
- package/dist/sdk/plugins/builtin.d.ts +1 -0
- package/dist/sdk/plugins/builtin.js +145 -33
- package/dist/sdk/plugins/builtinRegistry.d.ts +60 -0
- package/dist/sdk/plugins/builtinRegistry.js +71 -0
- package/dist/sdk/plugins/catalogue.js +2 -1
- package/dist/sdk/plugins/cli.d.ts +1 -1
- package/dist/sdk/plugins/cli.js +4 -3
- package/dist/sdk/plugins/commandPrefix.d.ts +8 -0
- package/dist/sdk/plugins/commandPrefix.js +21 -0
- package/dist/sdk/plugins/firstParty.d.ts +1 -1
- package/dist/sdk/plugins/firstParty.js +8 -2
- package/dist/sdk/plugins/inventory.js +4 -3
- package/dist/sdk/plugins/manifest.d.ts +2 -0
- package/dist/sdk/plugins/manifest.js +2 -1
- package/dist/sdk/plugins/surfaces.d.ts +5 -0
- package/dist/sdk/plugins/surfaces.js +11 -0
- package/dist/sdk/plugins/types.d.ts +15 -0
- package/dist/sdk/prompt/compile.d.ts +19 -2
- package/dist/sdk/prompt/compile.js +79 -26
- package/dist/sdk/prompt/standard.d.ts +17 -0
- package/dist/sdk/prompt/standard.js +21 -0
- package/dist/sdk/refine/cli.d.ts +1 -1
- package/dist/sdk/refine/cli.js +2 -2
- package/dist/sdk/refine/serverMethods.d.ts +3 -0
- package/dist/sdk/refine/serverMethods.js +20 -0
- package/dist/sdk/server/methods.js +27 -86
- package/dist/sdk/server/params.d.ts +4 -0
- package/dist/sdk/server/params.js +11 -0
- package/dist/sdk/server/remote.js +36 -11
- package/dist/sdk/sessions/compactionCheckpoint.d.ts +11 -0
- package/dist/sdk/sessions/compactionCheckpoint.js +79 -0
- package/dist/sdk/sessions/continuation.d.ts +3 -0
- package/dist/sdk/sessions/continuation.js +5 -1
- package/dist/sdk/sessions/contracts.d.ts +185 -0
- package/dist/sdk/sessions/contracts.js +101 -4
- package/dist/sdk/sessions/control.d.ts +72 -0
- package/dist/sdk/sessions/control.js +158 -0
- package/dist/sdk/sessions/initialBash.d.ts +7 -0
- package/dist/sdk/sessions/initialBash.js +7 -0
- package/dist/sdk/sessions/planMode.d.ts +18 -4
- package/dist/sdk/sessions/planMode.js +82 -16
- package/dist/sdk/sessions/reasoningSummaries.d.ts +2 -2
- package/dist/sdk/sessions/reasoningSummaries.js +3 -3
- package/dist/sdk/sessions/registry.d.ts +2 -1
- package/dist/sdk/sessions/registry.js +6 -3
- package/dist/sdk/sessions/runIndex.js +7 -7
- package/dist/sdk/sessions/runStore.d.ts +14 -0
- package/dist/sdk/sessions/runStore.js +38 -1
- package/dist/sdk/sessions/runner.d.ts +40 -2
- package/dist/sdk/sessions/runner.js +163 -15
- package/dist/sdk/sessions/sessions.d.ts +40 -8
- package/dist/sdk/sessions/sessions.js +279 -27
- package/dist/sdk/sessions/sideQuestionServerMethods.d.ts +3 -0
- package/dist/sdk/sessions/sideQuestionServerMethods.js +19 -0
- package/dist/sdk/sessions/sideQuestions.d.ts +66 -0
- package/dist/sdk/sessions/sideQuestions.js +290 -0
- package/dist/sdk/sessions/subagentServerMethods.d.ts +3 -0
- package/dist/sdk/sessions/subagentServerMethods.js +41 -0
- package/dist/sdk/sessions/subagentService.d.ts +22 -2
- package/dist/sdk/sessions/subagentService.js +93 -16
- package/dist/sdk/sessions/subagents.d.ts +9 -1
- package/dist/sdk/sessions/subagents.js +19 -0
- package/dist/sdk/sessions/workSlice.d.ts +103 -0
- package/dist/sdk/sessions/workSlice.js +297 -0
- package/dist/sdk/sessions/workspaceBoundary.d.ts +15 -0
- package/dist/sdk/sessions/workspaceBoundary.js +46 -0
- package/dist/sdk/terminal/browser.d.ts +62 -0
- package/dist/sdk/terminal/browser.js +41 -0
- package/dist/sdk/terminal/browserControl.d.ts +29 -0
- package/dist/sdk/terminal/browserControl.js +187 -0
- package/dist/sdk/terminal/commandHelp.d.ts +17 -0
- package/dist/sdk/terminal/commandHelp.js +20 -0
- package/dist/sdk/terminal/controller.d.ts +15 -1
- package/dist/sdk/terminal/controller.js +67 -8
- package/dist/sdk/terminal/opentui/subagentsWidget.d.ts +28 -0
- package/dist/sdk/terminal/opentui/subagentsWidget.js +52 -0
- package/dist/sdk/terminal/opentui/theme.d.ts +43 -0
- package/dist/sdk/terminal/opentui/theme.js +87 -0
- package/dist/sdk/terminal/piExtension.d.ts +6 -1
- package/dist/sdk/terminal/piExtension.js +169 -84
- package/dist/sdk/terminal/socket.d.ts +6 -3
- package/dist/sdk/terminal/socket.js +40 -9
- package/dist/sdk/terminal/standalone.d.ts +9 -0
- package/dist/sdk/terminal/standalone.js +118 -19
- package/dist/sdk/terminal/subagentMonitor.d.ts +47 -0
- package/dist/sdk/terminal/subagentMonitor.js +152 -0
- package/dist/sdk/terminal/terminals.d.ts +60 -7
- package/dist/sdk/terminal/terminals.js +598 -47
- package/dist/sdk/terminal/trajectory.js +2 -2
- package/dist/sdk/terminal/tui/annotateCommand.d.ts +3 -0
- package/dist/sdk/terminal/tui/annotateCommand.js +22 -0
- package/dist/sdk/terminal/tui/commandPages.d.ts +7 -1
- package/dist/sdk/terminal/tui/commandPages.js +57 -6
- package/dist/sdk/terminal/tui/commands.d.ts +10 -4
- package/dist/sdk/terminal/tui/commands.js +269 -65
- package/dist/sdk/terminal/tui/dialogGeometry.d.ts +2 -2
- package/dist/sdk/terminal/tui/dialogGeometry.js +4 -2
- package/dist/sdk/terminal/tui/dialogs.d.ts +6 -1
- package/dist/sdk/terminal/tui/dialogs.js +9 -2
- package/dist/sdk/terminal/tui/list.d.ts +14 -1
- package/dist/sdk/terminal/tui/list.js +96 -14
- package/dist/sdk/terminal/tui/modelPicker.js +1 -6
- package/dist/sdk/terminal/tui/page.d.ts +3 -0
- package/dist/sdk/terminal/tui/pageOverlay.js +27 -7
- package/dist/sdk/terminal/tui/pages/accountsPage.d.ts +26 -4
- package/dist/sdk/terminal/tui/pages/accountsPage.js +264 -36
- package/dist/sdk/terminal/tui/pages/agentsPage.d.ts +74 -0
- package/dist/sdk/terminal/tui/pages/agentsPage.js +190 -0
- package/dist/sdk/terminal/tui/pages/annotatePage.d.ts +94 -0
- package/dist/sdk/terminal/tui/pages/annotatePage.js +341 -0
- package/dist/sdk/terminal/tui/pages/browserPage.d.ts +87 -0
- package/dist/sdk/terminal/tui/pages/browserPage.js +738 -0
- package/dist/sdk/terminal/tui/pages/doctorPage.d.ts +2 -0
- package/dist/sdk/terminal/tui/pages/doctorPage.js +103 -15
- package/dist/sdk/terminal/tui/pages/notesPage.d.ts +75 -0
- package/dist/sdk/terminal/tui/pages/notesPage.js +201 -0
- package/dist/sdk/terminal/tui/pages/pluginsPage.d.ts +0 -5
- package/dist/sdk/terminal/tui/pages/pluginsPage.js +9 -3
- package/dist/sdk/terminal/tui/pages/settingsPage.d.ts +8 -0
- package/dist/sdk/terminal/tui/pages/settingsPage.js +16 -1
- package/dist/sdk/terminal/tui/pages/subagentsPage.d.ts +24 -0
- package/dist/sdk/terminal/tui/pages/subagentsPage.js +152 -0
- package/dist/sdk/terminal/tui/pages/trajectoryPage.d.ts +12 -5
- package/dist/sdk/terminal/tui/pages/trajectoryPage.js +156 -83
- package/dist/sdk/terminal/tui/pages/usagePage.d.ts +43 -0
- package/dist/sdk/terminal/tui/pages/usagePage.js +370 -0
- package/dist/sdk/terminal/tui/pages/worktreesPage.d.ts +72 -0
- package/dist/sdk/terminal/tui/pages/worktreesPage.js +203 -0
- package/dist/sdk/terminal/tui/piAdapter.d.ts +1 -2
- package/dist/sdk/terminal/tui/piAdapter.js +3 -5
- package/dist/sdk/terminal/tui/planMode.d.ts +1 -1
- package/dist/sdk/terminal/tui/planMode.js +1 -1
- package/dist/sdk/terminal/tui/pluginCommands.d.ts +16 -0
- package/dist/sdk/terminal/tui/pluginCommands.js +31 -0
- package/dist/sdk/terminal/tui/questions.d.ts +2 -0
- package/dist/sdk/terminal/tui/runtime.js +27 -5
- package/dist/sdk/terminal/tui/sessionPicker.d.ts +40 -0
- package/dist/sdk/terminal/tui/sessionPicker.js +141 -0
- package/dist/sdk/terminal/tui/settingsModel.d.ts +2 -0
- package/dist/sdk/terminal/tui/settingsModel.js +32 -2
- package/dist/sdk/terminal/tui/sideQuestion.d.ts +26 -26
- package/dist/sdk/terminal/tui/sideQuestion.js +131 -98
- package/dist/sdk/terminal/tui/ui.d.ts +2 -0
- package/dist/sdk/terminal/tui/ui.js +1 -0
- package/dist/sdk/tools/askUser.js +26 -8
- package/dist/sdk/tools/checklist.js +3 -2
- package/dist/sdk/tools/checkpoint.d.ts +27 -0
- package/dist/sdk/tools/checkpoint.js +154 -0
- package/dist/sdk/tools/getCapabilities.d.ts +31 -0
- package/dist/sdk/tools/getCapabilities.js +34 -0
- package/dist/sdk/tools/harnessTools.d.ts +2 -0
- package/dist/sdk/tools/harnessTools.js +1 -1
- package/dist/sdk/tools/spawnSubagent.js +14 -4
- package/dist/sdk/usage/analytics.d.ts +20 -0
- package/dist/sdk/usage/analytics.js +151 -0
- package/dist/sdk/usage/ledger.d.ts +38 -0
- package/dist/sdk/usage/ledger.js +142 -0
- package/dist/sdk/usage/limits/adapters.d.ts +11 -0
- package/dist/sdk/usage/limits/adapters.js +349 -0
- package/dist/sdk/usage/limits/capture.d.ts +8 -0
- package/dist/sdk/usage/limits/capture.js +47 -0
- package/dist/sdk/usage/limits/http.d.ts +12 -0
- package/dist/sdk/usage/limits/http.js +51 -0
- package/dist/sdk/usage/limits/normalize.d.ts +15 -0
- package/dist/sdk/usage/limits/normalize.js +211 -0
- package/dist/sdk/usage/limits/service.d.ts +30 -0
- package/dist/sdk/usage/limits/service.js +176 -0
- package/dist/sdk/usage/limits/types.d.ts +65 -0
- package/dist/sdk/usage/limits/types.js +1 -0
- package/dist/sdk/usage/serverMethods.d.ts +3 -0
- package/dist/sdk/usage/serverMethods.js +22 -0
- package/dist/sdk/usage/service.d.ts +32 -0
- package/dist/sdk/usage/service.js +66 -0
- package/dist/sdk/usage/settings.d.ts +7 -0
- package/dist/sdk/usage/settings.js +28 -0
- package/dist/sdk/usage/types.d.ts +60 -0
- package/dist/sdk/usage/types.js +4 -0
- package/dist/sdk/web/part.d.ts +10 -0
- package/dist/sdk/web/part.js +6 -0
- package/dist/sdk/web/serverMethods.d.ts +3 -0
- package/dist/sdk/web/serverMethods.js +7 -0
- package/dist/sdk/wiki/serverMethods.d.ts +3 -0
- package/dist/sdk/wiki/serverMethods.js +26 -0
- package/dist/sdk/worktrees/cli.d.ts +11 -0
- package/dist/sdk/worktrees/cli.js +146 -0
- package/dist/sdk/worktrees/copy.d.ts +9 -0
- package/dist/sdk/worktrees/copy.js +111 -0
- package/dist/sdk/worktrees/git.d.ts +16 -0
- package/dist/sdk/worktrees/git.js +81 -0
- package/dist/sdk/worktrees/serverMethods.d.ts +3 -0
- package/dist/sdk/worktrees/serverMethods.js +30 -0
- package/dist/sdk/worktrees/service.d.ts +73 -0
- package/dist/sdk/worktrees/service.js +483 -0
- package/dist/sdk/worktrees/setup.d.ts +23 -0
- package/dist/sdk/worktrees/setup.js +65 -0
- package/dist/sdk/worktrees/types.d.ts +91 -0
- package/dist/sdk/worktrees/types.js +1 -0
- package/dist/sdk/world/cli.d.ts +2 -2
- package/dist/sdk/world/cli.js +6 -6
- package/dist/sdk/world/world.d.ts +3 -1
- package/dist/sdk/world/world.js +8 -9
- package/dist/terminal-opentui.js +16456 -5966
- package/dist/terminal-pi.js +1579 -573
- package/docs/README.md +22 -2
- package/docs/architecture/00-overview.md +28 -16
- package/docs/architecture/02-sessions-and-runs.md +184 -25
- package/docs/architecture/03-variants.md +10 -2
- package/docs/architecture/04-prompt.md +65 -7
- package/docs/architecture/06-plugins-and-capabilities.md +57 -4
- package/docs/architecture/07-tools-and-gate.md +112 -17
- package/docs/architecture/08-memory-and-history.md +4 -2
- package/docs/architecture/09-accounts-and-connectors.md +39 -1
- package/docs/architecture/10-server-protocol.md +168 -4
- package/docs/architecture/11-terminal.md +251 -8
- package/docs/architecture/12-storage.md +45 -3
- package/docs/architecture/13-world-tools.md +49 -6
- package/docs/architecture/14-surfaces-and-parity.md +136 -0
- package/docs/architecture/README.md +17 -4
- package/docs/architecture/proposals/PROP-0006-run-checklist.md +4 -2
- package/docs/architecture/proposals/PROP-0013-resource-browser.md +58 -0
- package/docs/architecture/proposals/PROP-0014-fullscreen-browser-tabs.md +58 -0
- package/docs/architecture/proposals/PROP-0015-one-capability-surface.md +88 -0
- package/docs/architecture/proposals/PROP-0016-managed-worktrees.md +156 -0
- package/docs/architecture/proposals/PROP-0017-page-annotations.md +131 -0
- package/docs/architecture/proposals/PROP-0018-annotation-queue-and-app-targets.md +179 -0
- package/docs/architecture/proposals/PROP-0019-contextual-side-questions.md +122 -0
- package/docs/architecture/proposals/PROP-0020-builtin-feature-plugins.md +105 -0
- package/docs/architecture/proposals/PROP-0021-command-namespaces-and-duplicates.md +99 -0
- package/docs/architecture/proposals/PROP-0022-plan-only-questions.md +102 -0
- package/docs/architecture/proposals/PROP-0023-xagent-cli-name.md +61 -0
- package/docs/architecture/proposals/PROP-0024-question-sketch-previews.md +130 -0
- package/docs/architecture/proposals/PROP-0025-contextual-question-wireframes.md +49 -0
- package/docs/architecture/proposals/PROP-0026-connected-sketch-display.md +94 -0
- package/docs/architecture/proposals/PROP-0027-xingular-agent-product-identity.md +60 -0
- package/docs/architecture/proposals/PROP-0028-normalize-question-wireframes.md +55 -0
- package/docs/architecture/proposals/PROP-0029-agent-profiles.md +124 -0
- package/docs/architecture/proposals/PROP-0030-subagent-title.md +86 -0
- package/docs/architecture/proposals/PROP-0031-plugin-command-owner-namespaces.md +54 -0
- package/docs/architecture/proposals/PROP-0032-planning-quality.md +196 -0
- package/docs/architecture/proposals/PROP-0033-compaction-plan-checkpoints.md +75 -0
- package/docs/architecture/proposals/PROP-0034-conversation-fast-mode.md +120 -0
- package/docs/architecture/proposals/PROP-0035-retain-subagent-status.md +47 -0
- package/docs/architecture/proposals/PROP-0036-first-class-harness-commands.md +59 -0
- package/docs/architecture/proposals/PROP-0037-workspace-write-boundary.md +63 -0
- package/docs/architecture/proposals/PROP-0038-annotate-open-shadow-roots.md +73 -0
- package/docs/architecture/proposals/PROP-0039-workspace-boundary-hardening.md +139 -0
- package/docs/architecture/proposals/PROP-0040-one-launch-path.md +87 -0
- package/docs/architecture/proposals/PROP-0041-bounded-work-slices.md +247 -0
- package/docs/architecture/proposals/PROP-0042-usage-and-provider-limits.md +127 -0
- package/docs/architecture/proposals/PROP-0043-read-only-plan-bash.md +74 -0
- package/docs/architecture/proposals/README.md +39 -0
- package/docs/decisions/ADR-0001-owned-terminal.md +1 -1
- package/docs/decisions/ADR-0009-xharness-command.md +6 -5
- package/docs/decisions/ADR-0014-public-release-baseline.md +3 -2
- package/docs/decisions/ADR-0015-resource-browser.md +28 -0
- package/docs/decisions/ADR-0016-fullscreen-browser-tabs.md +31 -0
- package/docs/decisions/ADR-0017-one-capability-surface.md +34 -0
- package/docs/decisions/ADR-0018-builtin-feature-plugins.md +35 -0
- package/docs/decisions/ADR-0019-managed-git-worktrees.md +31 -0
- package/docs/decisions/ADR-0020-page-annotations.md +37 -0
- package/docs/decisions/ADR-0021-required-command-namespaces.md +40 -0
- package/docs/decisions/ADR-0022-plan-only-question-tools.md +34 -0
- package/docs/decisions/ADR-0023-contextual-side-question-runs.md +35 -0
- package/docs/decisions/ADR-0024-xagent-command.md +40 -0
- package/docs/decisions/ADR-0025-question-choice-sketches.md +49 -0
- package/docs/decisions/ADR-0026-contextual-question-wireframes.md +58 -0
- package/docs/decisions/ADR-0027-connected-sketch-display.md +60 -0
- package/docs/decisions/ADR-0028-xingular-agent-product-identity.md +56 -0
- package/docs/decisions/ADR-0029-normalized-question-wireframe-borders.md +33 -0
- package/docs/decisions/ADR-0030-agent-profiles.md +43 -0
- package/docs/decisions/ADR-0031-plugin-command-owner-namespaces.md +37 -0
- package/docs/decisions/ADR-0032-compaction-run-checkpoints.md +33 -0
- package/docs/decisions/ADR-0033-plan-mode-research-delegation.md +47 -0
- package/docs/decisions/ADR-0034-first-class-harness-terminal-commands.md +25 -0
- package/docs/decisions/ADR-0035-one-launch-path.md +40 -0
- package/docs/decisions/ADR-0036-workspace-write-operation-boundary.md +54 -0
- package/docs/decisions/ADR-0037-usage-ledger-and-provider-limits.md +44 -0
- package/docs/decisions/ADR-0038-plan-bash-os-read-only.md +29 -0
- package/docs/doctor.md +10 -4
- package/docs/experiments/EXP-0003-performance-audit/data/session-runtime-replication.json +420 -0
- package/docs/experiments/EXP-0003-performance-audit/data/session-runtime.json +420 -0
- package/docs/experiments/EXP-0003-performance-audit/data/terminal-compact.json +376 -0
- package/docs/experiments/EXP-0003-performance-audit/data/terminal-history.json +422 -0
- package/docs/experiments/EXP-0003-performance-audit/data/terminal-question.json +95 -0
- package/docs/experiments/EXP-0003-performance-audit/data/terminal-wide.json +1036 -0
- package/docs/experiments/EXP-0003-performance-audit/plan.md +109 -0
- package/docs/experiments/EXP-0003-performance-audit/results.md +151 -0
- package/docs/features/README.md +26 -0
- package/docs/features/interfaces/README.md +1 -1
- package/docs/future-work.md +48 -0
- package/docs/handoff-2026-10-25-app-parity-and-solo-tabs.md +202 -0
- package/docs/parity.md +134 -0
- package/docs/releases/1.0.0.md +2 -2
- package/docs/releases/1.1.1.md +28 -0
- package/docs/releases/1.2.0.md +49 -0
- package/docs/releases/4.0.0.md +47 -0
- package/docs/releases/README.md +37 -27
- package/docs/study/00-index.md +8 -0
- package/docs/study/STUDY-0003-long-horizon-runs.md +210 -0
- package/docs/study/autonomous-engineering-sources.md +277 -0
- package/docs/study/autonomy-sources.md +147 -0
- package/docs/study/proactive-autonomy.md +408 -0
- package/docs/study/tui-resource-browser.md +78 -0
- package/docs/terminal-tui.md +386 -113
- package/package.json +6 -5
|
@@ -1,89 +1,79 @@
|
|
|
1
|
-
// `/btw <question>`: a side question answered by a separate, tool-free session while the main
|
|
2
|
-
// conversation carries on. It lives in a dock above the composer, never enters the transcript, the
|
|
3
|
-
// queue or the run history, and is gone on dismiss, on a session change, or when a new one replaces it.
|
|
4
|
-
import { createAgentSession, DefaultResourceLoader, SessionManager, SettingsManager } from '@earendil-works/pi-coding-agent';
|
|
5
1
|
import { truncateToWidth, visibleWidth, wrapTextWithAnsi } from '@earendil-works/pi-tui';
|
|
6
2
|
import { contentWidth, footerRow, neutralRule, padded, safeText } from "./chrome.js";
|
|
7
3
|
import { getWorkingSpinnerFrames, SPINNER_INTERVAL_MS, spinnerFrameAt } from "./workingStatus.js";
|
|
8
|
-
export const SIDE_PROMPT = [
|
|
9
|
-
'You answer one short side question from a person whose main task is still running in another session.',
|
|
10
|
-
'You have no tools: you cannot read or change files, run commands, browse, or call anything, and you must not say you did.',
|
|
11
|
-
'Use the conversation excerpt for context and do not invent what it does not show. Answer briefly and plainly.'
|
|
12
|
-
].join(' ');
|
|
13
|
-
const TURNS = 6;
|
|
14
|
-
const TURN_CHARS = 1_200;
|
|
15
|
-
const TOTAL_CHARS = 6_000;
|
|
16
4
|
const QUESTION_ROWS = 2;
|
|
17
|
-
|
|
5
|
+
const ANSWER_ROWS = 8;
|
|
6
|
+
const MAX_CLIENT_CONTEXT_CHARS = 300_000;
|
|
7
|
+
const POLL_INTERVAL_MS = 160;
|
|
18
8
|
function blocks(content) {
|
|
19
9
|
if (typeof content === 'string')
|
|
20
10
|
return [{ type: 'text', text: content }];
|
|
21
11
|
return Array.isArray(content) ? content.filter((block) => typeof block === 'object' && block !== null) : [];
|
|
22
12
|
}
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
13
|
+
function contentText(content) {
|
|
14
|
+
return blocks(content).filter(block => block.type === 'text' && typeof block.text === 'string').map(block => String(block.text)).join('\n').trim();
|
|
15
|
+
}
|
|
16
|
+
function serialized(value) {
|
|
17
|
+
if (typeof value === 'string')
|
|
18
|
+
return value;
|
|
19
|
+
try {
|
|
20
|
+
return JSON.stringify(value) ?? '';
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
return '[unavailable]';
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
/** Visible native history and tool activity, with hidden reasoning and image bytes omitted. */
|
|
29
27
|
export function sideQuestionSnapshot(messages, running) {
|
|
30
|
-
const
|
|
28
|
+
const lines = [];
|
|
31
29
|
for (const message of messages) {
|
|
32
|
-
|
|
33
|
-
if (role !== 'user' && role !== 'assistant')
|
|
34
|
-
continue;
|
|
35
|
-
const parts = blocks(content);
|
|
36
|
-
const text = parts.filter(block => block.type === 'text' && typeof block.text === 'string').map(block => String(block.text)).join('\n').trim();
|
|
37
|
-
const tools = [...new Set(parts.filter(block => block.type === 'toolCall' && typeof block.name === 'string').map(block => String(block.name)))];
|
|
38
|
-
if (!text && !tools.length)
|
|
30
|
+
if (!message || typeof message !== 'object')
|
|
39
31
|
continue;
|
|
40
|
-
|
|
32
|
+
const item = message;
|
|
33
|
+
const parts = blocks(item.content);
|
|
34
|
+
if (item.role === 'user' || item.role === 'assistant') {
|
|
35
|
+
const text = parts.filter(block => block.type === 'text' && typeof block.text === 'string').map(block => String(block.text)).join('\n').trim();
|
|
36
|
+
if (text)
|
|
37
|
+
lines.push(`${item.role === 'user' ? 'User' : 'Assistant'}:\n${safeText(text)}`);
|
|
38
|
+
for (const call of parts.filter(block => block.type === 'toolCall')) {
|
|
39
|
+
const name = typeof call.name === 'string' ? call.name : 'unknown tool';
|
|
40
|
+
lines.push(`Assistant tool call (${name}):\n${safeText(serialized(call.arguments))}`);
|
|
41
|
+
}
|
|
42
|
+
if (parts.some(block => block.type === 'image'))
|
|
43
|
+
lines.push(`${item.role === 'user' ? 'User' : 'Assistant'} attached an image; image bytes are omitted.`);
|
|
44
|
+
}
|
|
45
|
+
else if (item.role === 'toolResult') {
|
|
46
|
+
const name = typeof item.toolName === 'string' ? item.toolName : typeof item.name === 'string' ? item.name : 'unknown tool';
|
|
47
|
+
const output = contentText(item.content);
|
|
48
|
+
lines.push(`Tool result (${name}${typeof item.toolCallId === 'string' ? `, ${item.toolCallId}` : ''}):\n${safeText(output || '(no text result)')}`);
|
|
49
|
+
if (parts.some(block => block.type === 'image'))
|
|
50
|
+
lines.push('Tool result also contained an image; image bytes are omitted.');
|
|
51
|
+
}
|
|
41
52
|
}
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
recent = recent.slice(1);
|
|
45
|
-
const lines = ['Conversation excerpt, oldest first:', ...(recent.length ? recent : ['(nothing yet)'])];
|
|
53
|
+
if (!lines.length)
|
|
54
|
+
lines.push('(No visible conversation messages yet.)');
|
|
46
55
|
if (running)
|
|
47
|
-
lines.push('
|
|
48
|
-
|
|
56
|
+
lines.push('The main reply is still running; its latest message may be unfinished.');
|
|
57
|
+
const full = lines.join('\n\n');
|
|
58
|
+
return full.length > MAX_CLIENT_CONTEXT_CHARS
|
|
59
|
+
? `[Earlier visible context omitted to fit the side-question request.]\n\n${full.slice(-MAX_CLIENT_CONTEXT_CHARS)}`
|
|
60
|
+
: full;
|
|
49
61
|
}
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
const model = main.model;
|
|
53
|
-
if (!model || model.id === 'unknown')
|
|
54
|
-
throw new Error('Choose a model before asking a side question.');
|
|
55
|
-
const settingsManager = SettingsManager.inMemory({});
|
|
56
|
-
const resourceLoader = new DefaultResourceLoader({
|
|
57
|
-
cwd: services.cwd, agentDir: services.agentDir, settingsManager,
|
|
58
|
-
noExtensions: true, noSkills: true, noPromptTemplates: true, noThemes: true, noContextFiles: true, systemPrompt: SIDE_PROMPT
|
|
59
|
-
});
|
|
60
|
-
await resourceLoader.reload();
|
|
61
|
-
const { session } = await createAgentSession({
|
|
62
|
-
cwd: services.cwd, agentDir: services.agentDir, modelRuntime: services.modelRuntime, settingsManager, resourceLoader,
|
|
63
|
-
sessionManager: SessionManager.inMemory(services.cwd), model, thinkingLevel: 'off', noTools: 'all'
|
|
64
|
-
});
|
|
65
|
-
return {
|
|
66
|
-
prompt: text => session.prompt(text),
|
|
67
|
-
abort: () => session.abort(),
|
|
68
|
-
subscribe: listener => session.subscribe(() => listener()),
|
|
69
|
-
answer: () => session.getLastAssistantText() ?? '',
|
|
70
|
-
failure: () => {
|
|
71
|
-
const last = session.messages.at(-1);
|
|
72
|
-
return last?.role === 'assistant' && last.stopReason === 'error' ? last.errorMessage ?? 'The provider returned an error.' : undefined;
|
|
73
|
-
},
|
|
74
|
-
dispose: () => session.dispose()
|
|
75
|
-
};
|
|
62
|
+
function payload(event) {
|
|
63
|
+
return event.payload && typeof event.payload === 'object' ? event.payload : {};
|
|
76
64
|
}
|
|
77
65
|
export class SideQuestion {
|
|
78
66
|
options;
|
|
79
|
-
|
|
80
|
-
unsubscribe;
|
|
67
|
+
runId;
|
|
81
68
|
generation = 0;
|
|
69
|
+
pollTimer;
|
|
82
70
|
question = '';
|
|
83
71
|
answerText = '';
|
|
84
72
|
error;
|
|
85
73
|
current;
|
|
86
74
|
startedAt = 0;
|
|
75
|
+
afterSequence = -1;
|
|
76
|
+
answerOffset = 0;
|
|
87
77
|
dock;
|
|
88
78
|
constructor(options) {
|
|
89
79
|
this.options = options;
|
|
@@ -91,6 +81,7 @@ export class SideQuestion {
|
|
|
91
81
|
}
|
|
92
82
|
get visible() { return this.current !== undefined; }
|
|
93
83
|
get state() { return this.current; }
|
|
84
|
+
get answer() { return this.answerText; }
|
|
94
85
|
/** Asks one question, replacing any earlier one. Empty text dismisses. */
|
|
95
86
|
async ask(question) {
|
|
96
87
|
const text = question.trim();
|
|
@@ -98,73 +89,111 @@ export class SideQuestion {
|
|
|
98
89
|
await this.dismiss();
|
|
99
90
|
return;
|
|
100
91
|
}
|
|
101
|
-
await this.release();
|
|
102
92
|
const generation = ++this.generation;
|
|
93
|
+
await this.release();
|
|
103
94
|
this.question = text;
|
|
104
95
|
this.startedAt = Date.now();
|
|
105
96
|
this.answerText = '';
|
|
106
97
|
this.error = undefined;
|
|
98
|
+
this.answerOffset = 0;
|
|
99
|
+
this.afterSequence = -1;
|
|
107
100
|
this.current = 'thinking';
|
|
108
101
|
this.options.mount(this.dock);
|
|
109
102
|
this.options.changed();
|
|
110
103
|
try {
|
|
111
|
-
const
|
|
104
|
+
const started = await this.options.start(text, this.options.context());
|
|
112
105
|
if (generation !== this.generation) {
|
|
113
|
-
|
|
106
|
+
await this.options.interrupt(started.runId).catch(() => undefined);
|
|
114
107
|
return;
|
|
115
108
|
}
|
|
116
|
-
this.
|
|
117
|
-
this.
|
|
118
|
-
if (generation !== this.generation)
|
|
119
|
-
return;
|
|
120
|
-
this.answerText = session.answer();
|
|
121
|
-
this.options.changed();
|
|
122
|
-
});
|
|
123
|
-
await session.prompt(`${this.options.snapshot()}\n\nSide question: ${text}`);
|
|
124
|
-
if (generation !== this.generation)
|
|
125
|
-
return;
|
|
126
|
-
this.answerText = session.answer();
|
|
127
|
-
const failure = session.failure();
|
|
128
|
-
if (failure) {
|
|
129
|
-
this.current = 'failed';
|
|
130
|
-
this.error = failure;
|
|
131
|
-
}
|
|
132
|
-
else
|
|
133
|
-
this.current = 'answered';
|
|
109
|
+
this.runId = started.runId;
|
|
110
|
+
void this.poll(generation);
|
|
134
111
|
}
|
|
135
112
|
catch (error) {
|
|
136
113
|
if (generation !== this.generation)
|
|
137
114
|
return;
|
|
138
115
|
this.current = 'failed';
|
|
139
116
|
this.error = error instanceof Error ? error.message : String(error);
|
|
117
|
+
this.options.changed();
|
|
140
118
|
}
|
|
119
|
+
}
|
|
120
|
+
/** Scrolls the answer while it is longer than the dock. */
|
|
121
|
+
scroll(direction) {
|
|
122
|
+
const step = Math.max(1, ANSWER_ROWS - 2);
|
|
123
|
+
this.answerOffset = Math.max(0, this.answerOffset + (direction === 'up' ? -step : step));
|
|
141
124
|
this.options.changed();
|
|
142
125
|
}
|
|
126
|
+
/** Copies the whole side answer, including any lines outside the dock. */
|
|
127
|
+
async copyAnswer() {
|
|
128
|
+
if (this.answerText)
|
|
129
|
+
await this.options.copyText(this.answerText);
|
|
130
|
+
}
|
|
143
131
|
/** Interrupts a running side question and removes the dock. */
|
|
144
132
|
async dismiss() {
|
|
145
133
|
this.generation++;
|
|
146
|
-
this.current = undefined;
|
|
147
134
|
await this.release();
|
|
135
|
+
this.current = undefined;
|
|
148
136
|
this.options.mount(undefined);
|
|
149
137
|
this.options.changed();
|
|
150
138
|
}
|
|
151
|
-
async
|
|
152
|
-
const
|
|
153
|
-
this.
|
|
154
|
-
this.unsubscribe?.();
|
|
155
|
-
this.unsubscribe = undefined;
|
|
156
|
-
if (!session)
|
|
139
|
+
async poll(generation) {
|
|
140
|
+
const runId = this.runId;
|
|
141
|
+
if (!runId || generation !== this.generation)
|
|
157
142
|
return;
|
|
158
143
|
try {
|
|
159
|
-
await
|
|
144
|
+
const result = await this.options.read(runId, this.afterSequence);
|
|
145
|
+
if (generation !== this.generation)
|
|
146
|
+
return;
|
|
147
|
+
for (const event of result.events) {
|
|
148
|
+
this.afterSequence = Math.max(this.afterSequence, event.sequence);
|
|
149
|
+
const data = payload(event);
|
|
150
|
+
if (event.type === 'message.delta' && typeof data.text === 'string')
|
|
151
|
+
this.answerText += data.text;
|
|
152
|
+
if (event.type === 'message.completed' && data.role === 'assistant' && typeof data.text === 'string')
|
|
153
|
+
this.answerText = data.text;
|
|
154
|
+
if (event.type === 'failed')
|
|
155
|
+
this.error = typeof data.message === 'string' ? data.message : 'The side question failed.';
|
|
156
|
+
if (event.type === 'status.changed' && data.status === 'failed')
|
|
157
|
+
this.error = typeof data.reason === 'string' ? data.reason : 'The side question failed.';
|
|
158
|
+
if (event.type === 'status.changed' && data.status === 'interrupted')
|
|
159
|
+
this.error = 'The side question was interrupted.';
|
|
160
|
+
if (event.type === 'completed')
|
|
161
|
+
this.current = 'answered';
|
|
162
|
+
if (event.type === 'failed' || event.type === 'status.changed' && ['failed', 'interrupted'].includes(String(data.status)))
|
|
163
|
+
this.current = 'failed';
|
|
164
|
+
}
|
|
165
|
+
if (result.run.status === 'completed')
|
|
166
|
+
this.current = 'answered';
|
|
167
|
+
if (result.run.status === 'failed' || result.run.status === 'interrupted') {
|
|
168
|
+
this.current = 'failed';
|
|
169
|
+
this.error ??= result.run.terminalReason ?? 'The side question did not complete.';
|
|
170
|
+
}
|
|
171
|
+
this.options.changed();
|
|
172
|
+
if (this.current === 'thinking')
|
|
173
|
+
this.pollTimer = setTimeout(() => { void this.poll(generation); }, POLL_INTERVAL_MS);
|
|
160
174
|
}
|
|
161
|
-
catch {
|
|
175
|
+
catch (error) {
|
|
176
|
+
if (generation !== this.generation)
|
|
177
|
+
return;
|
|
178
|
+
this.current = 'failed';
|
|
179
|
+
this.error = error instanceof Error ? error.message : String(error);
|
|
180
|
+
this.options.changed();
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
async release() {
|
|
184
|
+
if (this.pollTimer)
|
|
185
|
+
clearTimeout(this.pollTimer);
|
|
186
|
+
this.pollTimer = undefined;
|
|
187
|
+
const runId = this.runId;
|
|
188
|
+
this.runId = undefined;
|
|
189
|
+
if (!runId || this.current !== 'thinking')
|
|
190
|
+
return;
|
|
162
191
|
try {
|
|
163
|
-
|
|
192
|
+
await this.options.interrupt(runId);
|
|
164
193
|
}
|
|
165
|
-
catch { /*
|
|
194
|
+
catch { /* The dock can still be dismissed if a run already stopped. */ }
|
|
166
195
|
}
|
|
167
|
-
/** The dock:
|
|
196
|
+
/** The dock: question, a scrollable answer window, and a hint for browsing or copying. */
|
|
168
197
|
render(width) {
|
|
169
198
|
if (width < 1 || !this.current)
|
|
170
199
|
return [];
|
|
@@ -184,14 +213,18 @@ export class SideQuestion {
|
|
|
184
213
|
question[QUESTION_ROWS - 1] = truncateToWidth(question[QUESTION_ROWS - 1], inner - 1, '') + theme.fg('muted', '…');
|
|
185
214
|
const text = this.current === 'failed' ? `${this.answerText ? `${this.answerText}\n` : ''}${this.error ?? 'The side question failed.'}` : this.answerText;
|
|
186
215
|
const wrapped = text ? text.split('\n').flatMap(line => line ? wrapTextWithAnsi(safeText(line), inner) : ['']) : [];
|
|
187
|
-
const
|
|
188
|
-
|
|
216
|
+
const maxOffset = Math.max(0, wrapped.length - ANSWER_ROWS);
|
|
217
|
+
this.answerOffset = Math.min(this.answerOffset, maxOffset);
|
|
218
|
+
const shown = wrapped.slice(this.answerOffset, this.answerOffset + ANSWER_ROWS).map(line => this.current === 'failed' ? theme.fg('error', line) : line);
|
|
219
|
+
const before = this.answerOffset;
|
|
220
|
+
const after = Math.max(0, wrapped.length - before - shown.length);
|
|
221
|
+
const textHint = before || after ? `PgUp/PgDn browse · ${before ? `${before} above` : ''}${before && after ? ' · ' : ''}${after ? `${after} below` : ''} · ` : '';
|
|
189
222
|
return [
|
|
190
223
|
neutralRule(width, theme),
|
|
191
224
|
...question.map(line => padded(line, width)),
|
|
192
225
|
...(shown.length ? ['', ...shown.map(line => padded(line, width))] : []),
|
|
193
226
|
'',
|
|
194
|
-
footerRow(`${
|
|
227
|
+
footerRow(`${textHint}Ctrl+X copy · /btw or Esc dismiss`, width, theme)
|
|
195
228
|
].map(line => truncateToWidth(line, width, ''));
|
|
196
229
|
}
|
|
197
230
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Theme, type AutocompleteProviderFactory, type ExtensionUIContext, type KeybindingsManager, type WorkingIndicatorOptions } from '@earendil-works/pi-coding-agent';
|
|
2
2
|
import { type AutocompleteProvider, type EditorComponent, type TUI } from '@earendil-works/pi-tui';
|
|
3
|
+
import { type GroupedSelectOption } from "./dialogs.js";
|
|
3
4
|
type EditorFactory = Parameters<ExtensionUIContext['setEditorComponent']>[0];
|
|
4
5
|
type HeaderFactory = Parameters<ExtensionUIContext['setHeader']>[0];
|
|
5
6
|
type FooterFactory = Parameters<ExtensionUIContext['setFooter']>[0];
|
|
@@ -37,6 +38,7 @@ export interface TerminalUiOptions {
|
|
|
37
38
|
export interface TerminalUi extends ExtensionUIContext {
|
|
38
39
|
readonly state: TerminalUiState;
|
|
39
40
|
secretInput: ExtensionUIContext['input'];
|
|
41
|
+
selectGrouped(title: string, options: readonly GroupedSelectOption[], opts?: Parameters<ExtensionUIContext['select']>[2]): Promise<string | undefined>;
|
|
40
42
|
wrapAutocomplete(provider: AutocompleteProvider): AutocompleteProvider;
|
|
41
43
|
reset(): void;
|
|
42
44
|
setThemes(themes: Theme[]): void;
|
|
@@ -22,6 +22,7 @@ export function createTerminalUi(options) {
|
|
|
22
22
|
return {
|
|
23
23
|
state,
|
|
24
24
|
select: (title, items, opts) => dialogs.select(title, items, opts),
|
|
25
|
+
selectGrouped: (title, items, opts) => dialogs.selectGrouped(title, items, opts),
|
|
25
26
|
confirm: (title, message, opts) => dialogs.confirm(title, message, opts),
|
|
26
27
|
input: (title, placeholder, opts) => dialogs.input(title, placeholder, opts),
|
|
27
28
|
secretInput: (title, placeholder, opts) => dialogs.input(title, placeholder, opts, true),
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
// Asking the person a question and waiting for the answer: part of every session.
|
|
2
|
+
import { QUESTION_SKETCH_MAX_LENGTH, QUESTION_SKETCH_PATTERN, questionSketchError } from "../sessions/contracts.js";
|
|
1
3
|
export const ASK_USER = 'ask_user';
|
|
2
4
|
const MAX_OPTIONS = 6;
|
|
3
5
|
const text = (value) => (typeof value === 'string' ? value.trim() : '');
|
|
@@ -16,16 +18,18 @@ export function askUserTool(run) {
|
|
|
16
18
|
return {
|
|
17
19
|
name: ASK_USER,
|
|
18
20
|
label: 'Ask the user',
|
|
19
|
-
description: 'Ask the user one to five related questions in one request and wait until they answer the batch. Give each question a concise 2
|
|
20
|
-
'Every question must offer 1–6 short choices, each with a helpful description of 25 words or fewer, and at least one choice must be marked recommended: true. Recommendations are visual only: they never preselect an answer, and submitted choices keep their original labels. Mark at most one recommended choice for a single-choice question, or several when multiSelect is true. Every question also includes a free-text “Something else” answer. Set multiSelect when several can apply. Use it when you need a decision, not for small talk.',
|
|
21
|
+
description: 'Ask the user one to five related questions in one request and wait until they answer the batch. Give each question a concise 1–2 word label and aim to keep each question to 25 words or fewer. ' +
|
|
22
|
+
'Every question must offer 1–6 short choices, each with a helpful description of 25 words or fewer, and at least one choice must be marked recommended: true. Recommendations are visual only: they never preselect an answer, and submitted choices keep their original labels. Mark at most one recommended choice for a single-choice question, or several when multiSelect is true. Every question also includes a free-text “Something else” answer. Set multiSelect when several can apply. When a UI diagram helps compare choices, set uiSketch: true and give every choice a detailed, bounded ASCII wireframe. Use ASCII tables and panel borders to show the affected element with relevant neighboring UI, readable labels and spacing. Keep each panel’s outer `+` corners in fixed columns, include side edges at those same columns on every content row, and run horizontal borders corner to corner; if a title sits in a top border, leave at least two dashes on both sides. Keep framing consistent across alternatives and concentrate detail on what changes. Do not substitute a symbolic logo or one-line illustration. The preview supplies its outer border; draw internal borders only for actual UI panels. Use it when you need a decision, not for small talk.',
|
|
21
23
|
parameters: (Type) => Type.Object({
|
|
22
24
|
questions: Type.Array(Type.Object({
|
|
23
|
-
summary: Type.String({ description: 'A concise
|
|
25
|
+
summary: Type.String({ description: 'A concise label of one or two words' }),
|
|
24
26
|
question: Type.String({ description: 'A clear, standalone question; aim for 25 words or fewer' }),
|
|
27
|
+
uiSketch: Type.Optional(Type.Literal(true, { description: 'Enable an ASCII sketch preview; when true, every choice must include a sketch.' })),
|
|
25
28
|
options: Type.Array(Type.Object({
|
|
26
29
|
label: Type.String({ description: 'A short choice; do not add a recommendation suffix to the label' }),
|
|
27
30
|
description: Type.String({ description: 'Required. Briefly explain this choice in 25 words or fewer.' }),
|
|
28
|
-
recommended: Type.Optional(Type.Boolean({ description: 'Whether to show this choice as a visual recommendation; at least one choice per question must be recommended, it is not preselected, and answers use the original label' }))
|
|
31
|
+
recommended: Type.Optional(Type.Boolean({ description: 'Whether to show this choice as a visual recommendation; at least one choice per question must be recommended, it is not preselected, and answers use the original label' })),
|
|
32
|
+
sketch: Type.Optional(Type.String({ minLength: 1, maxLength: QUESTION_SKETCH_MAX_LENGTH, pattern: QUESTION_SKETCH_PATTERN, description: 'Literal ASCII UI wireframe: 1–24 LF-separated lines, at most 80 printable ASCII characters per line. Include the affected element and neighboring UI using structured table/panel borders with fixed, matching outer-edge columns on every row. Required for every choice when uiSketch is true.' }))
|
|
29
33
|
}), {
|
|
30
34
|
minItems: 1,
|
|
31
35
|
maxItems: MAX_OPTIONS,
|
|
@@ -37,26 +41,33 @@ export function askUserTool(run) {
|
|
|
37
41
|
execute: async (input) => {
|
|
38
42
|
if (!Array.isArray(input.questions) || input.questions.length < 1 || input.questions.length > 5)
|
|
39
43
|
return 'Ask between one and five questions.';
|
|
44
|
+
let invalidSketchMode = false;
|
|
45
|
+
let invalidSketchValue = false;
|
|
40
46
|
const questions = input.questions.map((raw) => {
|
|
41
47
|
const item = raw;
|
|
42
48
|
const summary = text(item.summary);
|
|
43
49
|
const question = text(item.question);
|
|
50
|
+
if (item.uiSketch !== undefined && item.uiSketch !== true)
|
|
51
|
+
invalidSketchMode = true;
|
|
44
52
|
const options = (Array.isArray(item.options) ? item.options : [])
|
|
45
53
|
.map((option) => {
|
|
46
54
|
const choice = option;
|
|
55
|
+
if (choice.sketch !== undefined && typeof choice.sketch !== 'string')
|
|
56
|
+
invalidSketchValue = true;
|
|
47
57
|
return {
|
|
48
58
|
label: text(choice.label),
|
|
49
59
|
description: text(choice.description),
|
|
50
|
-
...(choice.recommended === true ? { recommended: true } : {})
|
|
60
|
+
...(choice.recommended === true ? { recommended: true } : {}),
|
|
61
|
+
...(typeof choice.sketch === 'string' ? { sketch: choice.sketch } : {})
|
|
51
62
|
};
|
|
52
63
|
})
|
|
53
64
|
.filter((option) => option.label)
|
|
54
65
|
.slice(0, MAX_OPTIONS);
|
|
55
66
|
const multiSelect = options.length > 1 && item.multiSelect === true;
|
|
56
|
-
return { summary, question, options, multiSelect, allowText: true };
|
|
67
|
+
return { summary, question, options, ...(item.uiSketch === true ? { uiSketch: true } : {}), multiSelect, allowText: true };
|
|
57
68
|
});
|
|
58
|
-
if (questions.some((question) => !question.summary || question.summary.length > 100))
|
|
59
|
-
return 'Each question needs a
|
|
69
|
+
if (questions.some((question) => !question.summary || question.summary.length > 100 || question.summary.split(/\s+/u).filter(Boolean).length > 2))
|
|
70
|
+
return 'Each question needs a concise 1–2 word summary label of no more than 100 characters.';
|
|
60
71
|
if (questions.some((question) => !question.question))
|
|
61
72
|
return 'Every question needs a question sentence.';
|
|
62
73
|
if (questions.some((question) => question.options.length === 0))
|
|
@@ -67,6 +78,13 @@ export function askUserTool(run) {
|
|
|
67
78
|
return 'A single-choice question may recommend at most one option.';
|
|
68
79
|
if (questions.some((question) => question.options.some((option) => !option.description)))
|
|
69
80
|
return 'Every choice needs a short description. Aim for 25 words or fewer.';
|
|
81
|
+
if (invalidSketchMode)
|
|
82
|
+
return 'Set uiSketch to true to enable question sketches.';
|
|
83
|
+
if (invalidSketchValue)
|
|
84
|
+
return 'Each choice sketch must be text.';
|
|
85
|
+
const sketchError = questions.map((question) => questionSketchError(question.uiSketch, question.options)).find(Boolean);
|
|
86
|
+
if (sketchError)
|
|
87
|
+
return sketchError;
|
|
70
88
|
const answers = await run.requestAnswers(questions);
|
|
71
89
|
return questions.map((question, index) => `${question.summary}: ${describeAnswer(answers[index])}`).join('\n');
|
|
72
90
|
}
|
|
@@ -22,9 +22,10 @@ export function checklistTool(run) {
|
|
|
22
22
|
return {
|
|
23
23
|
name: CHECKLIST_WRITE,
|
|
24
24
|
label: 'Write the checklist',
|
|
25
|
-
description: 'Keep a checklist of the steps of the current work, so
|
|
26
|
-
'Use it
|
|
25
|
+
description: 'Keep a checklist of the steps of the current work. The user watches this list to follow your progress, so write it early and keep it current. Send the whole list every time; it replaces the previous one. ' +
|
|
26
|
+
'Use it whenever the work has several distinct steps or will take several tool calls, when the user gives several things to do, and when a subagent brief asks for more than one thing. Do not use it for a single, simple step or for conversation. ' +
|
|
27
27
|
'Statuses: pending, in_progress, completed. Keep exactly one step in_progress while you work, mark a step completed as soon as it is done (not in batches), and add steps you discover. ' +
|
|
28
|
+
'Before your final response, write one final checklist update: mark every fully completed step completed, and leave unfinished or blocked steps in their accurate pending or in_progress status. Never mark unfinished work completed. ' +
|
|
28
29
|
'Only mark a step completed when it is fully done; if something blocks it, keep it in_progress and add a step for the blocker.',
|
|
29
30
|
parameters: (Type) => Type.Object({
|
|
30
31
|
items: Type.Array(Type.Object({
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { type Checkpoint } from "../sessions/contracts.js";
|
|
2
|
+
import type { ActiveRun } from "../sessions/registry.js";
|
|
3
|
+
import type { RunEvent } from "../sessions/contracts.js";
|
|
4
|
+
import type { ToolSpec } from "./types.js";
|
|
5
|
+
export declare const CHECKPOINT_WRITE = "checkpoint_write";
|
|
6
|
+
/** What the model may send: the record without the numbers the harness fills in. */
|
|
7
|
+
export type CheckpointInput = Omit<Checkpoint, 'slice' | 'reason'> & {
|
|
8
|
+
reason?: Checkpoint['reason'];
|
|
9
|
+
};
|
|
10
|
+
export type CheckpointParse = {
|
|
11
|
+
ok: true;
|
|
12
|
+
value: CheckpointInput;
|
|
13
|
+
} | {
|
|
14
|
+
ok: false;
|
|
15
|
+
error: string;
|
|
16
|
+
};
|
|
17
|
+
/** The checkpoint as the model sent it, or one line saying what to fix. */
|
|
18
|
+
export declare function parseCheckpoint(value: unknown): CheckpointParse;
|
|
19
|
+
/** The model's own checkpoint, filled in with this slice and why it was written. */
|
|
20
|
+
export declare function checkpointRecord(input: CheckpointInput, slice: number, fallbackReason: Checkpoint['reason']): Checkpoint;
|
|
21
|
+
/** The latest checkpoint a run recorded, if any. */
|
|
22
|
+
export declare function checkpointOf(events: readonly RunEvent[]): Checkpoint | null;
|
|
23
|
+
/** The checkpoint as the next slice reads it. Bounded by its parts; the caller clips the whole. */
|
|
24
|
+
export declare function describeCheckpoint(checkpoint: Checkpoint): string;
|
|
25
|
+
/** The checkpoint as the model reads it back after writing it. */
|
|
26
|
+
export declare function ackCheckpoint(checkpoint: Checkpoint): string;
|
|
27
|
+
export declare function checkpointTool(run: Pick<ActiveRun, 'emit'>, slice: number | (() => number)): ToolSpec;
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
// The reply's handoff when it reaches a work-slice budget (PROP-0041). The model writes it once,
|
|
2
|
+
// the run records it as a `checkpoint` event, and the next slice is told where this one stopped.
|
|
3
|
+
// It only changes run state, so it stays callable in plan mode.
|
|
4
|
+
import { checkpointCheckSchema, checkpointDecisionSchema, checkpointFileSchema, checkpointItemSchema, checkpointReasonSchema, MAX_CHECKPOINT_CHECKS, MAX_CHECKPOINT_DECISIONS, MAX_CHECKPOINT_DONE, MAX_CHECKPOINT_FILES, MAX_CHECKPOINT_SUMMARY, runEventPayloadSchemas } from "../sessions/contracts.js";
|
|
5
|
+
export const CHECKPOINT_WRITE = 'checkpoint_write';
|
|
6
|
+
const text = (value) => (typeof value === 'string' ? value.trim() : '');
|
|
7
|
+
/** Trims and bounds one entry; an unusable entry is dropped rather than failing the whole record. */
|
|
8
|
+
function collect(value, parse, limit) {
|
|
9
|
+
if (!Array.isArray(value))
|
|
10
|
+
return [];
|
|
11
|
+
const out = [];
|
|
12
|
+
for (const entry of value.slice(0, limit)) {
|
|
13
|
+
const parsed = parse(entry);
|
|
14
|
+
if (parsed)
|
|
15
|
+
out.push(parsed);
|
|
16
|
+
}
|
|
17
|
+
return out;
|
|
18
|
+
}
|
|
19
|
+
/** The checkpoint as the model sent it, or one line saying what to fix. */
|
|
20
|
+
export function parseCheckpoint(value) {
|
|
21
|
+
if (!value || typeof value !== 'object')
|
|
22
|
+
return { ok: false, error: 'Send checkpoint_write an object with summary and next.' };
|
|
23
|
+
const input = value;
|
|
24
|
+
const summary = text(input.summary);
|
|
25
|
+
if (summary.length > MAX_CHECKPOINT_SUMMARY)
|
|
26
|
+
return { ok: false, error: `Keep summary to ${MAX_CHECKPOINT_SUMMARY} characters.` };
|
|
27
|
+
if (text(input.next).length > 500)
|
|
28
|
+
return { ok: false, error: 'Keep next to 500 characters.' };
|
|
29
|
+
if (text(input.blocked).length > 500)
|
|
30
|
+
return { ok: false, error: 'Keep blocked to 500 characters.' };
|
|
31
|
+
const done = collect(input.done, (entry) => {
|
|
32
|
+
const parsed = checkpointItemSchema.safeParse(entry);
|
|
33
|
+
return parsed.success ? parsed.data : null;
|
|
34
|
+
}, MAX_CHECKPOINT_DONE);
|
|
35
|
+
const files = collect(input.files, (entry) => {
|
|
36
|
+
const parsed = checkpointFileSchema.safeParse(entry);
|
|
37
|
+
return parsed.success ? parsed.data : null;
|
|
38
|
+
}, MAX_CHECKPOINT_FILES);
|
|
39
|
+
const checks = collect(input.checks, (entry) => {
|
|
40
|
+
const parsed = checkpointCheckSchema.safeParse(entry);
|
|
41
|
+
return parsed.success ? parsed.data : null;
|
|
42
|
+
}, MAX_CHECKPOINT_CHECKS);
|
|
43
|
+
const decisions = collect(input.decisions, (entry) => {
|
|
44
|
+
const parsed = checkpointDecisionSchema.safeParse(entry);
|
|
45
|
+
return parsed.success ? parsed.data : null;
|
|
46
|
+
}, MAX_CHECKPOINT_DECISIONS);
|
|
47
|
+
if (!summary && !done.length && !text(input.next) && !files.length) {
|
|
48
|
+
return { ok: false, error: 'A checkpoint needs a summary, a next step, or what is done; write the handoff the next slice will read.' };
|
|
49
|
+
}
|
|
50
|
+
const reason = input.reason === undefined ? undefined : checkpointReasonSchema.safeParse(input.reason);
|
|
51
|
+
if (reason && !reason.success)
|
|
52
|
+
return { ok: false, error: 'reason is budget, stuck, compaction-thrash or requested.' };
|
|
53
|
+
return {
|
|
54
|
+
ok: true,
|
|
55
|
+
value: {
|
|
56
|
+
summary,
|
|
57
|
+
done,
|
|
58
|
+
next: text(input.next),
|
|
59
|
+
...(text(input.blocked) ? { blocked: text(input.blocked) } : {}),
|
|
60
|
+
files,
|
|
61
|
+
checks,
|
|
62
|
+
decisions,
|
|
63
|
+
...(reason && reason.success ? { reason: reason.data } : {})
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
/** The model's own checkpoint, filled in with this slice and why it was written. */
|
|
68
|
+
export function checkpointRecord(input, slice, fallbackReason) {
|
|
69
|
+
return { ...input, slice, reason: input.reason ?? fallbackReason };
|
|
70
|
+
}
|
|
71
|
+
/** The latest checkpoint a run recorded, if any. */
|
|
72
|
+
export function checkpointOf(events) {
|
|
73
|
+
const last = events.filter((event) => event.type === 'checkpoint').at(-1);
|
|
74
|
+
if (!last)
|
|
75
|
+
return null;
|
|
76
|
+
const parsed = runEventPayloadSchemas.checkpoint.safeParse(last.payload);
|
|
77
|
+
return parsed.success ? parsed.data : null;
|
|
78
|
+
}
|
|
79
|
+
const line = (value) => value.replace(/\s+/gu, ' ').trim();
|
|
80
|
+
/** The checkpoint as the next slice reads it. Bounded by its parts; the caller clips the whole. */
|
|
81
|
+
export function describeCheckpoint(checkpoint) {
|
|
82
|
+
const parts = [];
|
|
83
|
+
if (checkpoint.summary)
|
|
84
|
+
parts.push(`Where the last slice stopped: ${line(checkpoint.summary)}`);
|
|
85
|
+
if (checkpoint.done.length)
|
|
86
|
+
parts.push(`Done:\n${checkpoint.done.map((entry) => `- ${line(entry.item)}${entry.evidence.length ? ` (${entry.evidence.map(line).join('; ')})` : ''}`).join('\n')}`);
|
|
87
|
+
if (checkpoint.files.length)
|
|
88
|
+
parts.push(`Files touched:\n${checkpoint.files.map((entry) => `- ${line(entry.path)}${entry.why ? `: ${line(entry.why)}` : ''}`).join('\n')}`);
|
|
89
|
+
if (checkpoint.checks.length) {
|
|
90
|
+
const failing = checkpoint.checks.filter((entry) => !entry.ok);
|
|
91
|
+
const passing = checkpoint.checks.filter((entry) => entry.ok);
|
|
92
|
+
if (passing.length)
|
|
93
|
+
parts.push(`Checks that passed: ${passing.map((entry) => line(entry.command)).join('; ')}`);
|
|
94
|
+
if (failing.length)
|
|
95
|
+
parts.push(`Checks that failed:\n${failing.map((entry) => `- ${line(entry.command)}${entry.detail ? `: ${line(entry.detail)}` : ''}`).join('\n')}`);
|
|
96
|
+
}
|
|
97
|
+
if (checkpoint.decisions.length)
|
|
98
|
+
parts.push(`Decisions:\n${checkpoint.decisions.map((entry) => `- ${line(entry.what)}${entry.why ? `: ${line(entry.why)}` : ''}`).join('\n')}`);
|
|
99
|
+
if (checkpoint.blocked)
|
|
100
|
+
parts.push(`Blocked: ${line(checkpoint.blocked)}`);
|
|
101
|
+
if (checkpoint.next)
|
|
102
|
+
parts.push(`Next step: ${line(checkpoint.next)}`);
|
|
103
|
+
return parts.join('\n\n');
|
|
104
|
+
}
|
|
105
|
+
/** The checkpoint as the model reads it back after writing it. */
|
|
106
|
+
export function ackCheckpoint(checkpoint) {
|
|
107
|
+
const counts = [
|
|
108
|
+
checkpoint.done.length ? `${checkpoint.done.length} done` : '',
|
|
109
|
+
checkpoint.files.length ? `${checkpoint.files.length} files` : '',
|
|
110
|
+
checkpoint.checks.length ? `${checkpoint.checks.length} checks` : ''
|
|
111
|
+
].filter(Boolean).join(', ');
|
|
112
|
+
return `Checkpoint recorded for slice ${checkpoint.slice}${counts ? ` (${counts})` : ''}.${checkpoint.next ? ` Next: ${line(checkpoint.next)}` : ''}`;
|
|
113
|
+
}
|
|
114
|
+
export function checkpointTool(run, slice) {
|
|
115
|
+
return {
|
|
116
|
+
name: CHECKPOINT_WRITE,
|
|
117
|
+
label: 'Write the handoff checkpoint',
|
|
118
|
+
description: 'Record where this reply stopped and what the next slice must know. Call it when the harness asks for a handoff at the end of a work slice, or before you stop on purpose. ' +
|
|
119
|
+
'Include what is done (with the file or check that proves it), the single next step, anything blocking, the files you touched, the checks you ran with their outcome, and decisions the next slice must not revisit. ' +
|
|
120
|
+
'It records run state for the next slice; it does not change files and does not replace your reply to the user.',
|
|
121
|
+
parameters: (Type) => Type.Object({
|
|
122
|
+
summary: Type.Optional(Type.String({ description: 'Where this slice stopped, in 1 to 3 sentences (max 1200 characters)' })),
|
|
123
|
+
done: Type.Optional(Type.Array(Type.Object({
|
|
124
|
+
item: Type.String({ description: 'One finished step' }),
|
|
125
|
+
evidence: Type.Optional(Type.Array(Type.String({ description: 'A path, a check command, or a decision that proves it' }), { maxItems: 5 }))
|
|
126
|
+
}), { maxItems: MAX_CHECKPOINT_DONE })),
|
|
127
|
+
next: Type.Optional(Type.String({ description: 'The single next step (max 500 characters)' })),
|
|
128
|
+
blocked: Type.Optional(Type.String({ description: 'What stops progress, if anything (max 500 characters)' })),
|
|
129
|
+
files: Type.Optional(Type.Array(Type.Object({
|
|
130
|
+
path: Type.String(),
|
|
131
|
+
why: Type.Optional(Type.String({ description: 'Why this file matters to the next slice' }))
|
|
132
|
+
}), { maxItems: MAX_CHECKPOINT_FILES })),
|
|
133
|
+
checks: Type.Optional(Type.Array(Type.Object({
|
|
134
|
+
command: Type.String(),
|
|
135
|
+
ok: Type.Boolean(),
|
|
136
|
+
detail: Type.Optional(Type.String({ description: 'The failing line, when it failed' }))
|
|
137
|
+
}), { maxItems: MAX_CHECKPOINT_CHECKS })),
|
|
138
|
+
decisions: Type.Optional(Type.Array(Type.Object({
|
|
139
|
+
what: Type.String(),
|
|
140
|
+
why: Type.Optional(Type.String())
|
|
141
|
+
}), { maxItems: MAX_CHECKPOINT_DECISIONS }))
|
|
142
|
+
}),
|
|
143
|
+
readOnly: true,
|
|
144
|
+
planModeSafe: () => true,
|
|
145
|
+
execute: async (input) => {
|
|
146
|
+
const parsed = parseCheckpoint(input);
|
|
147
|
+
if (!parsed.ok)
|
|
148
|
+
return parsed.error;
|
|
149
|
+
const checkpoint = checkpointRecord(parsed.value, typeof slice === 'function' ? slice() : slice, 'requested');
|
|
150
|
+
run.emit('checkpoint', checkpoint);
|
|
151
|
+
return ackCheckpoint(checkpoint);
|
|
152
|
+
}
|
|
153
|
+
};
|
|
154
|
+
}
|