gentle-pi 3.7.0 → 4.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.
- package/README.md +37 -6
- package/assets/agents/gentle-ai-explore.md +4 -4
- package/assets/agents/gentle-ai-verify.md +6 -4
- package/assets/agents/gentle-ai-worker.md +7 -9
- package/assets/orchestrator-delegation.md +40 -41
- package/assets/orchestrator-memory.md +1 -22
- package/assets/orchestrator-skills.md +1 -1
- package/assets/orchestrator.md +12 -24
- package/assets/support/strict-tdd-verify.md +4 -266
- package/assets/support/strict-tdd.md +8 -360
- package/bin/gentle-shell.mjs +254 -29
- package/docs/delegated-verification.md +26 -1
- package/docs/gentle-agents-activity.md +24 -0
- package/docs/gentle-shell.md +92 -28
- package/docs/native-authority-architecture.md +2 -2
- package/docs/prompt-history.md +280 -0
- package/docs/readme-reference.md +112 -206
- package/docs/telemetry.md +1 -1
- package/docs/yolo-mode.md +86 -0
- package/extensions/child-context.ts +26 -0
- package/extensions/child-safety.ts +23 -0
- package/extensions/gentle-agents.ts +429 -262
- package/extensions/gentle-ai.ts +781 -683
- package/extensions/gentle-shell.ts +1375 -88
- package/extensions/gentle-stats.ts +101 -0
- package/extensions/gentle-todo.ts +18 -7
- package/extensions/history/atomic-write.ts +38 -0
- package/extensions/history/hide-prompts.ts +183 -0
- package/extensions/history/index.ts +1419 -0
- package/extensions/history/load-shared-history.ts +39 -0
- package/extensions/history/selector-helpers.ts +538 -0
- package/extensions/history/session-scan.ts +233 -0
- package/extensions/history/store.ts +1119 -0
- package/extensions/nan-provider.ts +6 -0
- package/extensions/quiet-tools.ts +179 -87
- package/extensions/resume-hint.ts +60 -0
- package/extensions/skill-registry.ts +16 -12
- package/extensions/startup-banner.ts +60 -31
- package/lib/agent-assets.ts +604 -0
- package/lib/agent-profile-pin.ts +12 -0
- package/lib/agents-message-delivery.ts +181 -0
- package/lib/agents-protocol.ts +60 -0
- package/lib/agents-runner.ts +96 -98
- package/lib/agents-view.ts +26 -3
- package/lib/agents-widget.ts +16 -9
- package/lib/append-system-prompt.ts +21 -0
- package/lib/bounded-writer-admission.ts +147 -0
- package/lib/card-style-policy.ts +60 -0
- package/lib/child-context-files.ts +166 -0
- package/lib/codemode-renderer.ts +185 -0
- package/lib/command-palette-catalog.ts +3 -9
- package/lib/command-palette.ts +25 -14
- package/lib/destructive-command-guard.ts +144 -0
- package/lib/gentle-ai-elapsed-store.ts +87 -0
- package/lib/gentle-ai-renderer.ts +196 -39
- package/lib/gentle-shell-launcher.ts +24 -13
- package/lib/gentle-shell-resume-hint.ts +176 -0
- package/lib/history-capture-policy.ts +95 -0
- package/lib/model-routing-authority.ts +5 -1
- package/lib/nan-provider.ts +227 -0
- package/lib/native-review-cli.ts +49 -108
- package/lib/odd-phase-inference.ts +231 -0
- package/lib/odd-phase.ts +141 -0
- package/lib/overlay-repaint.ts +26 -0
- package/lib/pi-tui-keys.ts +53 -0
- package/lib/review-candidate-view-owner.ts +67 -17
- package/lib/review-candidate-view.ts +112 -25
- package/lib/review-reminder-receipt.ts +48 -8
- package/lib/review-risk-assessment.ts +156 -11
- package/lib/review-sidebar-state.ts +223 -0
- package/lib/selection-engine.ts +515 -0
- package/lib/session-messaging-grants.ts +135 -0
- package/lib/session-worktree-registry.ts +14 -2
- package/lib/shell-bar.ts +179 -24
- package/lib/shell-card.ts +287 -18
- package/lib/shell-changes-view.ts +2 -1
- package/lib/shell-prompt.ts +102 -6
- package/lib/shell-sidebar-layout.ts +78 -14
- package/lib/shell-sidebar.ts +15 -1
- package/lib/shell-todo.ts +23 -13
- package/lib/shell-usage-view.ts +9 -4
- package/lib/shell-usage.ts +66 -10
- package/lib/stats-collector.ts +381 -0
- package/lib/stats-view.ts +431 -0
- package/lib/theme-customization.ts +52 -0
- package/lib/vim-editor-adapter.ts +379 -0
- package/lib/vim-normal-engine.ts +154 -0
- package/lib/vim-operator-engine.ts +416 -0
- package/lib/vim-policy.ts +49 -0
- package/lib/vim-visual-engine.ts +107 -0
- package/lib/visual-customization-policy.ts +108 -0
- package/lib/visual-customize-view.ts +330 -0
- package/lib/visual-profiles.ts +228 -0
- package/lib/yolo-session-policy.ts +240 -0
- package/package.json +20 -8
- package/runtime/gentle-shell-launcher.mjs +23 -12
- package/runtime/gentle-shell-resume-hint.mjs +177 -0
- package/runtime/native-review-cli.mjs +49 -108
- package/runtime/review-risk-assessment.mjs +154 -9
- package/scripts/build-runtime-modules.mjs +1 -0
- package/scripts/gentle-ai-installer.mjs +14 -13
- package/scripts/mirror-odd-routing.mjs +2 -2
- package/scripts/run-test-suite.mjs +76 -0
- package/scripts/test-packed-runner.mjs +31 -14
- package/scripts/verify-package-files.mjs +11 -22
- package/skills/branch-pr/SKILL.md +24 -52
- package/skills/chained-pr/SKILL.md +31 -15
- package/skills/chained-pr/references/chaining-details.md +31 -20
- package/skills/gentle-ai/SKILL.md +9 -15
- package/skills/issue-creation/SKILL.md +8 -2
- package/skills/issue-creation/references/delegated-workflow-actions.md +19 -0
- package/skills/work-unit-commits/SKILL.md +4 -3
- package/tests/agent-profiles.test.ts +18 -0
- package/tests/agents-fake-child.ts +2 -2
- package/tests/agents-message-delivery.test.ts +106 -0
- package/tests/agents-protocol.test.ts +40 -0
- package/tests/agents-runner.test.ts +378 -89
- package/tests/agents-view-thread-identity.test.ts +169 -0
- package/tests/agents-view.test.ts +8 -2
- package/tests/agents-widget.test.ts +154 -15
- package/tests/append-system-prompt-route.test.ts +160 -0
- package/tests/append-system-prompt.test.ts +46 -0
- package/tests/artifact-language.test.ts +19 -213
- package/tests/ask-user-question.test.ts +44 -1
- package/tests/asset-installation-runtime.test.ts +5 -16
- package/tests/autonomous-guard.test.ts +69 -1
- package/tests/bounded-writer-admission.test.ts +95 -0
- package/tests/branch-pr-skill.test.ts +43 -0
- package/tests/card-style-policy.test.ts +55 -0
- package/tests/chained-pr-skill.test.ts +124 -0
- package/tests/child-context-files.test.ts +255 -0
- package/tests/child-safety.test.ts +82 -0
- package/tests/codemode-rendering.test.ts +491 -0
- package/tests/command-palette.test.ts +39 -3
- package/tests/delegated-key-learnings-contract.test.ts +0 -76
- package/tests/destructive-command-guard.test.ts +84 -0
- package/tests/devbinary/native-review-parity.devtest.ts +170 -2
- package/tests/devbinary/non-git-subagent-bootstrap.devtest.ts +193 -0
- package/tests/fixtures/stats/sessions/--work-alpha--/2026-09-28T10-00-00-000Z_aaa.jsonl +7 -0
- package/tests/fixtures/stats/sessions/--work-alpha--/2026-09-29T23-00-00-000Z_bbb.jsonl +3 -0
- package/tests/fixtures/stats/sessions/--work-alpha--/2026-09-30T08-00-00-000Z_ddd.jsonl +2 -0
- package/tests/fixtures/stats/sessions/--work-alpha--/2026-09-30T09-00-00-000Z_eee.jsonl +3 -0
- package/tests/fixtures/stats/sessions/--work-alpha--/run-1/session.jsonl +2 -0
- package/tests/fixtures/stats/sessions/--work-beta--/2026-09-01T12-00-00-000Z_ccc.jsonl +2 -0
- package/tests/fixtures/stats/user-pi/sessions/--work-alpha--/2026-09-28T10-00-00-000Z_aaa.jsonl +2 -0
- package/tests/fixtures/stats/user-pi/sessions/--work-alpha--/2026-09-29T23-00-00-000Z_bbb.jsonl +4 -0
- package/tests/fixtures/stats/user-pi/sessions/--work-gamma--/2026-09-20T09-00-00-000Z_fff.jsonl +3 -0
- package/tests/generic-agent-tools.test.ts +54 -0
- package/tests/gentle-agents.test.ts +1047 -252
- package/tests/gentle-ai-binary.test.ts +3 -3
- package/tests/gentle-ai-elapsed-store.test.ts +68 -0
- package/tests/gentle-ai-installer.test.ts +68 -54
- package/tests/gentle-ai-renderer.test.ts +487 -8
- package/tests/gentle-ai.test.ts +288 -65
- package/tests/gentle-card-text.ts +2 -1
- package/tests/gentle-shell-bin.test.ts +651 -114
- package/tests/gentle-shell-launcher.test.ts +99 -52
- package/tests/gentle-shell-resume-hint.test.ts +270 -0
- package/tests/gentle-shell.test.ts +3839 -187
- package/tests/gentle-stats.test.ts +152 -0
- package/tests/gentle-theme.test.ts +4 -1
- package/tests/gentle-todo.test.ts +80 -8
- package/tests/history-atomic-write.test.ts +57 -0
- package/tests/history-capture-policy.test.ts +102 -0
- package/tests/history-command-registration.test.ts +164 -0
- package/tests/history-dedupe-entries.test.ts +123 -0
- package/tests/history-delete-backfill.test.ts +190 -0
- package/tests/history-delete-confirm.test.ts +460 -0
- package/tests/history-dispatch.test.ts +180 -0
- package/tests/history-drain-hidden.test.ts +110 -0
- package/tests/history-drain-order.test.ts +98 -0
- package/tests/history-expanded-globals.test.ts +62 -0
- package/tests/history-gc.test.ts +832 -0
- package/tests/history-header-layout.test.ts +265 -0
- package/tests/history-hide-prompts.test.ts +275 -0
- package/tests/history-lazy-windowing.test.ts +508 -0
- package/tests/history-legacy-migrate-v2.test.ts +297 -0
- package/tests/history-load-shared-history.test.ts +53 -0
- package/tests/history-max-results-cap.test.ts +76 -0
- package/tests/history-multi-reader.test.ts +203 -0
- package/tests/history-off-path.test.ts +170 -0
- package/tests/history-openflow-integration.test.ts +173 -0
- package/tests/history-overlay-margin.test.ts +326 -0
- package/tests/history-preview-layout.test.ts +93 -0
- package/tests/history-registry.test.ts +143 -0
- package/tests/history-scope-delete.test.ts +411 -0
- package/tests/history-search-caret-keys.test.ts +142 -0
- package/tests/history-seed-bootstrap.test.ts +170 -0
- package/tests/history-seed-regen.test.ts +129 -0
- package/tests/history-selector-windowing.test.ts +94 -0
- package/tests/history-session-scan-directory.test.ts +87 -0
- package/tests/history-session-scan-extract.test.ts +583 -0
- package/tests/history-session-writer.test.ts +351 -0
- package/tests/history-store-paths.test.ts +79 -0
- package/tests/history-tombstone-exact.test.ts +139 -0
- package/tests/history-wheel-mouse.test.ts +242 -0
- package/tests/inprocess-reviewer.test.ts +29 -19
- package/tests/issue-creation-skill.test.ts +61 -0
- package/tests/model-routing-authority.test.ts +16 -0
- package/tests/nan-provider.test.ts +471 -0
- package/tests/native-review-capability-contract.test.ts +7 -1
- package/tests/native-review-cli.test.ts +6 -120
- package/tests/native-review-parity-runtime.test.ts +100 -3
- package/tests/odd-integration.test.ts +67 -0
- package/tests/odd-phase-inference.test.ts +213 -0
- package/tests/odd-phase-loader.test.ts +253 -0
- package/tests/odd-phase.test.ts +307 -0
- package/tests/odd-routing-canonical-ratchet.test.ts +11 -6
- package/tests/odd-routing-contract.test.ts +86 -35
- package/tests/orchestrator-budget.test.ts +14 -39
- package/tests/orchestrator-rdd-ownership.test.ts +3 -3
- package/tests/overlay-repaint.test.ts +74 -0
- package/tests/package-manifest.test.ts +252 -115
- package/tests/packed-runner-owned-path.test.ts +46 -0
- package/tests/persona-single-channel.test.ts +6 -6
- package/tests/provider-defect-handoff.test.ts +3 -11
- package/tests/quiet-bash-runtime.test.ts +76 -0
- package/tests/quiet-tool-rendering.test.ts +409 -184
- package/tests/rdd-aware-verification-contract.test.ts +76 -1
- package/tests/rdd-status-line.test.ts +9 -4
- package/tests/resume-hint-extension.test.ts +122 -0
- package/tests/review-agent-end-preflight.test.ts +176 -12
- package/tests/review-candidate-owner-retry.test.ts +22 -1
- package/tests/review-candidate-view.test.ts +298 -0
- package/tests/review-contract-prompt.test.ts +108 -43
- package/tests/review-controller-lock-status.test.ts +0 -1
- package/tests/review-controller-native-routing.test.ts +611 -5
- package/tests/review-controller-workspace-root.test.ts +163 -4
- package/tests/review-host-relay-routing.test.ts +338 -2
- package/tests/review-integration-v2-forward.test.ts +200 -0
- package/tests/review-ledger-contract.test.ts +10 -34
- package/tests/review-reminder-receipt.test.ts +47 -1
- package/tests/review-risk-assessment.test.ts +498 -6
- package/tests/review-sidebar-state.test.ts +402 -0
- package/tests/run-test-suite.test.ts +124 -0
- package/tests/runtime-harness.mjs +145 -786
- package/tests/runtime-metrics-children.test.ts +16 -23
- package/tests/selection-engine.test.ts +421 -0
- package/tests/session-messaging-grants.test.ts +255 -0
- package/tests/session-worktree-registry.test.ts +77 -0
- package/tests/shell-bar.test.ts +382 -1
- package/tests/shell-card.test.ts +353 -1
- package/tests/shell-changes-view.test.ts +52 -0
- package/tests/shell-prompt.test.ts +94 -2
- package/tests/shell-sidebar-layout.test.ts +325 -21
- package/tests/shell-sidebar-scroll-benchmark.test.ts +255 -0
- package/tests/shell-todo.test.ts +87 -1
- package/tests/shell-usage-view.test.ts +27 -0
- package/tests/shell-usage.test.ts +73 -0
- package/tests/skill-registry.test.ts +50 -1
- package/tests/startup-banner.test.ts +130 -2
- package/tests/stats-collector.test.ts +195 -0
- package/tests/stats-view.test.ts +202 -0
- package/tests/telemetry-trigger.test.ts +81 -20
- package/tests/theme-customization.test.ts +72 -0
- package/tests/vim-editor-adapter-host-resolution.test.ts +37 -0
- package/tests/vim-editor-adapter.test.ts +804 -0
- package/tests/vim-normal-engine.test.ts +101 -0
- package/tests/vim-operator-engine.test.ts +215 -0
- package/tests/vim-policy.test.ts +19 -0
- package/tests/vim-visual-engine.test.ts +52 -0
- package/tests/visual-customization-policy.test.ts +110 -0
- package/tests/visual-customize-view.test.ts +418 -0
- package/tests/visual-profiles.test.ts +87 -0
- package/tests/yolo-customize.test.ts +256 -0
- package/tests/yolo-mode-runtime.test.ts +161 -0
- package/tests/yolo-mode.test.ts +261 -0
- package/tests/yolo-session-policy.test.ts +59 -0
- package/themes/Gentle.json +2 -1
- package/themes/Gentleman-Cute.json +2 -1
- package/themes/Gentleman-Sexy.json +2 -1
- package/assets/agents/sdd-apply.md +0 -159
- package/assets/agents/sdd-archive.md +0 -228
- package/assets/agents/sdd-design.md +0 -49
- package/assets/agents/sdd-explore.md +0 -48
- package/assets/agents/sdd-init.md +0 -56
- package/assets/agents/sdd-onboard.md +0 -52
- package/assets/agents/sdd-proposal.md +0 -64
- package/assets/agents/sdd-remediate.md +0 -37
- package/assets/agents/sdd-research.md +0 -49
- package/assets/agents/sdd-spec.md +0 -192
- package/assets/agents/sdd-status.md +0 -54
- package/assets/agents/sdd-tasks.md +0 -108
- package/assets/agents/sdd-verify.md +0 -124
- package/assets/chains/sdd-full.chain.md +0 -83
- package/assets/chains/sdd-plan.chain.md +0 -56
- package/assets/chains/sdd-verify.chain.md +0 -43
- package/assets/sdd-orchestrator-workflow.md +0 -319
- package/assets/support/sdd-status-contract.md +0 -77
- package/docs/assets/diagrams/sdd-cycle.svg +0 -14
- package/extensions/sdd-init.ts +0 -816
- package/lib/openspec-deltas.ts +0 -156
- package/lib/sdd-preflight.ts +0 -1066
- package/lib/sdd-research-capabilities.ts +0 -94
- package/lib/sdd-status.ts +0 -26
- package/tests/fixtures/legacy/sdd-research-v2.5.0.md +0 -54
- package/tests/fixtures/native-review-cli/v2.1.3/bind-sdd.json +0 -25
- package/tests/fixtures/v0.10.7/assets/agents/sdd-apply.md +0 -132
- package/tests/openspec-deltas.test.ts +0 -209
- package/tests/sdd-agent-tools.test.ts +0 -156
- package/tests/sdd-archive-replay.test.ts +0 -82
- package/tests/sdd-classical-continuation.test.ts +0 -74
- package/tests/sdd-execution-routing-contract.test.ts +0 -44
- package/tests/sdd-managed-runtime-settlement.test.ts +0 -155
- package/tests/sdd-native-managed-uptake.test.ts +0 -243
- package/tests/sdd-no-attempts-contract.test.ts +0 -15
- package/tests/sdd-odd-integration.test.ts +0 -33
- package/tests/sdd-optional-research.test.ts +0 -124
- package/tests/sdd-planning-routing-contract.test.ts +0 -45
- package/tests/sdd-preflight-rpc-input.test.ts +0 -125
- package/tests/sdd-preflight.test.ts +0 -541
- package/tests/sdd-research-capabilities.test.ts +0 -114
- package/tests/sdd-research-live.test.ts +0 -241
- package/tests/sdd-selection-transport.test.ts +0 -653
- package/tests/sdd-status.test.ts +0 -9
- package/tests/sdd-task-truth.test.ts +0 -43
|
@@ -1,27 +1,29 @@
|
|
|
1
|
-
import { fileURLToPath } from "node:url";
|
|
2
1
|
import { agentsViewKey, agentsCollapseKey, agentsStopKey } from "../lib/agents-keys.ts";
|
|
3
|
-
import { extractParentConfirmedSddPreflightContext, getPackageAssetOwner, isParentConfirmedSddPreflightContext, SHIPPED_SDD_AGENT_NAMES } from "../lib/sdd-preflight.ts";
|
|
4
|
-
import { NativeReviewCliV216, createNodeExecFileAdapter, decodeNativeSddStatusV2, type NativeReviewCli } from "../lib/native-review-cli.ts";
|
|
5
2
|
import { spawn } from "node:child_process";
|
|
6
3
|
import { recordReviewMutation } from "../lib/review-reminder-receipt.ts";
|
|
7
4
|
import { SESSION_CHANGE_RELAY } from "../lib/session-changes.ts";
|
|
8
5
|
import { publishForeignSessionChange } from "../lib/session-change-capture.ts";
|
|
9
6
|
import { SessionWorktreeRegistry, resolveSessionWorktree, type WorktreeResolver } from "../lib/session-worktree-registry.ts";
|
|
10
7
|
import { ForeignTargetGrants } from "../lib/foreign-target-grants.ts";
|
|
11
|
-
import {
|
|
8
|
+
import { MESSAGING_REASON_MAX_UTF8_BYTES, MESSAGING_REASON_MIN_CHARACTERS, normalizeMessagingReason, SessionMessagingGrants } from "../lib/session-messaging-grants.ts";
|
|
9
|
+
import { existsSync, mkdirSync, readFileSync, realpathSync } from "node:fs";
|
|
12
10
|
import { randomUUID } from "node:crypto";
|
|
13
11
|
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
14
12
|
import os from "node:os";
|
|
15
|
-
import { join, resolve, isAbsolute
|
|
16
|
-
import {
|
|
13
|
+
import { join, resolve, isAbsolute } from "node:path";
|
|
14
|
+
import { fileURLToPath } from "node:url";
|
|
15
|
+
import { keyHint, type ExtensionAPI, type ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
17
16
|
import { Text, type TUI } from "@earendil-works/pi-tui";
|
|
18
17
|
import { invalidateSidebar } from "../lib/shell-sidebar-layout.ts";
|
|
18
|
+
import { VISUAL_SETTINGS_CHANGED } from "../lib/shell-sidebar.ts";
|
|
19
|
+
import { resolveVisualSettings } from "../lib/visual-customization-policy.ts";
|
|
19
20
|
import { createCompletionQueue } from "../lib/agents-completion-delivery.ts";
|
|
20
|
-
import {
|
|
21
|
+
import { createAgentMessageQueue, type PendingAgentMessage } from "../lib/agents-message-delivery.ts";
|
|
22
|
+
import { AGENT_MODE, discoverAgents, formatModelRef, loadAgentsConfig, resolveAgentProfile, withPinnedModelProfiles, type AgentDefinition, type AgentMode } from "../lib/agents-config.ts";
|
|
21
23
|
import { resolveBackgroundSubagentsPolicy } from "../lib/background-subagents-policy.ts";
|
|
22
24
|
import { installBackgroundCacheWarming } from "../lib/background-cache-warming.ts";
|
|
23
25
|
import { isFinished, TASK_EVENT, TASK_STATUS, TaskStore, type AskRequest, type TaskRecord } from "../lib/agents-protocol.ts";
|
|
24
|
-
import { AgentRunner, piCommand, abortReasonText,
|
|
26
|
+
import { AgentRunner, piCommand, abortReasonText, type AskAnswer, type RunnerDeps, type TaskRequest } from "../lib/agents-runner.ts";
|
|
25
27
|
import { ChildMessenger, type IpcEndpoint } from "../lib/agents-messaging.ts";
|
|
26
28
|
import { ActiveSessionClient, ActiveSessionListener, SessionPresenceRegistry, type PresenceRecord, type ReceivedNotification, type SentNotification, type SessionPresenceCandidate } from "../lib/agents-session-transport.ts";
|
|
27
29
|
import { WindowsActiveSessionClient, WindowsActiveSessionListener, WindowsSessionPresenceRegistry, type WindowsSessionRegistryPhaseObserver } from "../lib/windows-session-transport.ts";
|
|
@@ -30,6 +32,7 @@ import { inheritedUnsafeGitEnvironmentKeys } from "../lib/review-repository.ts";
|
|
|
30
32
|
import { historyDir, loadHistory, loadStoredTask, pruneHistory, saveTask } from "../lib/agents-history.ts";
|
|
31
33
|
import { sessionToMarkdown } from "../lib/agents-transcript.ts";
|
|
32
34
|
import { AgentsView } from "../lib/agents-view.ts";
|
|
35
|
+
import { withOverlayRepaint } from "../lib/overlay-repaint.ts";
|
|
33
36
|
import { PresencePublisher } from "../lib/orchestrator-presence.ts";
|
|
34
37
|
import { createRpcActivityPublisher, type RpcActivityPublisher } from "../lib/agents-rpc-publisher.ts";
|
|
35
38
|
import { isInteractiveRpcHost } from "../lib/rpc-host.ts";
|
|
@@ -38,8 +41,8 @@ import { AGENTS_GLYPH, renderAgentsCard, widgetExpiryMs, widgetRows } from "../l
|
|
|
38
41
|
import { CARD_TONE, renderCard } from "../lib/shell-card.ts";
|
|
39
42
|
import { openInExternalEditor } from "./gentle-shell.ts";
|
|
40
43
|
import { resolveGentlePiAgentHome, gentlePiConfigHome } from "../lib/agent-home.ts";
|
|
41
|
-
import { resolveProfilePin } from "../lib/agent-profile-pin.ts";
|
|
42
|
-
import {
|
|
44
|
+
import { resolveProfilePin, resolveUnversionedProjectProfile } from "../lib/agent-profile-pin.ts";
|
|
45
|
+
import { allowedEditSurfaces, isDevelopmentSurface, isGenericBoundedWriter, prepareBoundSessionRepository, rejectUnscopedBoundedWriterDispatch, safeBootstrapDirectory, sessionRepositoryAuthority } from "../lib/bounded-writer-admission.ts";
|
|
43
46
|
import { CHILD_METRICS_EVENT, CHILD_METRICS_REVOKED, childEvent, launchSelection, type LaunchSelection } from "../lib/runtime-metrics-children.ts";
|
|
44
47
|
import { runtimeMetricsEnvAllows, type RuntimeMetricsPolicyDeps } from "../lib/runtime-metrics-policy.ts";
|
|
45
48
|
|
|
@@ -57,125 +60,14 @@ export const AGENTS_STALE_RESULT_TYPE = "gentle-agents.stale-result";
|
|
|
57
60
|
const RENDER_COALESCE_MS = 400;
|
|
58
61
|
const CLOCK_TICK_MS = 1000;
|
|
59
62
|
const TOOL_PREFIX = "subagent_";
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
const
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
"sdd-archive": "archive",
|
|
67
|
-
} as const;
|
|
68
|
-
|
|
69
|
-
function sddPhaseForAgent(name: string): SddChangeSelection["phase"] | undefined {
|
|
70
|
-
return Object.hasOwn(SDD_PHASE_BY_AGENT, name) ? SDD_PHASE_BY_AGENT[name as keyof typeof SDD_PHASE_BY_AGENT] : undefined;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
function parseSddChange(value: unknown, agentName: string): SddChangeSelection | undefined {
|
|
74
|
-
if (value === undefined) return undefined;
|
|
75
|
-
const expectedPhase = sddPhaseForAgent(agentName);
|
|
76
|
-
if (!expectedPhase) throw new Error("sdd_change is allowed only for SDD apply, verify, or archive agents.");
|
|
77
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("sdd_change must be an object with changeName, workspaceRoot, and phase.");
|
|
78
|
-
const selection = value as Record<string, unknown>;
|
|
79
|
-
const keys = Object.keys(selection).sort();
|
|
80
|
-
if (keys.join(",") !== (expectedPhase === "remediate" ? "changeName,failedEvidenceRevision,phase,workspaceRoot" : "changeName,phase,workspaceRoot") ||
|
|
81
|
-
typeof selection.changeName !== "string" || selection.changeName.length === 0 ||
|
|
82
|
-
typeof selection.workspaceRoot !== "string" || selection.workspaceRoot.length === 0 ||
|
|
83
|
-
selection.phase !== expectedPhase) {
|
|
84
|
-
throw new Error("sdd_change must contain only a non-empty changeName, workspaceRoot, and the agent's matching phase.");
|
|
85
|
-
}
|
|
86
|
-
if (expectedPhase === "remediate" && (typeof selection.failedEvidenceRevision !== "string" || !/^sha256:[0-9a-f]{64}$/.test(selection.failedEvidenceRevision))) throw new Error("Invalid remediation revision");
|
|
87
|
-
return { changeName: selection.changeName, workspaceRoot: selection.workspaceRoot, phase: expectedPhase, ...(expectedPhase === "remediate" ? { failedEvidenceRevision: selection.failedEvidenceRevision as string } : {}) };
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
const REMEDIATION_SCHEMA = {
|
|
91
|
-
type: "object", additionalProperties: false, required: ["plan"],
|
|
92
|
-
properties: {
|
|
93
|
-
plan: { type: "object", additionalProperties: false, required: ["cwd", "commands", "runtimeHarness", "rollback"], properties: {
|
|
94
|
-
editPaths: { type: "array", maxItems: 32, items: { type: "string" }, description: "Exact canonical files requested for this launch; no entry means no edit/write authority." }, cwd: { type: "string" }, commands: { type: "array", minItems: 1, maxItems: 16, items: { type: "string" } },
|
|
95
|
-
runtimeHarness: { type: "object", additionalProperties: false, description: "Exactly one concrete command or prior concrete naReason containing because.", properties: { command: { type: "string" }, naReason: { type: "string" } } },
|
|
96
|
-
rollback: { type: "object", additionalProperties: false, required: ["boundary", "command"], properties: { boundary: { type: "string" }, command: { type: "string" } } },
|
|
97
|
-
} },
|
|
63
|
+
// Wakes an idle parent after child content was stored as a custom message.
|
|
64
|
+
// It names itself as automated so the model never attributes it to the human.
|
|
65
|
+
const PARENT_WAKE_TEXT = "[System-generated Gentle Agents notification, not written by the user] Subagent output was delivered to this session above. Review it and continue.";
|
|
66
|
+
// How long a dispatched wake may take to start a parent run before a later
|
|
67
|
+
// delivery may send another one.
|
|
68
|
+
export const PARENT_WAKE_GRACE_MS = 30_000;
|
|
98
69
|
|
|
99
|
-
|
|
100
|
-
};
|
|
101
|
-
|
|
102
|
-
export async function confirmRemediationScope(plan: RemediationPlan, action: Record<string, unknown>, context?: Pick<ExtensionContext, "hasUI" | "ui">): Promise<RemediationScope> {
|
|
103
|
-
const cwd = plan.cwd, roots = action.allowedEditRoots;
|
|
104
|
-
if (realpathSync(cwd) !== cwd || action.workspaceRoot !== cwd || !Array.isArray(roots) || !roots.every(root => typeof root === "string" && isAbsolute(root) && canonicalArtifactPath(root) === root)) throw new Error("Remediation native scope mismatch");
|
|
105
|
-
const inside = (path: string, root: string) => path === root || path.startsWith(`${root}${sep}`);
|
|
106
|
-
const editPaths = plan.editPaths ?? [];
|
|
107
|
-
if (!Array.isArray(editPaths) || editPaths.length > 32 || new Set(editPaths).size !== editPaths.length) throw new Error("Ambiguous remediation paths");
|
|
108
|
-
for (const path of editPaths) {
|
|
109
|
-
if (typeof path !== "string" || !isAbsolute(path) || resolve(path) !== path || /[*?\[\]{}]/.test(path) || canonicalArtifactPath(path) !== path || !inside(path, cwd) || !roots.some(root => inside(path, root))) throw new Error("Remediation path outside canonical native scope");
|
|
110
|
-
if (existsSync(path) && !lstatSync(path).isFile()) throw new Error("Remediation requires exact file paths, not directories");
|
|
111
|
-
}
|
|
112
|
-
const scope: RemediationScope = { cwd, editPaths: [...editPaths], commands: plannedCommands(plan), allowedEditRoots: [...roots] };
|
|
113
|
-
if (!context?.hasUI || !context.ui?.confirm || await context.ui.confirm("Authorize one remediation launch", `Canonical worktree: ${cwd}\nNative allowed roots: ${JSON.stringify(roots)}\nExact edit/write files: ${JSON.stringify(editPaths)}\nExact invocations (not a shell sandbox):\n${scope.commands.map((command, slot) => `${slot}: cwd=${JSON.stringify(cwd)} command=${JSON.stringify(command)}`).join("\n")}`) !== true) throw new Error("Remediation requires fresh human authorization; no actor started");
|
|
114
|
-
if (realpathSync(cwd) !== cwd || editPaths.some(path => canonicalArtifactPath(path) !== path)) throw new Error("Remediation scope changed during authorization");
|
|
115
|
-
return scope;
|
|
116
|
-
}
|
|
117
|
-
export function remediationToolAllowed(scope: RemediationScope | undefined, cwd: string, tool: string, input: Record<string, unknown>): boolean {
|
|
118
|
-
try {
|
|
119
|
-
if (!scope || scope.cwd !== cwd || canonicalArtifactPath(cwd) !== cwd) return false;
|
|
120
|
-
if (tool === "bash") return typeof input.command === "string" && scope.commands.includes(input.command);
|
|
121
|
-
if (["edit", "write"].includes(tool)) {
|
|
122
|
-
if (typeof input.path !== "string") return false;
|
|
123
|
-
const path = resolve(cwd, input.path);
|
|
124
|
-
return scope.editPaths.includes(path) && canonicalArtifactPath(path) === path && scope.allowedEditRoots.some(root => path === root || path.startsWith(`${root}${sep}`));
|
|
125
|
-
}
|
|
126
|
-
if (["read", "grep", "find"].includes(tool)) {
|
|
127
|
-
if (input.path === undefined) return true;
|
|
128
|
-
if (typeof input.path !== "string" || !input.path || isAbsolute(input.path)) return false;
|
|
129
|
-
const path = resolve(cwd, input.path);
|
|
130
|
-
return canonicalArtifactPath(path) === path && (path === cwd || path.startsWith(`${cwd}${sep}`));
|
|
131
|
-
}
|
|
132
|
-
return tool === "subagent_parent_message";
|
|
133
|
-
} catch { return false; }
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
export async function admitManagedRemediation(request: TaskRequest, input: unknown, native: NativeReviewCli, context?: Pick<ExtensionContext, "hasUI" | "ui">): Promise<Partial<TaskRequest>> {
|
|
137
|
-
const selected = request.sddChange;
|
|
138
|
-
const asset = fileURLToPath(new URL("../assets/agents/sdd-remediate.md", import.meta.url));
|
|
139
|
-
if (request.agent.name !== "sdd-remediate" || selected?.phase !== "remediate" || selected.workspaceRoot !== request.cwd || !native.sddStatus || getPackageAssetOwner("agents/sdd-remediate.md") !== "sdd" || !existsSync(request.agent.filePath) ) throw new Error("Unsupported managed remediation owner/asset or native capability; install matching package assets before retrying");
|
|
140
|
-
const owned = parseAgentDefinition(readFileSync(asset, "utf8"), asset, "global");
|
|
141
|
-
const installed = parseAgentDefinition(readFileSync(request.agent.filePath, "utf8"), request.agent.filePath, request.agent.scope);
|
|
142
|
-
if (!("instructions" in owned) || !("instructions" in installed) || [request.agent, installed].some(agent => agent.instructions !== owned.instructions || JSON.stringify(agent.tools) !== JSON.stringify(owned.tools))) throw new Error("Unsupported remediation actor content; install matching managed assets");
|
|
143
|
-
const value = input as { plan?: unknown };
|
|
144
|
-
const plan = parseRemediationPlan(value?.plan, request.cwd);
|
|
145
|
-
const status = decodeNativeSddStatusV2(await native.sddStatus({ workspaceRoot: request.cwd, changeName: selected.changeName }), selected);
|
|
146
|
-
// Legacy remediation status carries the verification failure being repaired;
|
|
147
|
-
// that is not permission to bypass a missing native edit grant.
|
|
148
|
-
if (status.blockedReasons.some(reason => reason.includes("edit_authority_missing")) || status.nextRecommended !== "remediate" || !status.phaseInstructions?.remediate || status.remediationState?.failedEvidenceRevision !== selected.failedEvidenceRevision) throw new Error("Stale remediation selection; refresh native status");
|
|
149
|
-
const scope = await confirmRemediationScope(plan, status.actionContext, context);
|
|
150
|
-
return { sddRemediation: { scope, failedEvidenceRevision: selected.failedEvidenceRevision!, plan } };
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
// Installed only for the admitted remediation child. Stock shell execution,
|
|
154
|
-
// cancellation, truncation and rendering remain owned by the SDK definition.
|
|
155
|
-
export function remediationBash(cwd: string, operations: BashOperations = createLocalBashOperations(), scope?: RemediationScope) {
|
|
156
|
-
const captured = new Map<string, { toolCallId: string; command: string; cwd: string; exitCode: number | null }>();
|
|
157
|
-
const remaining = [...(scope?.commands ?? [])], used = new Set<string>();
|
|
158
|
-
const stock = createBashToolDefinition(cwd);
|
|
159
|
-
return {
|
|
160
|
-
definition: { ...stock, async execute(...input: Parameters<typeof stock.execute>) {
|
|
161
|
-
const [id, args, signal, onUpdate, ctx] = input;
|
|
162
|
-
const slot = remaining.indexOf(args.command);
|
|
163
|
-
if (!remediationToolAllowed(scope, ctx?.cwd ?? cwd, "bash", args) || slot < 0 || used.has(id)) throw new Error("Bash invocation is outside this launch human authorization");
|
|
164
|
-
remaining.splice(slot, 1); used.add(id);
|
|
165
|
-
const definition = createBashToolDefinition(cwd, { operations: { exec: async (command, directory, options) => {
|
|
166
|
-
const result = await operations.exec(command, directory, options);
|
|
167
|
-
if (captured.size < 32) captured.set(id, { toolCallId: id, command, cwd: directory, exitCode: result.exitCode });
|
|
168
|
-
return result;
|
|
169
|
-
} } });
|
|
170
|
-
return definition.execute(id, args, signal, onUpdate, ctx);
|
|
171
|
-
} },
|
|
172
|
-
result(event: { toolCallId: string; details?: unknown }) {
|
|
173
|
-
const observation = captured.get(event.toolCallId);
|
|
174
|
-
captured.delete(event.toolCallId);
|
|
175
|
-
return observation ? { details: { ...(event.details as object ?? {}), remediationCommand: observation } } : undefined;
|
|
176
|
-
},
|
|
177
|
-
};
|
|
178
|
-
}
|
|
70
|
+
const retiredSddAgent = (name: string): boolean => /^sdd(?:-|$)/.test(name);
|
|
179
71
|
|
|
180
72
|
export interface SessionTransportRegistry {
|
|
181
73
|
list(excludeSessionId?: string): Promise<readonly SessionPresenceCandidate[]>;
|
|
@@ -202,7 +94,6 @@ export interface SessionTransportFactory {
|
|
|
202
94
|
}
|
|
203
95
|
|
|
204
96
|
export interface AgentsDeps extends RunnerDeps {
|
|
205
|
-
nativeSdd?: NativeReviewCli;
|
|
206
97
|
home: string;
|
|
207
98
|
agentHome?: string;
|
|
208
99
|
childIpc?: IpcEndpoint;
|
|
@@ -212,6 +103,22 @@ export interface AgentsDeps extends RunnerDeps {
|
|
|
212
103
|
runtimeMetricsPolicy?: RuntimeMetricsPolicyDeps;
|
|
213
104
|
metricsNow?: () => number;
|
|
214
105
|
metricsSchedule?: RunnerDeps["schedule"];
|
|
106
|
+
// Extensions every child loads with --extension (gentle-shell#1587).
|
|
107
|
+
childExtensionPaths?: string[];
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// gentle-shell#1587: children do not load the gentle-pi package in the
|
|
111
|
+
// isolated Gentle Shell home, so context filtering and destructive-command
|
|
112
|
+
// safety are passed to every child explicitly. Missing files are omitted;
|
|
113
|
+
// installations must include both entries to provide the delegated boundary.
|
|
114
|
+
export function childContextExtensionPaths(exists: (path: string) => boolean = existsSync): string[] {
|
|
115
|
+
try {
|
|
116
|
+
return ["./child-context.ts", "./child-safety.ts"]
|
|
117
|
+
.map((path) => fileURLToPath(new URL(path, import.meta.url)))
|
|
118
|
+
.filter(exists);
|
|
119
|
+
} catch {
|
|
120
|
+
return [];
|
|
121
|
+
}
|
|
215
122
|
}
|
|
216
123
|
|
|
217
124
|
export function agentRuntimePaths(home: string, agentHome = join(home, ".pi", "agent")): { sessions: string; transcripts: string } {
|
|
@@ -234,9 +141,11 @@ const defaultDeps = (env: NodeJS.ProcessEnv): AgentsDeps => ({
|
|
|
234
141
|
return () => clearTimeout(timer);
|
|
235
142
|
},
|
|
236
143
|
pi: piCommand(),
|
|
144
|
+
resolvePi: () => piCommand(),
|
|
237
145
|
home: os.homedir(),
|
|
238
146
|
resolveWorktree: resolveSessionWorktree,
|
|
239
147
|
env,
|
|
148
|
+
childExtensionPaths: childContextExtensionPaths(),
|
|
240
149
|
});
|
|
241
150
|
|
|
242
151
|
export function agentsEnabled(env: NodeJS.ProcessEnv = process.env): boolean {
|
|
@@ -322,6 +231,22 @@ function text(value: string, details: Record<string, unknown> = {}, terminate =
|
|
|
322
231
|
return { content: [{ type: "text", text: value }], details, ...(terminate ? { terminate: true } : {}) };
|
|
323
232
|
}
|
|
324
233
|
|
|
234
|
+
// gentle-pi#1175: the writer profile the runtime resolved for this task,
|
|
235
|
+
// carried on its review-mutation receipt so ASSESS never trusts a model
|
|
236
|
+
// re-declaration. `task.model` is the child's reported model once get_state
|
|
237
|
+
// answers, or the launch profile's model. An inherited model is recorded as
|
|
238
|
+
// the unresolved `formatModelRef(undefined)` placeholder; the parent's current
|
|
239
|
+
// model is not reliable evidence (the child resolves its own default), so it is
|
|
240
|
+
// omitted and ASSESS treats the writer as unknown, i.e. small.
|
|
241
|
+
function runtimeWriterProfile(task: TaskRecord): { writerModelId?: string; writerEffort?: string } {
|
|
242
|
+
const modelId = typeof task.model === "string" && task.model !== formatModelRef(undefined) && task.model.trim() ? task.model : undefined;
|
|
243
|
+
const effort = typeof task.thinking === "string" && task.thinking.trim() ? task.thinking : undefined;
|
|
244
|
+
return {
|
|
245
|
+
...(modelId === undefined ? {} : { writerModelId: modelId }),
|
|
246
|
+
...(effort === undefined ? {} : { writerEffort: effort }),
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
|
|
325
250
|
function taskDetails(task: TaskRecord): Record<string, unknown> {
|
|
326
251
|
return { gentleAgents: { taskId: task.id, agent: task.agent, status: task.status, mode: task.mode, cwd: task.cwd } };
|
|
327
252
|
}
|
|
@@ -364,12 +289,24 @@ export function resolveDefaultSubagentMode(input: {
|
|
|
364
289
|
}
|
|
365
290
|
|
|
366
291
|
// What the model reads when a background task ends: the outcome first, then
|
|
367
|
-
// the answer itself. The card
|
|
292
|
+
// the answer itself. The expanded card shows the same text.
|
|
368
293
|
export function completionText(task: TaskRecord): string {
|
|
369
294
|
const outcome = task.status === "completed" ? "finished" : task.status.replace("_", " ");
|
|
370
295
|
return `Subagent ${task.agent} (task ${task.id}, "${task.label}") ${outcome}.\n\n${finishedText(task)}`;
|
|
371
296
|
}
|
|
372
297
|
|
|
298
|
+
const COMPLETION_HEADER = /^Subagent [^\n]+ \(task [^\n]*\) [^\n]+\.$/;
|
|
299
|
+
|
|
300
|
+
// The collapsed card leads with the answer or error: completionText's
|
|
301
|
+
// bookkeeping paragraph stays for the model and the expanded card. Entries
|
|
302
|
+
// without that header (older sessions) preview their full text.
|
|
303
|
+
export function agentResultPreview(text: string): string {
|
|
304
|
+
const split = text.indexOf("\n\n");
|
|
305
|
+
if (split < 0 || !COMPLETION_HEADER.test(text.slice(0, split))) return text;
|
|
306
|
+
const rest = text.slice(split + 2);
|
|
307
|
+
return rest.trim() === "" ? text : rest;
|
|
308
|
+
}
|
|
309
|
+
|
|
373
310
|
// Host-side answer to a child's dialog: the same ctx.ui the human already
|
|
374
311
|
// uses, so a subagent's question looks like any other pi dialog.
|
|
375
312
|
export async function answerThroughUi(ui: ExtensionContext["ui"] | undefined, ask: AskRequest, raw: Record<string, unknown>): Promise<AskAnswer> {
|
|
@@ -396,66 +333,21 @@ export async function answerThroughUi(ui: ExtensionContext["ui"] | undefined, as
|
|
|
396
333
|
}
|
|
397
334
|
}
|
|
398
335
|
|
|
399
|
-
const RESEARCH_SELECTION_SCHEMA = {
|
|
400
|
-
type: "object",
|
|
401
|
-
additionalProperties: false,
|
|
402
|
-
description: "Untrusted narrowing intent for sdd-research; exact tools and existing sourceInfo.path per tool. Never grants permissions or installs extensions.",
|
|
403
|
-
properties: Object.fromEntries(["documentation", "open-web"].map(kind => [kind, {
|
|
404
|
-
type: "object", additionalProperties: false, required: ["tools", "extensions"],
|
|
405
|
-
properties: {
|
|
406
|
-
tools: { type: "array", items: { type: "string" } },
|
|
407
|
-
extensions: { type: "object", additionalProperties: { type: "string" } },
|
|
408
|
-
},
|
|
409
|
-
}])),
|
|
410
|
-
};
|
|
411
|
-
|
|
412
336
|
export default function gentleAgents(pi: ExtensionAPI, env: NodeJS.ProcessEnv = process.env, overrides: Partial<AgentsDeps> = {}): void {
|
|
413
|
-
if (env.GENTLE_PI_AGENTS_CHILD === "1" && env[RESEARCH_CHILD_TOOLS_ENV] !== undefined) {
|
|
414
|
-
let allowed: string[] = [];
|
|
415
|
-
try {
|
|
416
|
-
const parsed: unknown = JSON.parse(env[RESEARCH_CHILD_TOOLS_ENV]!);
|
|
417
|
-
if (Array.isArray(parsed) && parsed.every(value => typeof value === "string")) allowed = parsed;
|
|
418
|
-
} catch { /* Invalid launch restrictions deny every tool. */ }
|
|
419
|
-
let selection: unknown;
|
|
420
|
-
try { selection = JSON.parse(env[RESEARCH_SELECTION_ENV] ?? "null"); } catch { /* Missing selection grants no research. */ }
|
|
421
|
-
const current = () => researchAgent({ tools: allowed, instructions: "" } as AgentDefinition, pi, selection);
|
|
422
|
-
pi.on("before_agent_start", event => ({
|
|
423
|
-
systemPrompt: `${event.systemPrompt}\n\n${renderResearchCapabilities(current().capabilities)}\n\nResearch is output-only. Use parent-supplied local context; do not read or mutate repository or Engram artifacts. Return useful partial findings and unavailable sources honestly. The parent owns authorized persistence and actual readback.`,
|
|
424
|
-
}));
|
|
425
|
-
pi.on("tool_call", event => {
|
|
426
|
-
const registered = pi.getAllTools().some(tool => tool.name === event.toolName && tool.sourceInfo?.source !== "sdk");
|
|
427
|
-
const selected = current().agent.tools.includes(event.toolName) || event.toolName === "subagent_parent_message";
|
|
428
|
-
if (!registered || !selected || !allowed.includes(event.toolName) || !pi.getActiveTools().includes(event.toolName)) {
|
|
429
|
-
return { block: true, reason: "Tool is outside the output-only research child's active launch allowlist." };
|
|
430
|
-
}
|
|
431
|
-
});
|
|
432
|
-
}
|
|
433
337
|
const childIpc = ownedChildIpc(env, overrides.childIpc ?? (process.send ? process as unknown as IpcEndpoint : undefined));
|
|
434
338
|
if (env.GENTLE_PI_AGENTS_CHILD === "1") {
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
pi.on("tool_call", (
|
|
438
|
-
|
|
439
|
-
granted = undefined;
|
|
440
|
-
try {
|
|
441
|
-
const retained = JSON.parse(env[REMEDIATION_PLAN_ENV]!);
|
|
442
|
-
// SDK flags are owner-local; the runner transports the same selected context.
|
|
443
|
-
const selection = Object.hasOwn(retained, "selection") ? retained.selection : JSON.parse(String(pi.getFlag("gentle-sdd-change")));
|
|
444
|
-
parseSddChange(selection, "sdd-remediate");
|
|
445
|
-
const plan = parseRemediationPlan(retained.plan, ctx.cwd);
|
|
446
|
-
if (selection.workspaceRoot !== ctx.cwd || JSON.stringify(plannedCommands(plan)) !== JSON.stringify(retained.scope?.commands) || JSON.stringify(plan.editPaths ?? []) !== JSON.stringify(retained.scope?.editPaths)) throw new Error("Remediation grant/plan mismatch");
|
|
447
|
-
const shell = remediationBash(ctx.cwd, undefined, retained.scope);
|
|
448
|
-
pi.registerTool(shell.definition);
|
|
449
|
-
pi.on("tool_result", event => event.toolName === "bash" ? shell.result(event) : undefined);
|
|
450
|
-
granted = retained.scope;
|
|
451
|
-
} catch { /* No valid host grant: deny every tool, even if Pi continues initialization. */ }
|
|
452
|
-
});
|
|
339
|
+
// A stale managed-SDD child must never inherit unrestricted ordinary tools.
|
|
340
|
+
if (env.GENTLE_PI_RESEARCH_TOOLS !== undefined || env.GENTLE_PI_RESEARCH_SELECTION !== undefined || env.GENTLE_PI_RESEARCH_ARTIFACT !== undefined || env.GENTLE_PI_SDD_REMEDIATION_PLAN !== undefined) {
|
|
341
|
+
pi.on("tool_call", () => ({ block: true, reason: "Retired SDD child launch is not supported." }));
|
|
342
|
+
return;
|
|
453
343
|
}
|
|
454
344
|
if (childIpc) registerChildMessaging(pi, childIpc);
|
|
455
345
|
return;
|
|
456
346
|
}
|
|
457
347
|
if (!agentsEnabled(env)) return;
|
|
458
348
|
const deps: AgentsDeps = { ...defaultDeps(env), ...overrides };
|
|
349
|
+
// An explicitly injected pi command wins over the default per-spawn resolver.
|
|
350
|
+
if (overrides?.pi && !overrides.resolvePi) delete deps.resolvePi;
|
|
459
351
|
const selectedHome = overrides.agentHome ?? (overrides.home === undefined ? resolveGentlePiAgentHome(deps.env) : join(deps.home, ".pi", "agent"));
|
|
460
352
|
// Expand environment tildes like Pi, but leave explicit path APIs literal.
|
|
461
353
|
const environmentHome = overrides.agentHome === undefined && overrides.home === undefined;
|
|
@@ -479,6 +371,9 @@ export default function gentleAgents(pi: ExtensionAPI, env: NodeJS.ProcessEnv =
|
|
|
479
371
|
let ui: ExtensionContext["ui"] | undefined;
|
|
480
372
|
let host: { requestRender(): void } | undefined;
|
|
481
373
|
let sidebarTui: TUI | undefined;
|
|
374
|
+
const visualHome = gentlePiConfigHome(env);
|
|
375
|
+
let agentsVisible = resolveVisualSettings({ gentlePiConfigHome: visualHome }).settings.visibility.agents;
|
|
376
|
+
let stopVisualUpdates: (() => void) | undefined;
|
|
482
377
|
let sessions: ExtensionContext["sessionManager"] | undefined;
|
|
483
378
|
let presence: PresencePublisher | undefined;
|
|
484
379
|
let rpcActivityPublisher: RpcActivityPublisher | undefined;
|
|
@@ -499,12 +394,17 @@ export default function gentleAgents(pi: ExtensionAPI, env: NodeJS.ProcessEnv =
|
|
|
499
394
|
} catch { presence?.dispose(); presence = undefined; }
|
|
500
395
|
};
|
|
501
396
|
let worktrees: SessionWorktreeRegistry | undefined;
|
|
397
|
+
let worktreeManager: ExtensionContext["sessionManager"] | undefined;
|
|
398
|
+
let worktreeAuthority: (() => boolean) | undefined;
|
|
502
399
|
const foreignGrants = new ForeignTargetGrants();
|
|
400
|
+
const messagingGrants = new SessionMessagingGrants();
|
|
503
401
|
const foreignTasks = new Map<string, { root: string; commonDir: string; manager: ExtensionContext["sessionManager"] }>();
|
|
504
402
|
const foreignRequests = new WeakMap<TaskRequest, { root: string; commonDir: string; manager: ExtensionContext["sessionManager"] }>();
|
|
505
403
|
const registryFor = (ctx: ExtensionContext) => {
|
|
506
|
-
if (!worktrees || worktrees.sessionId !== ctx.sessionManager.getSessionId()) {
|
|
404
|
+
if (!worktrees || worktreeManager !== ctx.sessionManager || worktrees.sessionId !== ctx.sessionManager.getSessionId()) {
|
|
507
405
|
worktrees?.close();
|
|
406
|
+
worktreeManager = ctx.sessionManager;
|
|
407
|
+
worktreeAuthority = sessionRepositoryAuthority(ctx.sessionManager.getCwd(), deps.resolveWorktree);
|
|
508
408
|
worktrees = new SessionWorktreeRegistry(pi, ctx.sessionManager, ctx.sessionManager.getCwd(), deps.resolveWorktree);
|
|
509
409
|
}
|
|
510
410
|
return worktrees;
|
|
@@ -688,20 +588,128 @@ export default function gentleAgents(pi: ExtensionAPI, env: NodeJS.ProcessEnv =
|
|
|
688
588
|
// flushed at the next turn boundary, and a stale one never re-enters the
|
|
689
589
|
// conversation.
|
|
690
590
|
const completions = createCompletionQueue<TaskRecord>();
|
|
591
|
+
const messages = createAgentMessageQueue();
|
|
691
592
|
let activeAgentRuns = 0;
|
|
593
|
+
// ExtensionAPI has no idle probe; ctx.isIdle() is the only one. It is live
|
|
594
|
+
// and also reports compaction, which activeAgentRuns cannot see. The
|
|
595
|
+
// session_start context is kept for it and dropped at shutdown; a stale
|
|
596
|
+
// context throws instead of answering, so delivery fails closed.
|
|
597
|
+
let parentCtx: ExtensionContext | undefined;
|
|
598
|
+
// Mirrors the host's agent run, which spans agent_start through
|
|
599
|
+
// agent_settled, including post-run retries and in-run compaction. Unlike
|
|
600
|
+
// activeAgentRuns it stays set between agent_end and agent_settled, where
|
|
601
|
+
// Pi still streams and still drains steering.
|
|
602
|
+
let parentRunActive = false;
|
|
603
|
+
// Idle wake-up state. Child content stored while the parent is idle owes a
|
|
604
|
+
// wake, and one wake covers everything stored before the run it starts.
|
|
605
|
+
// While a prompt is starting (a dispatched wake, or any prompt seen at
|
|
606
|
+
// before_agent_start) no second wake is sent: two prompts racing through
|
|
607
|
+
// Pi's asynchronous pre-run phase can both reach the agent, and the loser
|
|
608
|
+
// resets the winner's run state. A prompt that never starts a run (handled
|
|
609
|
+
// by an input handler, or rejected) emits no extension event, so the
|
|
610
|
+
// starting state expires after PARENT_WAKE_GRACE_MS and an owed wake is
|
|
611
|
+
// sent then; it never suppresses later wakes for longer than that.
|
|
612
|
+
let wakeOwed = false;
|
|
613
|
+
let wakeQueued = false;
|
|
614
|
+
let promptStarting = false;
|
|
615
|
+
let cancelPromptGrace: (() => void) | undefined;
|
|
616
|
+
let cancelBoundaryFlush: (() => void) | undefined;
|
|
617
|
+
|
|
618
|
+
const isTaskLive = (id: string): boolean => {
|
|
619
|
+
const task = store.get(id);
|
|
620
|
+
return Boolean(task && ownedTaskIds.has(task.id) && !isFinished(task.status));
|
|
621
|
+
};
|
|
622
|
+
|
|
623
|
+
// Hands model-visible child content to the parent session.
|
|
624
|
+
//
|
|
625
|
+
// A busy parent gets "steer" + triggerTurn, which keeps delivery bounded to
|
|
626
|
+
// the current turn: the host polls steering each turn and injects the
|
|
627
|
+
// message before the next LLM call. "followUp" is NOT acceptable here
|
|
628
|
+
// because the host drains the follow-up queue only in the run loop's stop
|
|
629
|
+
// branch, so a parent that keeps calling tools would see the content only
|
|
630
|
+
// when the whole run ends — the original #867 delay.
|
|
631
|
+
//
|
|
632
|
+
// An idle parent must not get triggerTurn: the host would run the custom
|
|
633
|
+
// message as a direct turn that skips the prompt lifecycle
|
|
634
|
+
// (before_agent_start and the prompt refresh), and prompt-capture
|
|
635
|
+
// integrations such as the Claude bridge reject that turn. The structured
|
|
636
|
+
// message is stored durably without a turn instead, and a short
|
|
637
|
+
// system-generated user message wakes the parent through the normal prompt
|
|
638
|
+
// path. The wake never repeats child content, so the model sees it once.
|
|
639
|
+
//
|
|
640
|
+
// A parent that is busy without a run (compaction, or a prompt's pre-run
|
|
641
|
+
// compaction) is not streaming, so steer + triggerTurn would also start a
|
|
642
|
+
// direct turn. Its content stays queued ("hold") until a later boundary.
|
|
643
|
+
type ParentRoute = "idle" | "run" | "hold";
|
|
644
|
+
// Throws for a missing or stale parent context, so delivery fails closed.
|
|
645
|
+
const parentRoute = (): ParentRoute => {
|
|
646
|
+
if (!parentCtx) throw new Error("Gentle Agents has no live parent session context");
|
|
647
|
+
if (parentCtx.isIdle()) {
|
|
648
|
+
// The host is authoritative: an idle parent has no run, even if a
|
|
649
|
+
// lifecycle event was missed.
|
|
650
|
+
parentRunActive = false;
|
|
651
|
+
return "idle";
|
|
652
|
+
}
|
|
653
|
+
return parentRunActive ? "run" : "hold";
|
|
654
|
+
};
|
|
655
|
+
|
|
656
|
+
const endPromptStart = () => {
|
|
657
|
+
promptStarting = false;
|
|
658
|
+
cancelPromptGrace?.();
|
|
659
|
+
cancelPromptGrace = undefined;
|
|
660
|
+
};
|
|
661
|
+
const beginPromptStart = () => {
|
|
662
|
+
endPromptStart();
|
|
663
|
+
promptStarting = true;
|
|
664
|
+
cancelPromptGrace = deps.schedule(() => {
|
|
665
|
+
cancelPromptGrace = undefined;
|
|
666
|
+
promptStarting = false;
|
|
667
|
+
requestWake();
|
|
668
|
+
}, PARENT_WAKE_GRACE_MS);
|
|
669
|
+
};
|
|
670
|
+
|
|
671
|
+
// Every wake requested during one synchronous delivery pass is coalesced
|
|
672
|
+
// into a single dispatch after it, so the wake follows all stored content.
|
|
673
|
+
const requestWake = () => {
|
|
674
|
+
if (!wakeOwed || wakeQueued || promptStarting) return;
|
|
675
|
+
wakeQueued = true;
|
|
676
|
+
queueMicrotask(dispatchWake);
|
|
677
|
+
};
|
|
678
|
+
const dispatchWake = () => {
|
|
679
|
+
wakeQueued = false;
|
|
680
|
+
if (!wakeOwed || promptStarting) return;
|
|
681
|
+
let route: ParentRoute;
|
|
682
|
+
try { route = parentRoute(); } catch { return; }
|
|
683
|
+
// A run in progress already carries the stored content; a parent busy
|
|
684
|
+
// without a run keeps the wake owed until a later boundary flush.
|
|
685
|
+
if (route !== "idle") return;
|
|
686
|
+
wakeOwed = false;
|
|
687
|
+
beginPromptStart();
|
|
688
|
+
try {
|
|
689
|
+
// "steer" matters only when a run started in between: the wake is then
|
|
690
|
+
// queued into it instead of being rejected as a concurrent prompt.
|
|
691
|
+
pi.sendUserMessage(PARENT_WAKE_TEXT, { deliverAs: "steer" });
|
|
692
|
+
} catch {
|
|
693
|
+
// A stale runtime fails closed instead of throwing from a microtask.
|
|
694
|
+
endPromptStart();
|
|
695
|
+
}
|
|
696
|
+
};
|
|
692
697
|
|
|
693
|
-
const
|
|
698
|
+
const sendToParent = (message: Parameters<ExtensionAPI["sendMessage"]>[0], route: Exclude<ParentRoute, "hold">) => {
|
|
699
|
+
if (route === "run") {
|
|
700
|
+
pi.sendMessage(message, { deliverAs: "steer", triggerTurn: true });
|
|
701
|
+
return;
|
|
702
|
+
}
|
|
703
|
+
pi.sendMessage(message, { triggerTurn: false });
|
|
704
|
+
wakeOwed = true;
|
|
705
|
+
requestWake();
|
|
706
|
+
};
|
|
707
|
+
|
|
708
|
+
const deliver = (task: TaskRecord, route: Exclude<ParentRoute, "hold">) => {
|
|
694
709
|
// Ownership is consulted at delivery time, matching onNotification and
|
|
695
710
|
// onQuery: a completion owned by another session is dropped, not delivered.
|
|
696
711
|
if (activeSessionId() !== task.parentSessionId) return;
|
|
697
|
-
|
|
698
|
-
// the parent streams, the host polls steering each turn and injects the
|
|
699
|
-
// message before the next LLM call; "followUp" is NOT acceptable here
|
|
700
|
-
// because the host drains the follow-up queue only in the run loop's stop
|
|
701
|
-
// branch, so a parent that keeps calling tools would see the completion
|
|
702
|
-
// only when the whole run ends — the original #867 delay. When the parent
|
|
703
|
-
// is idle, triggerTurn runs the prompt immediately, preserving wake-up.
|
|
704
|
-
pi.sendMessage({ customType: AGENTS_RESULT_TYPE, content: completionText(task), display: true, details: taskDetails(task) }, { deliverAs: "steer", triggerTurn: true });
|
|
712
|
+
sendToParent({ customType: AGENTS_RESULT_TYPE, content: completionText(task), display: true, details: taskDetails(task) }, route);
|
|
705
713
|
};
|
|
706
714
|
|
|
707
715
|
// A stale completion must not re-enter the LLM conversation, so it is
|
|
@@ -712,22 +720,85 @@ export default function gentleAgents(pi: ExtensionAPI, env: NodeJS.ProcessEnv =
|
|
|
712
720
|
pi.appendEntry(AGENTS_STALE_RESULT_TYPE, { taskId: task.id, agent: task.agent, label: task.label, status: task.status, ageSeconds });
|
|
713
721
|
};
|
|
714
722
|
|
|
723
|
+
const deliverMessage = (msg: PendingAgentMessage, route: Exclude<ParentRoute, "hold">) => {
|
|
724
|
+
if (activeSessionId() !== msg.parentSessionId) return;
|
|
725
|
+
sendToParent({ customType: AGENTS_MESSAGE_TYPE, content: msg.content, display: msg.display, details: msg.details }, route);
|
|
726
|
+
};
|
|
727
|
+
|
|
728
|
+
// Child content leaves its queue only when the parent can receive it. A
|
|
729
|
+
// held or undeliverable flush leaves everything queued for a later boundary;
|
|
730
|
+
// `rethrow` lets a query report a stale parent back to its child.
|
|
731
|
+
const deliveryRoute = (rethrow: boolean): Exclude<ParentRoute, "hold"> | undefined => {
|
|
732
|
+
try {
|
|
733
|
+
const route = parentRoute();
|
|
734
|
+
return route === "hold" ? undefined : route;
|
|
735
|
+
} catch (error) {
|
|
736
|
+
if (rethrow) throw error;
|
|
737
|
+
return undefined;
|
|
738
|
+
}
|
|
739
|
+
};
|
|
740
|
+
|
|
741
|
+
const flushMessages = (rethrow = false) => {
|
|
742
|
+
const route = deliveryRoute(rethrow);
|
|
743
|
+
if (!route) return;
|
|
744
|
+
for (const msg of messages.takeDeliverable(deps.now(), activeSessionId() ?? "", isTaskLive)) {
|
|
745
|
+
try {
|
|
746
|
+
deliverMessage(msg, route);
|
|
747
|
+
} catch (error) {
|
|
748
|
+
if (rethrow) throw error;
|
|
749
|
+
/* Best-effort delivery: at most once, even if forwarding fails. */
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
};
|
|
753
|
+
|
|
715
754
|
const flushCompletions = () => {
|
|
755
|
+
const route = deliveryRoute(false);
|
|
756
|
+
if (!route) return;
|
|
716
757
|
for (const { task, settledAt, stale } of completions.takeDeliverable(deps.now())) {
|
|
717
758
|
try {
|
|
718
759
|
if (stale) deliverStale(task, settledAt);
|
|
719
|
-
else deliver(task);
|
|
760
|
+
else deliver(task, route);
|
|
720
761
|
} catch { /* Best-effort delivery: at most once, even if forwarding fails. */ }
|
|
721
762
|
}
|
|
722
763
|
};
|
|
723
764
|
|
|
765
|
+
const flushAll = () => {
|
|
766
|
+
flushMessages();
|
|
767
|
+
flushCompletions();
|
|
768
|
+
// A wake left owed by an earlier held or expired attempt is retried here.
|
|
769
|
+
requestWake();
|
|
770
|
+
};
|
|
771
|
+
|
|
772
|
+
// Pi still reports compaction while these handlers run (a manual
|
|
773
|
+
// session_compact, an automatic compaction's session_compact_failed), so
|
|
774
|
+
// held content is flushed once on the next scheduler tick instead. A parent
|
|
775
|
+
// still busy then keeps it held for the next boundary; nothing polls.
|
|
776
|
+
const scheduleBoundaryFlush = () => {
|
|
777
|
+
if (cancelBoundaryFlush) return;
|
|
778
|
+
cancelBoundaryFlush = deps.schedule(() => {
|
|
779
|
+
cancelBoundaryFlush = undefined;
|
|
780
|
+
flushAll();
|
|
781
|
+
}, 0);
|
|
782
|
+
};
|
|
783
|
+
|
|
784
|
+
// Session changes discard every pending wake and boundary flush.
|
|
785
|
+
const resetParentDelivery = (ctx: ExtensionContext | undefined) => {
|
|
786
|
+
parentCtx = ctx;
|
|
787
|
+
parentRunActive = false;
|
|
788
|
+
wakeOwed = false;
|
|
789
|
+
endPromptStart();
|
|
790
|
+
cancelBoundaryFlush?.();
|
|
791
|
+
cancelBoundaryFlush = undefined;
|
|
792
|
+
};
|
|
793
|
+
|
|
724
794
|
// A completion settles into our queue. An idle parent flushes right away so
|
|
725
795
|
// the wake-up behavior is unchanged; a busy parent flushes at the next turn
|
|
726
796
|
// boundary, and the steer mode injects it before that turn's next LLM call
|
|
727
797
|
// instead of parking it behind the whole run.
|
|
728
798
|
const settleCompletion = (task: TaskRecord) => {
|
|
799
|
+
messages.invalidateTask(task.id);
|
|
729
800
|
completions.enqueue(task, deps.now());
|
|
730
|
-
if (activeAgentRuns === 0)
|
|
801
|
+
if (activeAgentRuns === 0) flushAll();
|
|
731
802
|
};
|
|
732
803
|
|
|
733
804
|
// `agent_start`/`agent_end` bracket a parent agent run; `turn_end` fires at
|
|
@@ -737,35 +808,60 @@ export default function gentleAgents(pi: ExtensionAPI, env: NodeJS.ProcessEnv =
|
|
|
737
808
|
// a final `turn_end` (an aborted run, or the host's early post-run return
|
|
738
809
|
// when a run produced no assistant message; the host compensates via
|
|
739
810
|
// hasQueuedMessages() + continue(), so steering there is still bounded).
|
|
740
|
-
// `agent_settled` is the final idle boundary after retries
|
|
741
|
-
//
|
|
742
|
-
|
|
811
|
+
// `agent_settled` is the final idle boundary after retries: it ends the
|
|
812
|
+
// host run and flushes anything held, including content held while a
|
|
813
|
+
// compaction ran inside or right before the run. `before_agent_start`
|
|
814
|
+
// marks a prompt that is about to start a run, and the compaction events
|
|
815
|
+
// release content held while the parent compacted without a run.
|
|
816
|
+
pi.on("before_agent_start", () => {
|
|
817
|
+
if (!parentRunActive) beginPromptStart();
|
|
818
|
+
});
|
|
819
|
+
pi.on("agent_start", () => {
|
|
820
|
+
activeAgentRuns += 1;
|
|
821
|
+
parentRunActive = true;
|
|
822
|
+
// The run carries everything stored while the parent was idle.
|
|
823
|
+
wakeOwed = false;
|
|
824
|
+
endPromptStart();
|
|
825
|
+
});
|
|
743
826
|
pi.on("agent_end", () => {
|
|
744
827
|
activeAgentRuns = Math.max(0, activeAgentRuns - 1);
|
|
745
|
-
|
|
828
|
+
flushAll();
|
|
746
829
|
});
|
|
747
|
-
pi.on("agent_settled", () =>
|
|
748
|
-
|
|
830
|
+
pi.on("agent_settled", () => {
|
|
831
|
+
parentRunActive = false;
|
|
832
|
+
endPromptStart();
|
|
833
|
+
flushAll();
|
|
834
|
+
});
|
|
835
|
+
pi.on("turn_end", () => flushAll());
|
|
836
|
+
pi.on("session_compact", scheduleBoundaryFlush);
|
|
837
|
+
pi.on("session_compact_failed", scheduleBoundaryFlush);
|
|
749
838
|
|
|
750
839
|
const runner = new AgentRunner(store, loadAgentsConfig({ cwd: process.cwd(), home: deps.home, agentHome }), deps, {
|
|
751
840
|
askUser: (_taskId, ask, raw) => answerThroughUi(ui, ask, raw),
|
|
752
841
|
onNotification: (task, message) => {
|
|
753
|
-
if (activeSessionId() !== task.parentSessionId) return false;
|
|
754
|
-
|
|
842
|
+
if (activeSessionId() !== task.parentSessionId || isFinished(task.status)) return false;
|
|
843
|
+
messages.enqueueNotification(task, message, deps.now());
|
|
844
|
+
if (activeAgentRuns === 0) flushMessages();
|
|
755
845
|
return true;
|
|
756
846
|
},
|
|
757
847
|
onQuery: (task, requestId, message) => {
|
|
758
|
-
if (activeSessionId() !== task.parentSessionId) return false;
|
|
848
|
+
if (activeSessionId() !== task.parentSessionId || isFinished(task.status)) return false;
|
|
759
849
|
const hadYield = yieldedTaskIds.has(task.id);
|
|
760
850
|
if (task.mode === AGENT_MODE.TASK) yieldedTaskIds.add(task.id);
|
|
761
851
|
try {
|
|
762
|
-
|
|
852
|
+
messages.enqueueQuery(task, requestId, message, deps.now());
|
|
853
|
+
if (activeAgentRuns === 0) flushMessages(true);
|
|
763
854
|
return true;
|
|
764
855
|
} catch (error) {
|
|
856
|
+
messages.expireQuery(task.id, requestId);
|
|
765
857
|
if (task.mode === AGENT_MODE.TASK && !hadYield) yieldedTaskIds.delete(task.id);
|
|
766
858
|
throw error;
|
|
767
859
|
}
|
|
768
860
|
},
|
|
861
|
+
onQuerySettled: (taskId, requestId, outcome) => {
|
|
862
|
+
if (outcome === "replied") messages.consumeQuery(taskId, requestId);
|
|
863
|
+
else messages.expireQuery(taskId, requestId);
|
|
864
|
+
},
|
|
769
865
|
onSuccessfulMutation: (task, tool) => {
|
|
770
866
|
// Same-clone registry attribution remains unchanged. A foreign task
|
|
771
867
|
// uses a separately bound, live-grant path only for successful tool evidence.
|
|
@@ -813,7 +909,7 @@ export default function gentleAgents(pi: ExtensionAPI, env: NodeJS.ProcessEnv =
|
|
|
813
909
|
}
|
|
814
910
|
else if (resolvedPath !== undefined) noteDrop("evidence-path-mismatch");
|
|
815
911
|
}
|
|
816
|
-
if (!foreignTask) recordReviewMutation(pi, sessions, root, { source: "subagent", taskId: task.id, toolName: tool.toolName, toolCallId: tool.toolCallId });
|
|
912
|
+
if (!foreignTask) recordReviewMutation(pi, sessions, root, { source: "subagent", taskId: task.id, toolName: tool.toolName, toolCallId: tool.toolCallId, ...runtimeWriterProfile(task) });
|
|
817
913
|
},
|
|
818
914
|
onFinish: (task, observations) => {
|
|
819
915
|
// Completion is the only forwarding opportunity. No pending event, policy
|
|
@@ -831,6 +927,7 @@ export default function gentleAgents(pi: ExtensionAPI, env: NodeJS.ProcessEnv =
|
|
|
831
927
|
} catch { /* Metrics must never interrupt task finalization. */ }
|
|
832
928
|
try {
|
|
833
929
|
ownedTaskIds.delete(task.id);
|
|
930
|
+
messages.invalidateTask(task.id);
|
|
834
931
|
requestRender();
|
|
835
932
|
persist(task);
|
|
836
933
|
const yielded = yieldedTaskIds.delete(task.id);
|
|
@@ -859,12 +956,13 @@ export default function gentleAgents(pi: ExtensionAPI, env: NodeJS.ProcessEnv =
|
|
|
859
956
|
pi.registerMessageRenderer(AGENTS_RESULT_TYPE, (message, options, theme) => {
|
|
860
957
|
const details = (message.details as { gentleAgents?: { agent?: string; status?: string } } | undefined)?.gentleAgents;
|
|
861
958
|
const content = message.content as string | Array<{ type: string; text?: string }>;
|
|
862
|
-
const
|
|
959
|
+
const text = typeof content === "string" ? content : content.map((part) => (part.type === "text" ? (part.text ?? "") : "")).join("\n");
|
|
960
|
+
const body = options.expanded ? text.split("\n") : agentResultPreview(text).split("\n").filter((line) => line.trim() !== "");
|
|
863
961
|
const tone = details?.status === "completed" ? CARD_TONE.SUCCESS : CARD_TONE.ERROR;
|
|
864
962
|
const hint = expandHint(options.expanded);
|
|
865
963
|
return {
|
|
866
964
|
render(width: number) {
|
|
867
|
-
return renderCard({ title: "Agent result", subtitle: details?.agent, body, tone, glyph: AGENTS_GLYPH }, theme, width, { expanded: options.expanded, hint });
|
|
965
|
+
return renderCard({ title: "Agent result", subtitle: details?.agent, body, tone, glyph: AGENTS_GLYPH }, theme, width, { expanded: options.expanded, previewRows: 3, hint });
|
|
868
966
|
},
|
|
869
967
|
invalidate() {},
|
|
870
968
|
};
|
|
@@ -886,7 +984,7 @@ export default function gentleAgents(pi: ExtensionAPI, env: NodeJS.ProcessEnv =
|
|
|
886
984
|
];
|
|
887
985
|
return {
|
|
888
986
|
render(width: number) {
|
|
889
|
-
return renderCard({ title: "Stale agent result", subtitle: `${agent} · task ${taskId}`, body, tone: CARD_TONE.WARNING, glyph: AGENTS_GLYPH }, theme, width, { expanded: options.expanded, hint: expandHint(options.expanded) });
|
|
987
|
+
return renderCard({ title: "Stale agent result", subtitle: `${agent} · task ${taskId}`, body, tone: CARD_TONE.WARNING, glyph: AGENTS_GLYPH }, theme, width, { expanded: options.expanded, previewRows: 3, hint: expandHint(options.expanded) });
|
|
890
988
|
},
|
|
891
989
|
invalidate() {},
|
|
892
990
|
};
|
|
@@ -990,6 +1088,7 @@ export default function gentleAgents(pi: ExtensionAPI, env: NodeJS.ProcessEnv =
|
|
|
990
1088
|
let overlayHost: { requestRender(force?: boolean): void; stop(): void; start(): void } | undefined;
|
|
991
1089
|
const chosen = await ctx.ui.custom<TaskRecord | null>(
|
|
992
1090
|
(tui, theme, _keybindings, done) => {
|
|
1091
|
+
const close = withOverlayRepaint(tui, done);
|
|
993
1092
|
overlayHost = tui;
|
|
994
1093
|
view = new AgentsView({
|
|
995
1094
|
theme,
|
|
@@ -1004,8 +1103,8 @@ export default function gentleAgents(pi: ExtensionAPI, env: NodeJS.ProcessEnv =
|
|
|
1004
1103
|
onCancel: (task) => void stopSelected(task, ctx),
|
|
1005
1104
|
canCancel: isOwnedActive,
|
|
1006
1105
|
isLocalTask: (task) => !restoredTaskIds.has(task.id),
|
|
1007
|
-
onOpen: (task) =>
|
|
1008
|
-
onClose: () =>
|
|
1106
|
+
onOpen: (task) => close(task),
|
|
1107
|
+
onClose: () => close(null),
|
|
1009
1108
|
requestRender: () => tui.requestRender(),
|
|
1010
1109
|
});
|
|
1011
1110
|
overlays.add(view);
|
|
@@ -1058,6 +1157,14 @@ export default function gentleAgents(pi: ExtensionAPI, env: NodeJS.ProcessEnv =
|
|
|
1058
1157
|
const showWidget = (ctx: ExtensionContext) => {
|
|
1059
1158
|
ui = ctx.hasUI ? ctx.ui : undefined;
|
|
1060
1159
|
sessions = ctx.sessionManager;
|
|
1160
|
+
agentsVisible = resolveVisualSettings({ gentlePiConfigHome: visualHome }).settings.visibility.agents;
|
|
1161
|
+
stopVisualUpdates?.();
|
|
1162
|
+
stopVisualUpdates = pi.events.on(VISUAL_SETTINGS_CHANGED, (event) => {
|
|
1163
|
+
if ((event as { configHome?: unknown } | undefined)?.configHome !== visualHome) return;
|
|
1164
|
+
agentsVisible = resolveVisualSettings({ gentlePiConfigHome: visualHome }).settings.visibility.agents;
|
|
1165
|
+
if (sidebarTui) invalidateSidebar(sidebarTui);
|
|
1166
|
+
host?.requestRender();
|
|
1167
|
+
});
|
|
1061
1168
|
tickClock();
|
|
1062
1169
|
// Agents is not a rail part: the fullscreen sidebar only suppresses
|
|
1063
1170
|
// bottom components registered through sidebarPart, and this widget is
|
|
@@ -1067,6 +1174,7 @@ export default function gentleAgents(pi: ExtensionAPI, env: NodeJS.ProcessEnv =
|
|
|
1067
1174
|
sidebarTui = tui;
|
|
1068
1175
|
return {
|
|
1069
1176
|
render(width: number) {
|
|
1177
|
+
if (!agentsVisible) return [];
|
|
1070
1178
|
const lines = renderAgentsCard(visibleTasks(), theme, width, deps.now(), { collapsed, collapseKey, maxRows: widgetRows(tui.terminal?.rows), viewKey });
|
|
1071
1179
|
return lines.length === 0 ? [] : [...lines, ""];
|
|
1072
1180
|
},
|
|
@@ -1077,15 +1185,38 @@ export default function gentleAgents(pi: ExtensionAPI, env: NodeJS.ProcessEnv =
|
|
|
1077
1185
|
|
|
1078
1186
|
const roots = (ctx: ExtensionContext) => ({ cwd: ctx.sessionManager.getCwd(), home: deps.home, agentHome });
|
|
1079
1187
|
|
|
1080
|
-
const buildRequest = async (ctx: ExtensionContext, agent: AgentDefinition, prompt: string, label: string | undefined, context: string | undefined, mode: AgentMode, resume?: string, workspaceRoot?: string,
|
|
1188
|
+
const buildRequest = async (ctx: ExtensionContext, agent: AgentDefinition, prompt: string, label: string | undefined, context: string | undefined, mode: AgentMode, resume?: string, workspaceRoot?: string, signal?: AbortSignal, repositoryRoot?: string): Promise<TaskRequest> => {
|
|
1189
|
+
if (retiredSddAgent(agent.name)) throw new Error("Retired SDD agents cannot be dispatched.");
|
|
1190
|
+
if (![AGENT_MODE.TASK, AGENT_MODE.BACKGROUND].includes(mode)) throw new Error("Subagent mode must be task or background.");
|
|
1191
|
+
if (ctx.mode === "print" && mode === AGENT_MODE.BACKGROUND) throw new Error("Background subagents are unavailable in print mode: pi -p exits before a parent session can receive results. Use task mode, RPC mode, or interactive Pi.");
|
|
1192
|
+
const scopeDenied = rejectUnscopedBoundedWriterDispatch({ agent: agent.name, task: prompt, context });
|
|
1193
|
+
if (scopeDenied) throw new Error(scopeDenied.reason);
|
|
1081
1194
|
if (signal?.aborted) throw new Error("Subagent launch aborted before authorization.");
|
|
1082
1195
|
const registry = registryFor(ctx);
|
|
1196
|
+
const authorityCurrent = worktreeAuthority!;
|
|
1197
|
+
const originalManager = ctx.sessionManager;
|
|
1198
|
+
const originalId = originalManager.getSessionId();
|
|
1199
|
+
const originalCwd = originalManager.getCwd();
|
|
1200
|
+
const current = () => sessions === originalManager && originalManager.getSessionId() === originalId && originalManager.getCwd() === originalCwd && !signal?.aborted && authorityCurrent();
|
|
1201
|
+
if (isGenericBoundedWriter(agent.name) && !current()) throw new Error("Writer session Git authority changed before admission.");
|
|
1202
|
+
let admittedModel: string | undefined;
|
|
1203
|
+
const surfaces = allowedEditSurfaces(prompt, context);
|
|
1204
|
+
if (!resume && isGenericBoundedWriter(agent.name) && surfaces?.some(isDevelopmentSurface) && !deps.resolveWorktree(originalCwd, originalCwd) && repositoryRoot === undefined) {
|
|
1205
|
+
const root = safeBootstrapDirectory(originalCwd);
|
|
1206
|
+
if (!root || (workspaceRoot !== undefined && (!isAbsolute(workspaceRoot) || safeBootstrapDirectory(workspaceRoot) !== root))) throw new Error("Writer bootstrap requires the original safe project root.");
|
|
1207
|
+
const config = withPinnedModelProfiles(loadAgentsConfig(roots(ctx)), resolveUnversionedProjectProfile(root, gentlePiConfigHome(deps.env))?.modelProfiles);
|
|
1208
|
+
const model = resolveAgentProfile(agent, config).model ?? ctx.model;
|
|
1209
|
+
const catalogModel = model?.provider ? ctx.modelRegistry?.find(model.provider, model.id) : ctx.modelRegistry?.getAll().find(candidate => candidate.id === model?.id);
|
|
1210
|
+
if (!catalogModel) throw new Error("Writer bootstrap requires a valid effective model in this session's catalog.");
|
|
1211
|
+
admittedModel = `${catalogModel.provider}/${catalogModel.id}`;
|
|
1212
|
+
if (!current() || !await prepareBoundSessionRepository(originalManager, originalCwd, signal) || !current() || safeBootstrapDirectory(originalCwd) !== root || resolveSessionWorktree(originalCwd, originalCwd)?.root !== root) throw new Error("Writer repository preparation was unavailable or its session/target changed.");
|
|
1213
|
+
}
|
|
1083
1214
|
const parentCwd = ctx.sessionManager.getCwd();
|
|
1084
1215
|
// An explicit target is validated before any queue or session-dir writes.
|
|
1085
1216
|
const parentIdentity = deps.resolveWorktree(parentCwd, parentCwd);
|
|
1086
1217
|
if (repositoryRoot !== undefined && workspaceRoot !== undefined) throw new Error("repository_root and workspace_root are mutually exclusive.");
|
|
1087
|
-
if (repositoryRoot !== undefined && (
|
|
1088
|
-
const selectedRoot = repositoryRoot ?? workspaceRoot
|
|
1218
|
+
if (repositoryRoot !== undefined && (deps.env.GENTLE_PI_AGENTS_CHILD === "1" || ctx.mode !== "tui" || !ctx.hasUI)) throw new Error("Foreign repository launch requires an interactive parent session.");
|
|
1219
|
+
const selectedRoot = repositoryRoot ?? workspaceRoot;
|
|
1089
1220
|
// Preserve ordinary non-Git continuation, without admitting any new root.
|
|
1090
1221
|
const sameNonGitContinuation = resume !== undefined && repositoryRoot === undefined && selectedRoot === parentCwd && !parentIdentity;
|
|
1091
1222
|
let foreign = false;
|
|
@@ -1099,7 +1230,7 @@ export default function gentleAgents(pi: ExtensionAPI, env: NodeJS.ProcessEnv =
|
|
|
1099
1230
|
// same-clone registry. Never use a caller-injected resolver for this identity.
|
|
1100
1231
|
const identity = resolveSessionWorktree(selectedRoot, parentCwd);
|
|
1101
1232
|
const canonicalParent = resolveSessionWorktree(parentCwd, parentCwd);
|
|
1102
|
-
if (!identity || (canonicalParent && identity.commonDir === canonicalParent.commonDir) || selectedRoot !== identity.root || !isAbsolute(selectedRoot) || realpathSync(selectedRoot) !== selectedRoot
|
|
1233
|
+
if (!identity || (canonicalParent && identity.commonDir === canonicalParent.commonDir) || selectedRoot !== identity.root || !isAbsolute(selectedRoot) || realpathSync(selectedRoot) !== selectedRoot) throw new Error("repository_root must select a canonical independent Git repository.");
|
|
1103
1234
|
await foreignGrants.authorize(ctx, identity, { continuation: resume !== undefined, signal });
|
|
1104
1235
|
foreignIdentity = identity;
|
|
1105
1236
|
foreignParent = canonicalParent;
|
|
@@ -1107,12 +1238,6 @@ export default function gentleAgents(pi: ExtensionAPI, env: NodeJS.ProcessEnv =
|
|
|
1107
1238
|
foreign = true;
|
|
1108
1239
|
}
|
|
1109
1240
|
} else target = parentIdentity?.root;
|
|
1110
|
-
if (sddChange && target !== sddChange.workspaceRoot && target !== resolve(sddChange.workspaceRoot)) {
|
|
1111
|
-
throw new Error("sdd_change workspaceRoot must resolve to the selected child worktree.");
|
|
1112
|
-
}
|
|
1113
|
-
const launchSddChange = sddChange === undefined || target === undefined
|
|
1114
|
-
? undefined
|
|
1115
|
-
: { ...sddChange, workspaceRoot: target };
|
|
1116
1241
|
// A per-repository profile pin replaces subagent routing for this launch without
|
|
1117
1242
|
// materialising anything: the global stores stay untouched, so two repositories
|
|
1118
1243
|
// worked in parallel stop fighting over one global routing. The child's own
|
|
@@ -1133,7 +1258,11 @@ export default function gentleAgents(pi: ExtensionAPI, env: NodeJS.ProcessEnv =
|
|
|
1133
1258
|
})?.modelProfiles,
|
|
1134
1259
|
);
|
|
1135
1260
|
const profile = resolveAgentProfile(agent, config);
|
|
1136
|
-
|
|
1261
|
+
if (admittedModel !== undefined) {
|
|
1262
|
+
const model = profile.model ?? ctx.model;
|
|
1263
|
+
const catalogModel = model?.provider ? ctx.modelRegistry?.find(model.provider, model.id) : ctx.modelRegistry?.getAll().find(candidate => candidate.id === model?.id);
|
|
1264
|
+
if (!catalogModel || `${catalogModel.provider}/${catalogModel.id}` !== admittedModel || !current()) throw new Error("Writer effective profile or session changed during preparation.");
|
|
1265
|
+
}
|
|
1137
1266
|
const sessionDir = agentRuntimePaths(deps.home, agentHome).sessions;
|
|
1138
1267
|
if (foreign && target) {
|
|
1139
1268
|
const identity = resolveSessionWorktree(target, parentCwd);
|
|
@@ -1146,21 +1275,20 @@ export default function gentleAgents(pi: ExtensionAPI, env: NodeJS.ProcessEnv =
|
|
|
1146
1275
|
const parentSessionId = ctx.sessionManager.getSessionId() ?? "";
|
|
1147
1276
|
const parentWorktreeRoot = ctx.sessionManager.getCwd();
|
|
1148
1277
|
const parentRepositoryIdentity = resolveCanonicalGitRepositoryIdentitySync(parentWorktreeRoot);
|
|
1149
|
-
const sddPreflightContext = SHIPPED_SDD_AGENT_NAME_SET.has(agent.name)
|
|
1150
|
-
? extractParentConfirmedSddPreflightContext(context)
|
|
1151
|
-
: undefined;
|
|
1152
1278
|
const childEnv = { ...deps.env };
|
|
1153
1279
|
if (foreign) for (const key of inheritedUnsafeGitEnvironmentKeys(childEnv)) delete childEnv[key];
|
|
1154
1280
|
const request: TaskRequest = {
|
|
1155
|
-
agent
|
|
1156
|
-
remediationIntent,
|
|
1281
|
+
agent,
|
|
1157
1282
|
prompt,
|
|
1158
1283
|
label,
|
|
1159
1284
|
context,
|
|
1160
|
-
...(sddPreflightContext === undefined ? {} : { sddPreflightContext }),
|
|
1161
1285
|
mode,
|
|
1162
1286
|
cwd: target ?? parentWorktreeRoot,
|
|
1163
1287
|
parentSessionId,
|
|
1288
|
+
...(admittedModel === undefined ? {} : { beforeSpawn: () => {
|
|
1289
|
+
if (!current()) throw new Error("Writer session changed before spawn.");
|
|
1290
|
+
registry.validate(originalCwd);
|
|
1291
|
+
} }),
|
|
1164
1292
|
...(foreign && target ? { beforeSpawn: () => {
|
|
1165
1293
|
if (signal?.aborted || sessions !== ctx.sessionManager || ctx.sessionManager.getSessionId() !== registry.sessionId || ctx.sessionManager.getCwd() !== parentCwd) throw new Error("Foreign clone session or tool call changed before spawn.");
|
|
1166
1294
|
const identity = resolveSessionWorktree(target, parentCwd);
|
|
@@ -1173,9 +1301,8 @@ export default function gentleAgents(pi: ExtensionAPI, env: NodeJS.ProcessEnv =
|
|
|
1173
1301
|
thinking: profile.thinking,
|
|
1174
1302
|
sessionDir,
|
|
1175
1303
|
resumeSessionPath: resume,
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
...(launchSddChange === undefined ? {} : { sddChange: launchSddChange }),
|
|
1304
|
+
...(deps.childExtensionPaths && deps.childExtensionPaths.length > 0 ? { extensionPaths: [...deps.childExtensionPaths] } : {}),
|
|
1305
|
+
env: childEnv,
|
|
1179
1306
|
...(foreign || parentRepositoryIdentity === undefined ? {} : {
|
|
1180
1307
|
authorizeParentStandingReviewPermission: (repositoryIdentity: string) => {
|
|
1181
1308
|
try {
|
|
@@ -1202,17 +1329,7 @@ export default function gentleAgents(pi: ExtensionAPI, env: NodeJS.ProcessEnv =
|
|
|
1202
1329
|
if (ctx.mode === "print" && request.mode === AGENT_MODE.BACKGROUND) {
|
|
1203
1330
|
throw new Error("Background subagents are unavailable in print mode: pi -p exits before a parent session can receive results. Use task mode, RPC mode, or interactive Pi.");
|
|
1204
1331
|
}
|
|
1205
|
-
|
|
1206
|
-
// after its RPC process starts, so validate the single parent transport here
|
|
1207
|
-
// rather than letting a child invent/persist preferences during startup.
|
|
1208
|
-
if (SHIPPED_SDD_AGENT_NAME_SET.has(request.agent.name) && !isParentConfirmedSddPreflightContext(request.context)) {
|
|
1209
|
-
throw new Error("SDD child dispatch refused: parent-confirmed SDD preflight context is missing or malformed.");
|
|
1210
|
-
}
|
|
1211
|
-
if (request.agent.name === "sdd-remediate") {
|
|
1212
|
-
const native = deps.nativeSdd ?? new NativeReviewCliV216(createNodeExecFileAdapter());
|
|
1213
|
-
Object.assign(request, await admitManagedRemediation(request, request.remediationIntent, native, ctx));
|
|
1214
|
-
if (activeSessionId() !== request.parentSessionId) throw new Error("Parent session changed; request fresh authorization before dispatch");
|
|
1215
|
-
}
|
|
1332
|
+
if (retiredSddAgent(request.agent.name)) throw new Error("Retired SDD agents cannot be dispatched.");
|
|
1216
1333
|
|
|
1217
1334
|
// Bounded live observation only. Native send owns the fresh policy decision;
|
|
1218
1335
|
// child execution never starts a telemetry policy process or renewal timer.
|
|
@@ -1265,10 +1382,17 @@ export default function gentleAgents(pi: ExtensionAPI, env: NodeJS.ProcessEnv =
|
|
|
1265
1382
|
const query = await runner.waitForQuery(task.id);
|
|
1266
1383
|
if (query) {
|
|
1267
1384
|
const live = store.get(task.id) ?? task;
|
|
1268
|
-
|
|
1385
|
+
messages.consumeQuery(task.id, query.requestId);
|
|
1386
|
+
const questionSuffix = query.message ? `\n\nQuestion:\n${query.message}` : "";
|
|
1387
|
+
return text(
|
|
1388
|
+
`Subagent ${live.agent} is waiting for your reply to request ${query.requestId}.${questionSuffix}`,
|
|
1389
|
+
{ gentleAgents: { taskId: live.id, agent: live.agent, status: live.status, mode: live.mode, requestId: query.requestId, ...(query.message ? { question: query.message } : {}) } },
|
|
1390
|
+
true,
|
|
1391
|
+
);
|
|
1269
1392
|
}
|
|
1270
1393
|
const finished = await runner.waitFor(task.id);
|
|
1271
1394
|
completions.consume(finished.id);
|
|
1395
|
+
messages.invalidateTask(finished.id);
|
|
1272
1396
|
return text(finishedText(finished), taskDetails(finished));
|
|
1273
1397
|
} finally {
|
|
1274
1398
|
signal?.removeEventListener("abort", onAbort);
|
|
@@ -1283,12 +1407,21 @@ export default function gentleAgents(pi: ExtensionAPI, env: NodeJS.ProcessEnv =
|
|
|
1283
1407
|
description,
|
|
1284
1408
|
parameters: { type: "object", additionalProperties: false, ...parameters } as never,
|
|
1285
1409
|
renderCall(args, theme) {
|
|
1410
|
+
// The result title belongs with the result body so a running poll
|
|
1411
|
+
// can hide both without changing the tool call or its model output.
|
|
1412
|
+
if (name === "result") return { render: () => [], invalidate() {} };
|
|
1286
1413
|
const params = args as { agent?: string; task_id?: string };
|
|
1287
1414
|
return new Text(theme.fg("toolTitle", `${AGENTS_GLYPH} agent ${name.replace(/_/g, " ")}${params.agent ? ` · ${params.agent}` : params.task_id ? ` · ${params.task_id}` : ""}`), 0, 0);
|
|
1288
1415
|
},
|
|
1289
1416
|
renderResult(result, options, theme) {
|
|
1417
|
+
const task = (result.details as { gentleAgents?: { status?: string; taskId?: string } } | undefined)?.gentleAgents;
|
|
1418
|
+
if (name === "result" && task?.status === TASK_STATUS.RUNNING) {
|
|
1419
|
+
return { render: () => [], invalidate() {} };
|
|
1420
|
+
}
|
|
1290
1421
|
const body = result.content.map((part) => (part.type === "text" ? part.text : "")).join("\n");
|
|
1291
|
-
|
|
1422
|
+
const visibleBody = options.expanded ? body : theme.fg("muted", body.split("\n")[0] ?? "");
|
|
1423
|
+
const title = `${AGENTS_GLYPH} agent result${task?.taskId ? ` · ${sanitizeTerminalText(task.taskId)}` : ""}`;
|
|
1424
|
+
return new Text(name === "result" ? `${theme.fg("toolTitle", title)}\n${visibleBody}` : visibleBody, 0, 0);
|
|
1292
1425
|
},
|
|
1293
1426
|
async execute(_id, params, signal, _onUpdate, ctx) {
|
|
1294
1427
|
return execute(params as Record<string, unknown>, ctx, signal);
|
|
@@ -1327,12 +1460,25 @@ export default function gentleAgents(pi: ExtensionAPI, env: NodeJS.ProcessEnv =
|
|
|
1327
1460
|
name: "orchestrator_send_message",
|
|
1328
1461
|
label: "Send orchestrator message",
|
|
1329
1462
|
description: "Send a notification to another active session in this trusted local profile. If recipient_session_id is omitted, the sole peer is selected or the user selects one. Acceptance means enqueued, not read or completed.",
|
|
1330
|
-
parameters: {
|
|
1463
|
+
parameters: {
|
|
1464
|
+
type: "object",
|
|
1465
|
+
additionalProperties: false,
|
|
1466
|
+
required: ["message", "reason"],
|
|
1467
|
+
properties: {
|
|
1468
|
+
recipient_session_id: { type: "string" },
|
|
1469
|
+
message: { type: "string" },
|
|
1470
|
+
reason: { type: "string", minLength: MESSAGING_REASON_MIN_CHARACTERS, maxLength: MESSAGING_REASON_MAX_UTF8_BYTES, description: "Required concrete caller-supplied reason: at least 8 characters after trimming and at most 512 UTF-8 bytes." },
|
|
1471
|
+
},
|
|
1472
|
+
} as never,
|
|
1331
1473
|
async execute(_id, params, signal, _onUpdate, ctx) {
|
|
1332
1474
|
const transport = activeTransportFor(ctx);
|
|
1333
|
-
const input = params as { recipient_session_id?: unknown; message?: unknown };
|
|
1475
|
+
const input = params as { recipient_session_id?: unknown; message?: unknown; reason?: unknown };
|
|
1334
1476
|
if (!transport) return text("Error: session messaging is not ready.", { error: "not ready" });
|
|
1335
|
-
|
|
1477
|
+
const message = input.message;
|
|
1478
|
+
if (typeof message !== "string" || Buffer.byteLength(message, "utf8") > 8192) return text("Error: recipient session ID or message is invalid.", { error: "invalid input" });
|
|
1479
|
+
let reason: string;
|
|
1480
|
+
try { reason = normalizeMessagingReason(input.reason); }
|
|
1481
|
+
catch { return text("Error: recipient session ID or message is invalid; a concrete reason of at least 8 trimmed characters and at most 512 UTF-8 bytes is required.", { error: "invalid input" }); }
|
|
1336
1482
|
let recipient: string | undefined;
|
|
1337
1483
|
let activation: PresenceRecord | undefined;
|
|
1338
1484
|
if (input.recipient_session_id !== undefined) {
|
|
@@ -1364,7 +1510,13 @@ export default function gentleAgents(pi: ExtensionAPI, env: NodeJS.ProcessEnv =
|
|
|
1364
1510
|
if (recipient === undefined || !validTransportSessionId(recipient)) return text("Error: recipient session ID or message is invalid.", { error: "invalid input" });
|
|
1365
1511
|
if (recipient === transport.sessionId) return text("Error: cannot send a message to the active session.", { error: "self" });
|
|
1366
1512
|
try {
|
|
1367
|
-
|
|
1513
|
+
await messagingGrants.authorize(ctx, recipient, { message, reason, signal });
|
|
1514
|
+
} catch (error) {
|
|
1515
|
+
if (signal?.aborted) return text("Cross-orchestrator communication was cancelled.", { error: "cancelled" });
|
|
1516
|
+
return text(`Error: ${error instanceof Error ? error.message : String(error)}`, { error: "denied" });
|
|
1517
|
+
}
|
|
1518
|
+
try {
|
|
1519
|
+
const accepted = await transport.client.sendNotification(recipient, message, { signal, expectedActivation: activation, beforeConnect: () => activeTransportFor(ctx) === transport });
|
|
1368
1520
|
return activeTransportFor(ctx) === transport ? text(`Message ${accepted.id} from ${transport.sessionId} to ${recipient} accepted for delivery; it is not a delivery or read receipt.`, { gentleAgents: { messageId: accepted.id, senderSessionId: transport.sessionId, recipientSessionId: recipient, state: "accepted" } }) : text("Error: session messaging is not ready.", { error: "stale" });
|
|
1369
1521
|
} catch {
|
|
1370
1522
|
return text("Error: session message was not accepted.", { error: "not accepted" });
|
|
@@ -1391,14 +1543,19 @@ export default function gentleAgents(pi: ExtensionAPI, env: NodeJS.ProcessEnv =
|
|
|
1391
1543
|
context: { type: "string", description: "Optional extra context appended to the task." },
|
|
1392
1544
|
workspace_root: { type: "string", description: "Optional canonical main or linked Git worktree within the parent's same clone only; mutually exclusive with repository_root." },
|
|
1393
1545
|
repository_root: { type: "string", description: "Optional canonical independent Git repository; requires direct interactive session-scoped consent before queueing; mutually exclusive with workspace_root." },
|
|
1394
|
-
research_selection: RESEARCH_SELECTION_SCHEMA,
|
|
1395
|
-
remediation: REMEDIATION_SCHEMA, sdd_change: { type: "object", additionalProperties: false, required: ["changeName", "workspaceRoot", "phase"], properties: { changeName: { type: "string" }, workspaceRoot: { type: "string" }, failedEvidenceRevision: { type: "string" }, phase: { type: "string", enum: ["apply", "verify", "archive", "remediate"] } }, description: "Launch-local selected SDD identity, accepted only by matching SDD phase agents." },
|
|
1396
1546
|
mode: { type: "string", enum: ["task", "background"], description: "task waits for the result (default); background returns immediately." },
|
|
1397
1547
|
},
|
|
1398
1548
|
},
|
|
1399
1549
|
async (params, ctx, signal) => {
|
|
1400
|
-
if (
|
|
1550
|
+
if (typeof params.agent !== "string" || !params.agent.trim() || typeof params.task !== "string" || !params.task.trim() || (params.context !== undefined && typeof params.context !== "string") || (params.label !== undefined && typeof params.label !== "string")) throw new Error("Subagent dispatch requires a named agent, non-empty task and string context/label.");
|
|
1551
|
+
if (params.mode !== undefined && params.mode !== AGENT_MODE.TASK && params.mode !== AGENT_MODE.BACKGROUND) throw new Error("Subagent mode must be task or background.");
|
|
1552
|
+
// An empty selector names no destination: only real roots are mutually
|
|
1553
|
+
// exclusive, so a blank string must not masquerade as a second target.
|
|
1554
|
+
const hasWorkspaceRoot = Object.hasOwn(params, "workspace_root") && params.workspace_root !== "";
|
|
1555
|
+
const hasRepositoryRoot = Object.hasOwn(params, "repository_root") && params.repository_root !== "";
|
|
1556
|
+
if (hasRepositoryRoot && hasWorkspaceRoot) throw new Error("repository_root and workspace_root are mutually exclusive.");
|
|
1401
1557
|
if ((Object.hasOwn(params, "repository_root") && typeof params.repository_root !== "string") || (Object.hasOwn(params, "workspace_root") && typeof params.workspace_root !== "string")) throw new Error("Root selectors must be strings.");
|
|
1558
|
+
if (Object.hasOwn(params, "sdd_change") || Object.hasOwn(params, "remediation") || Object.hasOwn(params, "research_selection") || retiredSddAgent(String(params.agent))) return text("Error: retired SDD delegation is not supported.", { error: "retired SDD delegation" });
|
|
1402
1559
|
const { agents } = discoverAgents(roots(ctx));
|
|
1403
1560
|
const agent = agents.find((candidate) => candidate.name === params.agent);
|
|
1404
1561
|
if (!agent) return text(`Error: no subagent named "${String(params.agent)}". Known: ${agents.map((candidate) => candidate.name).join(", ") || "none"}`, { error: "unknown agent" });
|
|
@@ -1407,10 +1564,9 @@ export default function gentleAgents(pi: ExtensionAPI, env: NodeJS.ProcessEnv =
|
|
|
1407
1564
|
policy: resolveBackgroundSubagentsPolicy(ctx.cwd).policy,
|
|
1408
1565
|
parentMode: ctx.mode,
|
|
1409
1566
|
});
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
return launch(ctx, await buildRequest(ctx, agent, String(params.task ?? ""), typeof params.label === "string" ? params.label : undefined, typeof params.context === "string" ? params.context : undefined, mode, undefined, typeof params.workspace_root === "string" ? params.workspace_root : undefined, sddChange, params.research_selection, params.remediation, signal, typeof params.repository_root === "string" ? params.repository_root : undefined), signal);
|
|
1567
|
+
const workspaceRoot = typeof params.workspace_root === "string" && params.workspace_root !== "" ? params.workspace_root : undefined;
|
|
1568
|
+
const repositoryRoot = typeof params.repository_root === "string" && params.repository_root !== "" ? params.repository_root : undefined;
|
|
1569
|
+
return launch(ctx, await buildRequest(ctx, agent, String(params.task ?? ""), typeof params.label === "string" ? params.label : undefined, typeof params.context === "string" ? params.context : undefined, mode, undefined, workspaceRoot, signal, repositoryRoot), signal);
|
|
1414
1570
|
},
|
|
1415
1571
|
);
|
|
1416
1572
|
|
|
@@ -1424,7 +1580,10 @@ export default function gentleAgents(pi: ExtensionAPI, env: NodeJS.ProcessEnv =
|
|
|
1424
1580
|
if (!task) return text(`Error: no task ${String(params.task_id)}`, { error: "unknown task" });
|
|
1425
1581
|
// The parent just pulled a finished result; its pending completion must
|
|
1426
1582
|
// never be replayed on top of it.
|
|
1427
|
-
if (isFinished(task.status))
|
|
1583
|
+
if (isFinished(task.status)) {
|
|
1584
|
+
completions.consume(task.id);
|
|
1585
|
+
messages.invalidateTask(task.id);
|
|
1586
|
+
}
|
|
1428
1587
|
return text(isFinished(task.status) ? finishedText(task) : `Task ${task.id} is still ${task.status} (last: ${task.lastStep}).`, taskDetails(task));
|
|
1429
1588
|
});
|
|
1430
1589
|
|
|
@@ -1434,12 +1593,15 @@ export default function gentleAgents(pi: ExtensionAPI, env: NodeJS.ProcessEnv =
|
|
|
1434
1593
|
});
|
|
1435
1594
|
|
|
1436
1595
|
tool("reply", "Reply once to a live query from a child of the current parent session.", { required: ["task_id", "request_id", "message"], properties: { task_id: { type: "string" }, request_id: { type: "string" }, message: { type: "string" } } }, async (params, ctx) => {
|
|
1437
|
-
const
|
|
1596
|
+
const taskId = String(params.task_id);
|
|
1597
|
+
const requestId = String(params.request_id);
|
|
1598
|
+
const accepted = await runner.reply(taskId, requestId, typeof params.message === "string" ? params.message : "", ctx.sessionManager.getSessionId() ?? "");
|
|
1438
1599
|
return accepted ? text("Reply accepted for delivery.") : text("Error: query is unavailable.", { error: "query unavailable" });
|
|
1439
1600
|
});
|
|
1440
1601
|
|
|
1441
1602
|
tool("cancel", "Cancel a queued or running subagent task.", { required: ["task_id"], properties: { task_id: { type: "string" } } }, async (params) => {
|
|
1442
1603
|
const id = String(params.task_id);
|
|
1604
|
+
messages.invalidateTask(id);
|
|
1443
1605
|
return runner.cancel(id, "cancelled by the cancel tool") ? text(`Cancelled task ${id}.`) : text(`Error: task ${id} is not running.`, { error: "not running" });
|
|
1444
1606
|
});
|
|
1445
1607
|
|
|
@@ -1451,24 +1613,22 @@ export default function gentleAgents(pi: ExtensionAPI, env: NodeJS.ProcessEnv =
|
|
|
1451
1613
|
tool(
|
|
1452
1614
|
"continue",
|
|
1453
1615
|
"Resume a finished subagent task in its own session with a follow-up prompt.",
|
|
1454
|
-
{ required: ["task_id", "prompt"], properties: {
|
|
1616
|
+
{ required: ["task_id", "prompt"], properties: { task_id: { type: "string" }, prompt: { type: "string" }, label: { type: "string", description: "Three to six words naming the follow-up." }, mode: { type: "string", enum: ["task", "background"] } } },
|
|
1455
1617
|
async (params, ctx, signal) => {
|
|
1618
|
+
if (Object.hasOwn(params, "sdd_change") || Object.hasOwn(params, "remediation") || Object.hasOwn(params, "research_selection")) return text("Error: retired SDD delegation is not supported.", { error: "retired SDD delegation" });
|
|
1456
1619
|
const previous = await resolveTask(String(params.task_id));
|
|
1457
1620
|
if (!previous) return text(`Error: no task ${String(params.task_id)}`, { error: "unknown task" });
|
|
1621
|
+
if (retiredSddAgent(previous.agent)) return text("Error: retired SDD agents cannot be continued.", { error: "retired SDD delegation" });
|
|
1458
1622
|
if (!isFinished(previous.status) || !previous.sessionPath) return text(`Error: task ${previous.id} cannot be continued yet (${previous.status}).`, { error: "not continuable" });
|
|
1459
1623
|
// Continuing acts on the previous result, so any pending completion for
|
|
1460
1624
|
// it is already consumed by the parent.
|
|
1461
1625
|
completions.consume(previous.id);
|
|
1626
|
+
messages.invalidateTask(previous.id);
|
|
1462
1627
|
const agent = discoverAgents(roots(ctx)).agents.find((candidate) => candidate.name === previous.agent);
|
|
1463
1628
|
if (!agent) return text(`Error: subagent "${previous.agent}" is no longer defined.`, { error: "unknown agent" });
|
|
1464
1629
|
const mode = (params.mode as AgentMode | undefined) ?? (previous.mode as AgentMode);
|
|
1465
|
-
let sddChange: SddChangeSelection | undefined;
|
|
1466
|
-
try { sddChange = parseSddChange(params.sdd_change, agent.name); }
|
|
1467
|
-
catch (error) { return text(`Error: ${error instanceof Error ? error.message : String(error)}`, { error: "invalid sdd_change" }); }
|
|
1468
|
-
if (sddPhaseForAgent(agent.name) && !sddChange) return text("Error: continuing an SDD phase agent requires a fresh sdd_change selection.", { error: "missing sdd_change" });
|
|
1469
|
-
|
|
1470
1630
|
const foreignContinuation = foreignTasks.has(previous.id);
|
|
1471
|
-
return launch(ctx, await buildRequest(ctx, agent, String(params.prompt ?? ""), typeof params.label === "string" ? params.label : undefined,
|
|
1631
|
+
return launch(ctx, await buildRequest(ctx, agent, String(params.prompt ?? ""), typeof params.label === "string" ? params.label : undefined, typeof params.context === "string" ? params.context : undefined, mode, previous.sessionPath, foreignContinuation ? undefined : previous.cwd, signal, foreignContinuation ? previous.cwd : undefined), signal);
|
|
1472
1632
|
},
|
|
1473
1633
|
);
|
|
1474
1634
|
|
|
@@ -1504,6 +1664,8 @@ export default function gentleAgents(pi: ExtensionAPI, env: NodeJS.ProcessEnv =
|
|
|
1504
1664
|
// A resumed, reloaded, or replaced session starts with an empty completion
|
|
1505
1665
|
// queue so nothing pending from another session can replay here.
|
|
1506
1666
|
completions.dropAll();
|
|
1667
|
+
messages.dropAll();
|
|
1668
|
+
resetParentDelivery(ctx);
|
|
1507
1669
|
presence?.dispose();
|
|
1508
1670
|
registryFor(ctx);
|
|
1509
1671
|
showWidget(ctx);
|
|
@@ -1551,7 +1713,9 @@ export default function gentleAgents(pi: ExtensionAPI, env: NodeJS.ProcessEnv =
|
|
|
1551
1713
|
});
|
|
1552
1714
|
pi.on("session_shutdown", async () => {
|
|
1553
1715
|
completions.dropAll();
|
|
1716
|
+
messages.dropAll();
|
|
1554
1717
|
activeAgentRuns = 0;
|
|
1718
|
+
resetParentDelivery(undefined);
|
|
1555
1719
|
presence?.dispose();
|
|
1556
1720
|
presence = undefined;
|
|
1557
1721
|
rpcActivityPublisher?.stop();
|
|
@@ -1560,9 +1724,12 @@ export default function gentleAgents(pi: ExtensionAPI, env: NodeJS.ProcessEnv =
|
|
|
1560
1724
|
for (const view of overlays) { view.handleInput("q"); view.dispose(); }
|
|
1561
1725
|
overlays.clear();
|
|
1562
1726
|
sessions = undefined;
|
|
1727
|
+
stopVisualUpdates?.();
|
|
1728
|
+
stopVisualUpdates = undefined;
|
|
1563
1729
|
sidebarTui = undefined;
|
|
1564
1730
|
worktrees?.close();
|
|
1565
1731
|
worktrees = undefined;
|
|
1732
|
+
worktreeManager = undefined;
|
|
1566
1733
|
const stopped = shutdownSessionTransport();
|
|
1567
1734
|
runner.cancelAll("cancelled: parent session shut down");
|
|
1568
1735
|
await stopped;
|