gentle-pi 3.6.0 → 3.7.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/docs/gentle-shell.md +5 -4
- package/docs/readme-reference.md +11 -35
- package/extensions/gentle-agents.ts +89 -22
- package/lib/agents-runner.ts +9 -0
- package/lib/foreign-target-grants.ts +32 -0
- package/lib/native-review-cli.ts +4 -0
- package/lib/session-change-capture.ts +10 -1
- package/package.json +1 -1
- package/runtime/native-review-cli.mjs +4 -0
- package/scripts/gentle-ai-installer.mjs +10 -10
- package/scripts/verify-package-files.mjs +2 -2
- package/tests/agents-runner.test.ts +22 -0
- package/tests/foreign-target-grants.test.ts +58 -0
- package/tests/gentle-agents.test.ts +362 -2
- package/tests/gentle-ai-binary.test.ts +1 -1
- package/tests/gentle-ai-installer.test.ts +50 -50
- package/tests/native-review-capability-contract.test.ts +7 -1
- package/tests/package-manifest.test.ts +6 -6
- package/tests/session-change-capture.test.ts +12 -1
package/docs/gentle-shell.md
CHANGED
|
@@ -15,7 +15,7 @@ The [v2.6.0 release](https://github.com/Gentleman-Programming/gentle-pi/releases
|
|
|
15
15
|
- The Agents List and Details views preserve the orchestrator/session hierarchy and completion, abort, and lost-exit history. Parent-child queries and notifications have an explicit handoff path, while model, effort, and usage stay observable per task.
|
|
16
16
|
- Named `/gentle:profiles` atomically route the orchestrator separately from packaged and review roles; see the [technical reference](readme-reference.md#agent-model-profiles) for the profile model.
|
|
17
17
|
|
|
18
|
-
The source checkout
|
|
18
|
+
The source checkout prepares `gentle-pi` `3.7.0` with a package-local Gentle AI `v3.7.0` pin; this does not imply that the package release has been published.
|
|
19
19
|
|
|
20
20
|
## Shell interactions and runtime behavior
|
|
21
21
|
|
|
@@ -71,7 +71,7 @@ Changes shows **captured write/edit operations from this agent session and its o
|
|
|
71
71
|
- A worktree appears only after a captured successful mutation. Reading a file, opening a directory, registering a worktree, or launching a child is not mutation evidence.
|
|
72
72
|
- Diffs compare the content observed before the agent's first captured operation with its latest captured result, not with HEAD. Consecutive agent edits combine; an agent revert removes its net change.
|
|
73
73
|
- Edits from your editor or other sessions do not update these captured diffs. If an external or unobserved edit breaks continuity before the next agent operation on the same file, the file is marked **diff unavailable**, rather than mixing ownership.
|
|
74
|
-
-
|
|
74
|
+
- Ordinary attribution accepts only worktrees in the coordinating session's Git clone. A separately authorized foreign child can contribute target-bound Changes without registering its repository as a same-clone worktree. Child evidence requires an owned, successfully spawned task, paired successful write/edit events, and matching canonical target and live grant; model claims alone are not evidence.
|
|
75
75
|
- A changed file's worktree is resolved from its own directory upward (`git rev-parse --show-toplevel` starting there, never from an ancestor's cwd), so a repository nested inside another — a project scaffolded inside a personal workspace clone, say — is always attributed to its own, inner repository, never the outer one.
|
|
76
76
|
- When changes span more than one worktree, each tree header shows that root's own branch name, `no commits yet` for an unborn branch, or `detached` only for a real detached HEAD. The label is read from Git's HEAD once per root while the overlay is open (`symbolic-ref` and `rev-parse --verify`); the overlay still never runs `status`, `diff` or a worktree scan on your behalf.
|
|
77
77
|
- **Coverage is deliberately limited to write/edit tools.** Shell commands, custom mutation tools, failed/interrupted outcomes and children without the capture extension provide no attributed diff. A missing row does not mean the repository is clean or that no other changes occurred.
|
|
@@ -179,9 +179,10 @@ The card is above the editor in every mode, including fullscreen — it is not o
|
|
|
179
179
|
|
|
180
180
|
Every subagent is its own `pi --mode rpc` child process, so the terminal never runs subagent work: the host reads JSON lines, applies each one as a small delta to a bounded per-task thread, and notifies only the listeners of that task. A task-mode child's question (`ctx.ui.select`, `confirm`, `input`, `editor`) reaches you as an ordinary pi dialog; a background child's question is dismissed. Subagents have no automatic total execution timeout: a long-running child remains live while it continues emitting RPC events. A silent child still times out through the configurable `stall_timeout_ms` watchdog (default four minutes). An announced tool call that is still running is live work, not silence, so it is bounded by `tool_stall_timeout_ms` instead (default 30 minutes, never below `stall_timeout_ms`). Closing pi stops the children that are still running.
|
|
181
181
|
|
|
182
|
-
- `subagent_list_agents`, `subagent_run` (`agent`, `task`, `label?`, `context?`, `workspace_root?`, `mode?` task or background), `subagent_status`, `subagent_result`, `subagent_list_tasks`, `subagent_reply` (one current-session reply to a live child query), `subagent_cancel`, `subagent_send_message` (steer a running child), `subagent_continue` (resume a finished task in its own session).
|
|
182
|
+
- `subagent_list_agents`, `subagent_run` (`agent`, `task`, `label?`, `context?`, `workspace_root?` or `repository_root?`, `mode?` task or background), `subagent_status`, `subagent_result`, `subagent_list_tasks`, `subagent_reply` (one current-session reply to a live child query), `subagent_cancel`, `subagent_send_message` (steer a running child), `subagent_continue` (resume a finished task in its own session).
|
|
183
183
|
- `orchestrator_session_id`, `orchestrator_list`, and `orchestrator_send_message` provide local-profile session notifications. List results advertise IDs only and reachability remains unknown. Sending selects the sole advertised peer or asks the user to choose; a successful ACK means the peer accepted the notification for delivery, not that it read or completed work. This is notification-and-ACK transport only: it has no cross-session queries, offline queue, retries, broadcasts, or read/completion guarantees. On Unix, presence records remain in the profile's private transport directory while socket endpoints use a private, profile-hashed directory below the canonical system temporary directory, keeping endpoint length independent of the profile path and at most 100 encoded bytes. The shared system temporary parent is only validated (current-user-owned without group/other write, or root/current-user-owned, world-writable, and sticky); it is never claimed, permissioned, or cleaned up by gentle-pi. On POSIX, the transport uses private Unix-domain sockets; on Windows, it uses private named pipes scoped by the current account SID, served by a package-local PowerShell helper (`runtime/windows-session-transport.ps1`): the transport selects that fixed helper, and availability and delivery depend on the helper's bounded startup and pipe checks. Notification and ACK limits remain bounded across platforms.
|
|
184
|
-
|
|
184
|
+
- `subagent_run.workspace_root` selects the parent's main worktree or an existing linked worktree in the parent's Git clone only. Validation happens before queueing; the child runs at that canonical root. Successful OS spawn registers it in the originating parent's same-clone registry, including delayed queued launches; failed spawns do not register.
|
|
185
|
+
- Alternatively, `subagent_run.repository_root` selects an explicit canonical root of an independent Git repository; the two root selectors cannot be combined. A direct interactive parent must grant that clone before queue or child session-directory writes. The grant belongs to the live parent session and canonical Git common directory: subsequent launches there can reuse it, but denial, cancellation, lost UI, reload, or changed session/repository identity fails closed. Print, RPC, and child callers cannot request a foreign target; foreign SDD and remediation launches are unsupported. Task and background launches still obey their ordinary mode restrictions. Queued tasks revalidate immediately before spawn; `subagent_continue` retains the target cwd and revalidates the grant rather than prompting to restore a lost one. Status and task details expose the cwd. Foreign children never enter the same-clone registry or its footer/widget counts. A delegation grant is not permission to review, commit, push, or deliver in the target repository.
|
|
185
186
|
- Background work requires a live interactive/RPC parent. Both `subagent_run` and `subagent_continue` reject background mode in `pi -p` before creating or spawning a task: the parent exits before it can receive a later result. Use task mode for bounded print-mode work.
|
|
186
187
|
- A background task's result comes back to the model as a `gentle-agents.result` message, drawn as a rose card, and starts a new turn when the agent is idle; the model never polls.
|
|
187
188
|
- A configured child can call `subagent_parent_message` with bounded, well-formed Unicode text. Notifications retain their existing admission semantics. A `kind: "query"` waits for one strictly correlated `subagent_reply` for at most 30 seconds; each child has at most four pending queries, and disconnect, timeout, stop, and send failure settle each request once. The current parent session alone can reply. The first admitted task-mode query ends the original tool response while its child keeps running; its eventual non-cancelled completion returns once as a follow-up only if that same session is still active. Channel closure prevents later sends and automatic retry is not provided. Peer transport, offline delivery, retries, and broadcasts are unsupported.
|
package/docs/readme-reference.md
CHANGED
|
@@ -106,7 +106,7 @@ This is guidance through existing tools, not a new CLI, phase, state engine, or
|
|
|
106
106
|
| **Skill creation workflow** | Provides the `gentle-ai-skill-creator`/`gentle-ai-skill-improver` skills, `/skill-creation` prompt, and packaged style guide for LLM-first skills. |
|
|
107
107
|
| **Delivery skills** | Includes issue-first PRs, chained PRs, work-unit commits, cognitive docs, comment writing, and Judgment Day review. |
|
|
108
108
|
| **Bounded native review** | Freezes one candidate, dispatches only controller-selected lenses, and records native authority. Review outcomes are informational; delivery follows ordinary repository policy. |
|
|
109
|
-
| **Verified native runtime** | The current source checkout provisions the exact package-local Gentle AI v3.
|
|
109
|
+
| **Verified native runtime** | The current source checkout provisions the exact package-local Gentle AI v3.7.0 runtime: signed, SHA-256-pinned release archives on Darwin/Linux and a Go SumDB-verified source build on Windows x64/arm64. It validates package-local integrity and rejects PATH, global, sibling, symlink, and mode fallbacks. |
|
|
110
110
|
| **Runtime safety** | Blocks destructive shell commands, asks for confirmation for sensitive operations, and blocks direct read/write/edit access to sensitive paths. |
|
|
111
111
|
|
|
112
112
|
## Native pointer regions
|
|
@@ -154,14 +154,14 @@ gentle-shell --link
|
|
|
154
154
|
### Path B: inside an existing pi
|
|
155
155
|
|
|
156
156
|
```bash
|
|
157
|
-
pi install npm:gentle-pi
|
|
157
|
+
pi install npm:gentle-pi
|
|
158
158
|
```
|
|
159
159
|
|
|
160
|
-
|
|
160
|
+
This installs the current npm release; select an explicit version if you need a reproducible pin. Restart Pi after installation, then run `gentle-ai sync`. Check the [published releases](https://github.com/Gentleman-Programming/gentle-shell/releases) for version-specific runtime and provider-contract pairing.
|
|
161
161
|
|
|
162
162
|
### Source checkout
|
|
163
163
|
|
|
164
|
-
This checkout
|
|
164
|
+
This checkout declares `gentle-pi` `3.7.0` with a package-local Gentle AI `v3.7.0` pin. Checkout metadata alone is not proof of npm publication; verify the registry version and its release workflow.
|
|
165
165
|
|
|
166
166
|
The native SDD status consumer accepts both the pinned producer's legacy
|
|
167
167
|
`apply`/`verify`/`remediate`/`archive` instruction record and the classical
|
|
@@ -172,7 +172,7 @@ Unknown or incomplete instruction records still fail closed.
|
|
|
172
172
|
The Pi runtime now uses native status exclusively for SDD and retires standalone
|
|
173
173
|
sync. The full chain follows completed apply to archive, where applicable delta
|
|
174
174
|
specs are composed; verification remains explicitly invokable. With the current
|
|
175
|
-
3.
|
|
175
|
+
3.7.0 pin, native still requires verification and its emitted evidence requirements;
|
|
176
176
|
a plain practical PASS report does not satisfy that legacy native gate. Pi forwards
|
|
177
177
|
those exact instructions without overriding readiness or inventing legacy evidence.
|
|
178
178
|
Classical direct-archive behavior is compatibility-tested with an identified
|
|
@@ -206,7 +206,7 @@ pi install npm:gentle-pi@3.5.1
|
|
|
206
206
|
|
|
207
207
|
RDD remains opt-in. Enable it only through an explicit user decision with `/gentle:review-mode enable`; `status` lets you inspect the mode without changing it.
|
|
208
208
|
|
|
209
|
-
The source checkout's RDD integration installs Gentle AI only into its private `.gentle-ai/` directory. Darwin and Linux use pinned release assets with asset and executable SHA-256 verification (signed archives for source pin `v3.
|
|
209
|
+
The source checkout's RDD integration installs Gentle AI only into its private `.gentle-ai/` directory. Darwin and Linux use pinned release assets with asset and executable SHA-256 verification (signed archives for source pin `v3.7.0`; raw prerelease binaries only under a prerelease pin). Windows x64 and arm64 build the exact `v3.7.0` source tag with a local Go 1.25.10+ toolchain, a sealed Go environment, `GOTOOLCHAIN=local`, and `GOSUMDB=sum.golang.org`; it does not download Go automatically. Windows provenance is Go-toolchain plus SumDB evidence and postinstall tamper detection, **not** Authenticode or protection against a malicious joint binary-and-manifest replacement. Package-private locks coordinate cooperative concurrent or crashed installers; their tombstones fail closed. A malicious same-user process with write access to package-private `node_modules` is outside that protocol because it can already replace package code, binary, or manifest, and portable Node has no pathname-delete CAS. It never uses `PATH` or a global `gentle-ai` installation. For development or offline installs only, set `GENTLE_PI_SKIP_GENTLE_AI_INSTALL=1`; native review operations then fail closed with an actionable `package-local-binary-missing` error. To recover explicitly, if `GENTLE_PI_SKIP_GENTLE_AI_INSTALL` is set, remove or unset it before changing to the installed `gentle-pi` package directory. Then run `node scripts/install-gentle-ai.mjs`. This invokes the package-owned installer without relying on a global binary or npm configuration change. A missing binary can result from skipped lifecycle scripts, but does not prove that lifecycle scripts were disabled.
|
|
210
210
|
|
|
211
211
|
Recommended companion packages, into the standalone `gentle-shell` home:
|
|
212
212
|
|
|
@@ -519,13 +519,13 @@ flowchart TD
|
|
|
519
519
|
|
|
520
520
|
VALIDATE is informational. Commit, push, PR, and release commands follow ordinary repository policy; RDD never authorizes, rewrites, consumes review state for, or blocks them. Dangerous-command safety and destructive-review consent remain independent.
|
|
521
521
|
|
|
522
|
-
For the source checkout, native contract pairing is exact: this adapter resolves only the integrity-verified package-local Gentle AI v3.
|
|
522
|
+
For the source checkout, native contract pairing is exact: this adapter resolves only the integrity-verified package-local Gentle AI v3.7.0 executable, independently hashes it, then negotiates `gentle-ai.review-integration/v2` outside the repository. Capabilities are cached by that executable digest. Every START, target status, FINALIZE, validate, and BIND-SDD request passes the same contract identifier. Negotiated envelopes decode exactly against the vendored schemas; `recover` routes only the provider-selected `action_disposition`, and optional additions require a future compatible schema/minor that the provider explicitly advertises and the consumer negotiates.
|
|
523
523
|
|
|
524
524
|
Contract `/v2` replaces the Base64 `candidate_diff` reviewer transport of `/v1` with immutable `base_tree`/`candidate_tree` plus an ordered `changed_path_manifest` and never an inline patch. `gentle-pi` negotiates `/v2` only, with no dual-lane fallback; the cutover landed as one atomic commit against gentle-ai v2.2.2 (tracked by the `migrate-review-integration-v2` change), and the `/v1` schemas stay packaged because the `/v2` schemas `$ref` into their fragments. This provider contract version is unrelated to Pi's own internal "compact-v2" review-authority naming used below — the shared digit is coincidental, not a version pairing.
|
|
525
525
|
|
|
526
526
|
Target status owns `current_target`, `unrelated`, `ambiguous`, and `corrupted` applicability and returns one native action. Pi does not reconstruct ordinary authority from provider-private files or choose a lineage from repository-wide history. Restart recovery rebuilds only the derived candidate view from the native Git/content projection, including intended-untracked paths, symlinks, and immutable gitlink identities. Native failure envelopes retain their exact mutation outcome, replayability, required inputs, request digest, and next action. After an unknown or lost mutating result, Pi calls target status before any replay decision and returns only the provider-declared action.
|
|
527
527
|
|
|
528
|
-
Once the source checkout's pinned gentle-ai runtime (currently v3.
|
|
528
|
+
Once the source checkout's pinned gentle-ai runtime (currently v3.7.0) has written review authority, rollback MUST preserve every native store and receipt and MUST NOT run a downgraded binary against that repository. Disable the Pi route or roll forward to a compatible authority-aware release instead; deleting authority data or reinstalling an older binary is not a rollback path.
|
|
529
529
|
|
|
530
530
|
### FINALIZE wrapper input
|
|
531
531
|
|
|
@@ -1189,35 +1189,11 @@ node --experimental-strip-types --check extensions/startup-banner.ts
|
|
|
1189
1189
|
npm pack --dry-run
|
|
1190
1190
|
```
|
|
1191
1191
|
|
|
1192
|
-
###
|
|
1193
|
-
|
|
1194
|
-
The cross-lane battery (`tests/crosslane/cross-lane.mjs`) validates the adapter against a real `gentle-ai` binary, end to end and out of CI on purpose. The pinned decoder lane only ever sees vendored fixtures, so new envelope schemas and full controller sequencing are never driven through a live lifecycle before merge; the battery closes that gap.
|
|
1195
|
-
|
|
1196
|
-
```bash
|
|
1197
|
-
pnpm test:cross-lane # requires the dev-binary override
|
|
1198
|
-
pnpm test:cross-lane --with-model # adds the real Go-owned pi reviewer run (model spend)
|
|
1199
|
-
```
|
|
1200
|
-
|
|
1201
|
-
What it checks, against live scratch repositories:
|
|
1202
|
-
|
|
1203
|
-
- a low-risk lifecycle: START → native-approved FINALIZE → terminal burn; the `pre-commit` gate is informational and unmanaged, not an allow decision or retained receipt;
|
|
1204
|
-
- the medium-risk `consent/v3` granted round-trip through the direct decoder lane;
|
|
1205
|
-
- controller sequencing: each decoded offered next step equals the native transition; correction evidence precedes Go-owned targeted validation, then native approval and terminal burn leave no retained receipt;
|
|
1206
|
-
- the active audited abandon end to end, asserting the adapter builds the exact nine-line `gentle-ai.review-abandon-authorization/v2` discarded-work binding and the native gate commits the quarantine record;
|
|
1207
|
-
- after a scope change, a burned approved predecessor exposes no recoverable authority; recovered-successor hydration remains covered at unit level;
|
|
1208
|
-
- forward-decoder freshness: every live envelope captured from the binary must decode without unknown-key rejection, the early warning that gentle-ai main grew a field gentle-pi lacks;
|
|
1209
|
-
- the default no-model lane: 13 of 14 checks pass while the real-model check is intentionally skipped; Go-owned validation uses a deterministic scratch fake `pi`, and only `--with-model` runs the real locked-down reviewer with model spend.
|
|
1210
|
-
|
|
1211
|
-
Prerequisites:
|
|
1212
|
-
|
|
1213
|
-
- A real `gentle-ai` binary selected through the dev-binary override; there is no PATH or pinned-binary fallback, and the battery refuses to run without one. Either export `GENTLE_PI_GENTLE_AI_DEV_BINARY=<absolute path>` for the session, or register a persistent override with `/gentle:dev-binary <absolute path>` (stored at `~/.pi/gentle-ai/dev-binary.json` with schema `gentle-pi.dev-binary/v1`; the environment variable takes precedence over the registration, and the binary is re-validated and re-hashed on every resolution). Any real build works: an installed release binary or a locally built gentle-ai main.
|
|
1214
|
-
- A Git checkout or worktree of this repository. The battery is a contributor tool wired to the repository layout and is excluded from `pnpm test` and CI by construction; run it from the repo, not from an installed Pi package.
|
|
1215
|
-
|
|
1216
|
-
The battery owns one throwaway scratch root under the OS temp directory and never touches the enclosing repository. Before any review lifecycle it creates private `HOME`, XDG config/cache/data/state, temporary, and RDD state directories inside that root; it proves RDD starts `off/default`, explicitly opts in with sandbox-global RDD, and removes the complete root after the run. It never requires or changes the user's ambient RDD mode. The default run spends no model tokens; `--with-model` launches one real reviewer model run and costs model spend.
|
|
1192
|
+
### Cross-lane checks
|
|
1217
1193
|
|
|
1218
|
-
|
|
1194
|
+
`tests/crosslane/cross-lane.mjs` (run with `pnpm test:cross-lane`) is a single fixture parity check, not a live battery. It imports `decodeReviewLastEventClosureV1` from the pinned decoder lane, decodes the vendored fixture `tests/fixtures/devbinary/last-event-capture-result-approved.captured.json`, asserts the approved `review/capture-result` closure shape (operation, state, and the `sha256:` store revision), and exits. It needs no `gentle-ai` binary and runs offline; the pinned decoder lane only ever sees vendored fixtures.
|
|
1219
1195
|
|
|
1220
|
-
|
|
1196
|
+
The live cross-lane battery — end-to-end lifecycle checks against a real `gentle-ai` binary, out of CI on purpose — is a contributor tool of the provider repository: `scripts/cross-lane-battery.sh` in [Gentleman-Programming/gentle-ai](https://github.com/Gentleman-Programming/gentle-ai). Run it from a checkout or worktree of that repository, not from this one.
|
|
1221
1197
|
|
|
1222
1198
|
Publish npm through GitHub Actions only:
|
|
1223
1199
|
|
|
@@ -5,7 +5,9 @@ import { NativeReviewCliV216, createNodeExecFileAdapter, decodeNativeSddStatusV2
|
|
|
5
5
|
import { spawn } from "node:child_process";
|
|
6
6
|
import { recordReviewMutation } from "../lib/review-reminder-receipt.ts";
|
|
7
7
|
import { SESSION_CHANGE_RELAY } from "../lib/session-changes.ts";
|
|
8
|
+
import { publishForeignSessionChange } from "../lib/session-change-capture.ts";
|
|
8
9
|
import { SessionWorktreeRegistry, resolveSessionWorktree, type WorktreeResolver } from "../lib/session-worktree-registry.ts";
|
|
10
|
+
import { ForeignTargetGrants } from "../lib/foreign-target-grants.ts";
|
|
9
11
|
import { existsSync, mkdirSync, readFileSync, lstatSync, realpathSync } from "node:fs";
|
|
10
12
|
import { randomUUID } from "node:crypto";
|
|
11
13
|
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
@@ -24,6 +26,7 @@ import { ChildMessenger, type IpcEndpoint } from "../lib/agents-messaging.ts";
|
|
|
24
26
|
import { ActiveSessionClient, ActiveSessionListener, SessionPresenceRegistry, type PresenceRecord, type ReceivedNotification, type SentNotification, type SessionPresenceCandidate } from "../lib/agents-session-transport.ts";
|
|
25
27
|
import { WindowsActiveSessionClient, WindowsActiveSessionListener, WindowsSessionPresenceRegistry, type WindowsSessionRegistryPhaseObserver } from "../lib/windows-session-transport.ts";
|
|
26
28
|
import { hasReviewSessionPermission, resolveCanonicalGitRepositoryIdentitySync, type ReviewSessionManager } from "../lib/review-session-standing-permission.ts";
|
|
29
|
+
import { inheritedUnsafeGitEnvironmentKeys } from "../lib/review-repository.ts";
|
|
27
30
|
import { historyDir, loadHistory, loadStoredTask, pruneHistory, saveTask } from "../lib/agents-history.ts";
|
|
28
31
|
import { sessionToMarkdown } from "../lib/agents-transcript.ts";
|
|
29
32
|
import { AgentsView } from "../lib/agents-view.ts";
|
|
@@ -496,6 +499,9 @@ export default function gentleAgents(pi: ExtensionAPI, env: NodeJS.ProcessEnv =
|
|
|
496
499
|
} catch { presence?.dispose(); presence = undefined; }
|
|
497
500
|
};
|
|
498
501
|
let worktrees: SessionWorktreeRegistry | undefined;
|
|
502
|
+
const foreignGrants = new ForeignTargetGrants();
|
|
503
|
+
const foreignTasks = new Map<string, { root: string; commonDir: string; manager: ExtensionContext["sessionManager"] }>();
|
|
504
|
+
const foreignRequests = new WeakMap<TaskRequest, { root: string; commonDir: string; manager: ExtensionContext["sessionManager"] }>();
|
|
499
505
|
const registryFor = (ctx: ExtensionContext) => {
|
|
500
506
|
if (!worktrees || worktrees.sessionId !== ctx.sessionManager.getSessionId()) {
|
|
501
507
|
worktrees?.close();
|
|
@@ -761,11 +767,8 @@ export default function gentleAgents(pi: ExtensionAPI, env: NodeJS.ProcessEnv =
|
|
|
761
767
|
}
|
|
762
768
|
},
|
|
763
769
|
onSuccessfulMutation: (task, tool) => {
|
|
764
|
-
//
|
|
765
|
-
//
|
|
766
|
-
// explain a drop -- they never widen or narrow what gets attributed.
|
|
767
|
-
// The cheap session/ownership guards decide before any worktree
|
|
768
|
-
// resolution, so a foreign or stale mutation never reaches git.
|
|
770
|
+
// Same-clone registry attribution remains unchanged. A foreign task
|
|
771
|
+
// uses a separately bound, live-grant path only for successful tool evidence.
|
|
769
772
|
let root: string | undefined;
|
|
770
773
|
let childRoot: string | undefined;
|
|
771
774
|
const noteDrop = (guard: string) => {
|
|
@@ -782,13 +785,20 @@ export default function gentleAgents(pi: ExtensionAPI, env: NodeJS.ProcessEnv =
|
|
|
782
785
|
childRoot = deps.resolveWorktree(task.cwd, task.cwd)?.root;
|
|
783
786
|
if (!root) return noteDrop("root-unresolved");
|
|
784
787
|
if (root !== childRoot) return noteDrop("root-mismatch");
|
|
785
|
-
|
|
788
|
+
const foreignTask = foreignTasks.get(task.id);
|
|
789
|
+
if (foreignTask) {
|
|
790
|
+
if (sessions !== foreignTask.manager || root !== foreignTask.root || tool.evidence?.root !== root) return noteDrop("foreign-identity-mismatch");
|
|
791
|
+
const identity = resolveSessionWorktree(root, root);
|
|
792
|
+
if (!identity || identity.commonDir !== foreignTask.commonDir) return noteDrop("foreign-identity-drift");
|
|
793
|
+
try { foreignGrants.assertCurrent({ sessionManager: sessions }, identity); }
|
|
794
|
+
catch { return noteDrop("foreign-grant-lost"); }
|
|
795
|
+
} else if (!worktrees.roots().includes(root)) return noteDrop("root-not-registered");
|
|
786
796
|
if (!tool.evidence) {
|
|
787
797
|
noteDrop("evidence-missing");
|
|
788
798
|
} else if (tool.evidence.root !== root) {
|
|
789
799
|
noteDrop("evidence-root-mismatch");
|
|
790
800
|
} else {
|
|
791
|
-
let path = tool.path.replace(/^@/, "");
|
|
801
|
+
let path = tool.path.replace(/^@/, "").replace(/[\u00a0\u2000-\u200a\u202f\u205f\u3000]/g, " ");
|
|
792
802
|
if (path === "~" || path.startsWith("~/")) path = os.homedir() + path.slice(1);
|
|
793
803
|
let resolvedPath: string | undefined;
|
|
794
804
|
try {
|
|
@@ -796,10 +806,14 @@ export default function gentleAgents(pi: ExtensionAPI, env: NodeJS.ProcessEnv =
|
|
|
796
806
|
} catch {
|
|
797
807
|
noteDrop("evidence-path-unreadable");
|
|
798
808
|
}
|
|
799
|
-
if (resolvedPath === resolve(root, tool.evidence.path))
|
|
809
|
+
if (resolvedPath === resolve(root, tool.evidence.path)) {
|
|
810
|
+
const evidence = { ...tool.evidence, id: `${task.id}:${tool.toolCallId}` };
|
|
811
|
+
if (foreignTask) publishForeignSessionChange(pi, task.parentSessionId, evidence);
|
|
812
|
+
else pi.events.emit(SESSION_CHANGE_RELAY, { sessionId: task.parentSessionId, evidence });
|
|
813
|
+
}
|
|
800
814
|
else if (resolvedPath !== undefined) noteDrop("evidence-path-mismatch");
|
|
801
815
|
}
|
|
802
|
-
recordReviewMutation(pi, sessions, root, { source: "subagent", taskId: task.id, toolName: tool.toolName, toolCallId: tool.toolCallId });
|
|
816
|
+
if (!foreignTask) recordReviewMutation(pi, sessions, root, { source: "subagent", taskId: task.id, toolName: tool.toolName, toolCallId: tool.toolCallId });
|
|
803
817
|
},
|
|
804
818
|
onFinish: (task, observations) => {
|
|
805
819
|
// Completion is the only forwarding opportunity. No pending event, policy
|
|
@@ -1063,15 +1077,36 @@ export default function gentleAgents(pi: ExtensionAPI, env: NodeJS.ProcessEnv =
|
|
|
1063
1077
|
|
|
1064
1078
|
const roots = (ctx: ExtensionContext) => ({ cwd: ctx.sessionManager.getCwd(), home: deps.home, agentHome });
|
|
1065
1079
|
|
|
1066
|
-
const buildRequest = (ctx: ExtensionContext, agent: AgentDefinition, prompt: string, label: string | undefined, context: string | undefined, mode: AgentMode, resume?: string, workspaceRoot?: string, sddChange?: SddChangeSelection, researchSelection?: unknown, remediationIntent?: unknown): TaskRequest => {
|
|
1080
|
+
const buildRequest = async (ctx: ExtensionContext, agent: AgentDefinition, prompt: string, label: string | undefined, context: string | undefined, mode: AgentMode, resume?: string, workspaceRoot?: string, sddChange?: SddChangeSelection, researchSelection?: unknown, remediationIntent?: unknown, signal?: AbortSignal, repositoryRoot?: string): Promise<TaskRequest> => {
|
|
1081
|
+
if (signal?.aborted) throw new Error("Subagent launch aborted before authorization.");
|
|
1067
1082
|
const registry = registryFor(ctx);
|
|
1068
1083
|
const parentCwd = ctx.sessionManager.getCwd();
|
|
1069
1084
|
// An explicit target is validated before any queue or session-dir writes.
|
|
1070
1085
|
const parentIdentity = deps.resolveWorktree(parentCwd, parentCwd);
|
|
1071
|
-
|
|
1086
|
+
if (repositoryRoot !== undefined && workspaceRoot !== undefined) throw new Error("repository_root and workspace_root are mutually exclusive.");
|
|
1087
|
+
if (repositoryRoot !== undefined && (SHIPPED_SDD_AGENT_NAME_SET.has(agent.name) || sddChange || remediationIntent || deps.env.GENTLE_PI_AGENTS_CHILD === "1" || ctx.mode !== "tui" || !ctx.hasUI)) throw new Error("Foreign repository launch requires an interactive parent session without SDD or remediation.");
|
|
1088
|
+
const selectedRoot = repositoryRoot ?? workspaceRoot ?? sddChange?.workspaceRoot;
|
|
1072
1089
|
// Preserve ordinary non-Git continuation, without admitting any new root.
|
|
1073
|
-
const sameNonGitContinuation = resume !== undefined && selectedRoot === parentCwd && !parentIdentity;
|
|
1074
|
-
|
|
1090
|
+
const sameNonGitContinuation = resume !== undefined && repositoryRoot === undefined && selectedRoot === parentCwd && !parentIdentity;
|
|
1091
|
+
let foreign = false;
|
|
1092
|
+
let foreignIdentity: { root: string; commonDir: string } | undefined;
|
|
1093
|
+
let foreignParent: { root: string; commonDir: string } | undefined;
|
|
1094
|
+
let target: string | undefined;
|
|
1095
|
+
if (selectedRoot !== undefined && !sameNonGitContinuation) {
|
|
1096
|
+
if (repositoryRoot === undefined) target = registry.validate(selectedRoot);
|
|
1097
|
+
else {
|
|
1098
|
+
// Only an explicitly selected canonical foreign Git root can escape the
|
|
1099
|
+
// same-clone registry. Never use a caller-injected resolver for this identity.
|
|
1100
|
+
const identity = resolveSessionWorktree(selectedRoot, parentCwd);
|
|
1101
|
+
const canonicalParent = resolveSessionWorktree(parentCwd, parentCwd);
|
|
1102
|
+
if (!identity || (canonicalParent && identity.commonDir === canonicalParent.commonDir) || selectedRoot !== identity.root || !isAbsolute(selectedRoot) || realpathSync(selectedRoot) !== selectedRoot || sddChange || remediationIntent) throw new Error("repository_root must select a canonical independent Git repository.");
|
|
1103
|
+
await foreignGrants.authorize(ctx, identity, { continuation: resume !== undefined, signal });
|
|
1104
|
+
foreignIdentity = identity;
|
|
1105
|
+
foreignParent = canonicalParent;
|
|
1106
|
+
target = identity.root;
|
|
1107
|
+
foreign = true;
|
|
1108
|
+
}
|
|
1109
|
+
} else target = parentIdentity?.root;
|
|
1075
1110
|
if (sddChange && target !== sddChange.workspaceRoot && target !== resolve(sddChange.workspaceRoot)) {
|
|
1076
1111
|
throw new Error("sdd_change workspaceRoot must resolve to the selected child worktree.");
|
|
1077
1112
|
}
|
|
@@ -1086,7 +1121,7 @@ export default function gentleAgents(pi: ExtensionAPI, env: NodeJS.ProcessEnv =
|
|
|
1086
1121
|
// launch stays in the session's own worktree the identity already resolved above
|
|
1087
1122
|
// is reused instead of asking Git a second time. The orchestrator is out of
|
|
1088
1123
|
// scope: only `modelProfiles` is replaced.
|
|
1089
|
-
const pinIdentity: WorktreeResolver = target !== undefined && parentIdentity !== undefined && target === parentIdentity.root
|
|
1124
|
+
const pinIdentity: WorktreeResolver = foreign ? resolveSessionWorktree : target !== undefined && parentIdentity !== undefined && target === parentIdentity.root
|
|
1090
1125
|
? () => parentIdentity
|
|
1091
1126
|
: deps.resolveWorktree;
|
|
1092
1127
|
const config = withPinnedModelProfiles(
|
|
@@ -1100,6 +1135,12 @@ export default function gentleAgents(pi: ExtensionAPI, env: NodeJS.ProcessEnv =
|
|
|
1100
1135
|
const profile = resolveAgentProfile(agent, config);
|
|
1101
1136
|
const research = agent.name === "sdd-research" ? researchAgent(agent, pi, researchSelection) : undefined;
|
|
1102
1137
|
const sessionDir = agentRuntimePaths(deps.home, agentHome).sessions;
|
|
1138
|
+
if (foreign && target) {
|
|
1139
|
+
const identity = resolveSessionWorktree(target, parentCwd);
|
|
1140
|
+
if (!identity || identity.root !== target || identity.commonDir !== foreignIdentity?.commonDir) throw new Error("Foreign clone identity changed before launch.");
|
|
1141
|
+
foreignGrants.assertCurrent(ctx, identity);
|
|
1142
|
+
}
|
|
1143
|
+
if (signal?.aborted) throw new Error("Subagent launch aborted before queueing.");
|
|
1103
1144
|
mkdirSync(sessionDir, { recursive: true });
|
|
1104
1145
|
const parentSessionManager = ctx.sessionManager as unknown as ReviewSessionManager;
|
|
1105
1146
|
const parentSessionId = ctx.sessionManager.getSessionId() ?? "";
|
|
@@ -1108,7 +1149,9 @@ export default function gentleAgents(pi: ExtensionAPI, env: NodeJS.ProcessEnv =
|
|
|
1108
1149
|
const sddPreflightContext = SHIPPED_SDD_AGENT_NAME_SET.has(agent.name)
|
|
1109
1150
|
? extractParentConfirmedSddPreflightContext(context)
|
|
1110
1151
|
: undefined;
|
|
1111
|
-
|
|
1152
|
+
const childEnv = { ...deps.env };
|
|
1153
|
+
if (foreign) for (const key of inheritedUnsafeGitEnvironmentKeys(childEnv)) delete childEnv[key];
|
|
1154
|
+
const request: TaskRequest = {
|
|
1112
1155
|
agent: research?.agent ?? agent,
|
|
1113
1156
|
remediationIntent,
|
|
1114
1157
|
prompt,
|
|
@@ -1118,15 +1161,22 @@ export default function gentleAgents(pi: ExtensionAPI, env: NodeJS.ProcessEnv =
|
|
|
1118
1161
|
mode,
|
|
1119
1162
|
cwd: target ?? parentWorktreeRoot,
|
|
1120
1163
|
parentSessionId,
|
|
1121
|
-
...(
|
|
1164
|
+
...(foreign && target ? { beforeSpawn: () => {
|
|
1165
|
+
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
|
+
const identity = resolveSessionWorktree(target, parentCwd);
|
|
1167
|
+
const parent = resolveSessionWorktree(parentCwd, parentCwd);
|
|
1168
|
+
if (!identity || identity.root !== target || identity.commonDir !== foreignIdentity?.commonDir || parent?.root !== foreignParent?.root || parent?.commonDir !== foreignParent?.commonDir) throw new Error("Foreign clone identity changed before spawn.");
|
|
1169
|
+
foreignGrants.assertCurrent(ctx, identity);
|
|
1170
|
+
} } : {}),
|
|
1171
|
+
...(target === undefined || foreign ? {} : { onLaunch: () => { registry.register(target, "subagent:spawn"); } }),
|
|
1122
1172
|
model: profile.model,
|
|
1123
1173
|
thinking: profile.thinking,
|
|
1124
1174
|
sessionDir,
|
|
1125
1175
|
resumeSessionPath: resume,
|
|
1126
|
-
env: research ? { ...
|
|
1176
|
+
env: research ? { ...childEnv, [RESEARCH_CHILD_TOOLS_ENV]: JSON.stringify([...research.agent.tools, "subagent_parent_message"]) } : childEnv,
|
|
1127
1177
|
...(research ? { researchSelection, extensionPaths: research.extensionPaths } : {}),
|
|
1128
1178
|
...(launchSddChange === undefined ? {} : { sddChange: launchSddChange }),
|
|
1129
|
-
...(parentRepositoryIdentity === undefined ? {} : {
|
|
1179
|
+
...(foreign || parentRepositoryIdentity === undefined ? {} : {
|
|
1130
1180
|
authorizeParentStandingReviewPermission: (repositoryIdentity: string) => {
|
|
1131
1181
|
try {
|
|
1132
1182
|
return repositoryIdentity === parentRepositoryIdentity &&
|
|
@@ -1144,6 +1194,8 @@ export default function gentleAgents(pi: ExtensionAPI, env: NodeJS.ProcessEnv =
|
|
|
1144
1194
|
},
|
|
1145
1195
|
}),
|
|
1146
1196
|
};
|
|
1197
|
+
if (foreign && target && foreignIdentity) foreignRequests.set(request, { root: target, commonDir: foreignIdentity.commonDir, manager: ctx.sessionManager });
|
|
1198
|
+
return request;
|
|
1147
1199
|
};
|
|
1148
1200
|
|
|
1149
1201
|
const launch = async (ctx: ExtensionContext, request: TaskRequest, signal?: AbortSignal): Promise<ToolText> => {
|
|
@@ -1170,8 +1222,16 @@ export default function gentleAgents(pi: ExtensionAPI, env: NodeJS.ProcessEnv =
|
|
|
1170
1222
|
current: () => owner === metricsOwner && request.parentSessionId === activeSessionId() && runtimeMetricsEnvAllows(deps.env),
|
|
1171
1223
|
valid: () => !metrics.finished && metrics.current() };
|
|
1172
1224
|
const observe = runtimeMetricsEnvAllows(deps.env) && metricTasks.size < 256;
|
|
1225
|
+
let launched = false;
|
|
1226
|
+
let launchedTaskId: string | undefined;
|
|
1173
1227
|
const task = runner.run({ ...request, collectResponseObservations: false,
|
|
1174
|
-
onLaunch: () => {
|
|
1228
|
+
onLaunch: () => {
|
|
1229
|
+
metrics.launched = true;
|
|
1230
|
+
request.onLaunch?.();
|
|
1231
|
+
launched = true;
|
|
1232
|
+
const foreign = foreignRequests.get(request);
|
|
1233
|
+
if (foreign && launchedTaskId) foreignTasks.set(launchedTaskId, foreign);
|
|
1234
|
+
},
|
|
1175
1235
|
...(observe ? { canCollectResponseObservations: metrics.valid, prepareResponseObservations: async () => {
|
|
1176
1236
|
if (metrics.finished || owner !== metricsOwner || request.parentSessionId !== activeSessionId() || !runtimeMetricsEnvAllows(deps.env)) return false;
|
|
1177
1237
|
if (!metrics.valid()) return false;
|
|
@@ -1181,6 +1241,9 @@ export default function gentleAgents(pi: ExtensionAPI, env: NodeJS.ProcessEnv =
|
|
|
1181
1241
|
} } : {}),
|
|
1182
1242
|
});
|
|
1183
1243
|
if (observe) metricTasks.set(task.id, metrics);
|
|
1244
|
+
launchedTaskId = task.id;
|
|
1245
|
+
const foreignRequest = foreignRequests.get(request);
|
|
1246
|
+
if (launched && foreignRequest) foreignTasks.set(task.id, foreignRequest);
|
|
1184
1247
|
ownedTaskIds.add(task.id);
|
|
1185
1248
|
store.subscribe(task.id, () => { publishActivity(); requestRender(); });
|
|
1186
1249
|
if (request.mode === AGENT_MODE.BACKGROUND) return text(`Started ${task.agent} in the background as task ${task.id}. Retain that id; completion is pushed automatically. Never sleep or periodically poll subagent_status/subagent_result for completion or cache maintenance. Inspect status only at a real orchestration decision boundary; never relaunch equivalent queued/running work.`, taskDetails(task));
|
|
@@ -1326,13 +1389,16 @@ export default function gentleAgents(pi: ExtensionAPI, env: NodeJS.ProcessEnv =
|
|
|
1326
1389
|
task: { type: "string", description: "What the subagent must do, self-contained." },
|
|
1327
1390
|
label: { type: "string", description: "Three to six words naming the work, shown on the agents card, e.g. 'map footer data sources'." },
|
|
1328
1391
|
context: { type: "string", description: "Optional extra context appended to the task." },
|
|
1329
|
-
workspace_root: { type: "string", description: "Optional
|
|
1392
|
+
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
|
+
repository_root: { type: "string", description: "Optional canonical independent Git repository; requires direct interactive session-scoped consent before queueing; mutually exclusive with workspace_root." },
|
|
1330
1394
|
research_selection: RESEARCH_SELECTION_SCHEMA,
|
|
1331
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." },
|
|
1332
1396
|
mode: { type: "string", enum: ["task", "background"], description: "task waits for the result (default); background returns immediately." },
|
|
1333
1397
|
},
|
|
1334
1398
|
},
|
|
1335
1399
|
async (params, ctx, signal) => {
|
|
1400
|
+
if (Object.hasOwn(params, "repository_root") && Object.hasOwn(params, "workspace_root")) throw new Error("repository_root and workspace_root are mutually exclusive.");
|
|
1401
|
+
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.");
|
|
1336
1402
|
const { agents } = discoverAgents(roots(ctx));
|
|
1337
1403
|
const agent = agents.find((candidate) => candidate.name === params.agent);
|
|
1338
1404
|
if (!agent) return text(`Error: no subagent named "${String(params.agent)}". Known: ${agents.map((candidate) => candidate.name).join(", ") || "none"}`, { error: "unknown agent" });
|
|
@@ -1344,7 +1410,7 @@ export default function gentleAgents(pi: ExtensionAPI, env: NodeJS.ProcessEnv =
|
|
|
1344
1410
|
let sddChange: SddChangeSelection | undefined;
|
|
1345
1411
|
try { sddChange = parseSddChange(params.sdd_change, agent.name); }
|
|
1346
1412
|
catch (error) { return text(`Error: ${error instanceof Error ? error.message : String(error)}`, { error: "invalid sdd_change" }); }
|
|
1347
|
-
return launch(ctx, 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);
|
|
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);
|
|
1348
1414
|
},
|
|
1349
1415
|
);
|
|
1350
1416
|
|
|
@@ -1401,7 +1467,8 @@ export default function gentleAgents(pi: ExtensionAPI, env: NodeJS.ProcessEnv =
|
|
|
1401
1467
|
catch (error) { return text(`Error: ${error instanceof Error ? error.message : String(error)}`, { error: "invalid sdd_change" }); }
|
|
1402
1468
|
if (sddPhaseForAgent(agent.name) && !sddChange) return text("Error: continuing an SDD phase agent requires a fresh sdd_change selection.", { error: "missing sdd_change" });
|
|
1403
1469
|
|
|
1404
|
-
|
|
1470
|
+
const foreignContinuation = foreignTasks.has(previous.id);
|
|
1471
|
+
return launch(ctx, await buildRequest(ctx, agent, String(params.prompt ?? ""), typeof params.label === "string" ? params.label : undefined, previous.sddPreflightContext, mode, previous.sessionPath, foreignContinuation ? undefined : sddChange?.workspaceRoot ?? previous.cwd, sddChange, params.research_selection, params.remediation, signal, foreignContinuation ? previous.cwd : undefined), signal);
|
|
1405
1472
|
},
|
|
1406
1473
|
);
|
|
1407
1474
|
|
package/lib/agents-runner.ts
CHANGED
|
@@ -166,6 +166,9 @@ export interface TaskRequest {
|
|
|
166
166
|
// Untrusted narrowing intent; paths come only from matching host provenance.
|
|
167
167
|
researchSelection?: unknown;
|
|
168
168
|
extensionPaths?: string[];
|
|
169
|
+
// Synchronous admission recheck at dequeue, before any OS spawn. Throws fail
|
|
170
|
+
// only this task; unlike onLaunch, it must never persist Changes evidence.
|
|
171
|
+
beforeSpawn?: () => void;
|
|
169
172
|
// Captures the originating session; invoked only after successful OS spawn.
|
|
170
173
|
onLaunch?: () => void;
|
|
171
174
|
/** Default off. Parent owns policy before opting into bounded local buffering,
|
|
@@ -489,6 +492,12 @@ export class AgentRunner {
|
|
|
489
492
|
// A child that cannot start (missing pi, bad cwd) fails only its task:
|
|
490
493
|
// spawn exceptions and process errors settle without uncaught host errors.
|
|
491
494
|
private launch(id: string, request: TaskRequest): void {
|
|
495
|
+
try { request.beforeSpawn?.(); }
|
|
496
|
+
catch (error) {
|
|
497
|
+
this.store.update(id, { status: TASK_STATUS.RUNNING, startedAt: this.deps.now(), lastStep: "starting" });
|
|
498
|
+
this.finish(id, TASK_STATUS.FAILED, `could not start pi: ${error instanceof Error ? error.message : String(error)}`);
|
|
499
|
+
return;
|
|
500
|
+
}
|
|
492
501
|
const detached = this.processControl.platform !== "win32";
|
|
493
502
|
const hasParentPermissionChannel = request.authorizeParentStandingReviewPermission !== undefined;
|
|
494
503
|
const permissionChannelStdio = this.processControl.platform === "win32" ? "overlapped" : "pipe";
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import type { WorktreeIdentity } from "./session-worktree-registry.ts";
|
|
3
|
+
|
|
4
|
+
// Deliberately ephemeral: no session entries or disk persistence can restore consent.
|
|
5
|
+
export class ForeignTargetGrants {
|
|
6
|
+
private readonly grants = new Map<string, Set<string>>();
|
|
7
|
+
private manager?: ExtensionContext["sessionManager"];
|
|
8
|
+
private sessionId?: string;
|
|
9
|
+
|
|
10
|
+
async authorize(ctx: Pick<ExtensionContext, "sessionManager" | "hasUI" | "ui">, target: WorktreeIdentity, options: { continuation?: boolean; signal?: AbortSignal } = {}): Promise<void> {
|
|
11
|
+
if (options.signal?.aborted) throw new Error("Foreign clone authorization aborted.");
|
|
12
|
+
const id = ctx.sessionManager.getSessionId();
|
|
13
|
+
if (!id) throw new Error("Foreign target requires an active session identity.");
|
|
14
|
+
if (this.manager !== ctx.sessionManager || this.sessionId !== id) {
|
|
15
|
+
this.grants.clear();
|
|
16
|
+
this.manager = ctx.sessionManager;
|
|
17
|
+
this.sessionId = id;
|
|
18
|
+
}
|
|
19
|
+
if (this.grants.get(id)?.has(target.commonDir)) return;
|
|
20
|
+
if (options.continuation) throw new Error("Foreign clone continuation grant was lost; start a new explicit launch.");
|
|
21
|
+
if (!ctx.hasUI || !ctx.ui?.confirm || await ctx.ui.confirm("Authorize foreign clone subagent", `Launch a subagent in ${target.root}? Git common directory: ${target.commonDir}. This grant applies only to this live session and clone.`) !== true) throw new Error("Foreign clone requires interactive human consent before launch.");
|
|
22
|
+
if (options.signal?.aborted) throw new Error("Foreign clone authorization aborted.");
|
|
23
|
+
if (ctx.sessionManager !== this.manager || ctx.sessionManager.getSessionId() !== id) throw new Error("Session identity changed during foreign clone authorization.");
|
|
24
|
+
const grants = this.grants.get(id) ?? new Set<string>();
|
|
25
|
+
grants.add(target.commonDir);
|
|
26
|
+
this.grants.set(id, grants);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
assertCurrent(ctx: Pick<ExtensionContext, "sessionManager">, target: WorktreeIdentity): void {
|
|
30
|
+
if (ctx.sessionManager !== this.manager || !this.sessionId || ctx.sessionManager.getSessionId() !== this.sessionId || !this.grants.get(this.sessionId)?.has(target.commonDir)) throw new Error("Foreign clone grant is no longer live.");
|
|
31
|
+
}
|
|
32
|
+
}
|
package/lib/native-review-cli.ts
CHANGED
|
@@ -1044,6 +1044,10 @@ export const NATIVE_CLI_CONTRACTS = Object.freeze({
|
|
|
1044
1044
|
// changes no review-integration schema or capability source. riskEvidence and
|
|
1045
1045
|
// hint remain dark because neither is proven in Pi's negotiated START path.
|
|
1046
1046
|
"3.6.1": Object.freeze({ start: true, finalize: true, validate: true, bindSdd: true, status: true, inventory: true, reclaim: true, recover: true, abandon: true, quarantineLegacy: true, reconcileAuthority: true, repairLegacyAlias: true, mode: true, riskEvidence: false, hint: false, delivery: true }),
|
|
1047
|
+
// v3.7.0 repeats 3.6.1: the published provider-contract tar remains SHA-256
|
|
1048
|
+
// 547b68e172cc87aa297309d61624e5fc2c24d407a494b53eeb5a2b053904352c
|
|
1049
|
+
// at contract 1.2.0. No new negotiated capability is asserted.
|
|
1050
|
+
"3.7.0": Object.freeze({ start: true, finalize: true, validate: true, bindSdd: true, status: true, inventory: true, reclaim: true, recover: true, abandon: true, quarantineLegacy: true, reconcileAuthority: true, repairLegacyAlias: true, mode: true, riskEvidence: false, hint: false, delivery: true }),
|
|
1047
1051
|
});
|
|
1048
1052
|
|
|
1049
1053
|
export interface NativeReviewProcessDiagnostics {
|
|
@@ -7,6 +7,12 @@ import { resolveSessionWorktree, type WorktreeResolver } from "./session-worktre
|
|
|
7
7
|
import { SessionChanges, readChangeSnapshot, sameSnapshot, textSnapshot, isSessionChangeEvidence,
|
|
8
8
|
SESSION_CHANGE_ENTRY, SESSION_CHANGE_EVENT, SESSION_CHANGE_RELAY, type SessionChangeEvidence, type ChangeSnapshot } from "./session-changes.ts";
|
|
9
9
|
|
|
10
|
+
const foreignPublishers = new WeakMap<ExtensionAPI, (sessionId: string, evidence: SessionChangeEvidence) => void>();
|
|
11
|
+
/** Internal authenticated handoff from a successful child tool callback, never a model message or public event. */
|
|
12
|
+
export function publishForeignSessionChange(pi: ExtensionAPI, sessionId: string, evidence: SessionChangeEvidence): void {
|
|
13
|
+
foreignPublishers.get(pi)?.(sessionId, evidence);
|
|
14
|
+
}
|
|
15
|
+
|
|
10
16
|
interface Pending { sessionId: string; inputPath: string; path: string; root: string; relativePath: string; before: ChangeSnapshot; toolName: string; evidence?: SessionChangeEvidence }
|
|
11
17
|
const normalized = (text: string) => text.replace(/^\uFEFF/, "").replace(/\r\n/g, "\n");
|
|
12
18
|
const unknown = (): ChangeSnapshot => ({ kind: "unavailable", reason: "Tool snapshots could not be verified; diff unavailable." });
|
|
@@ -23,6 +29,9 @@ export function installSessionChangeCapture(pi: ExtensionAPI, env: NodeJS.Proces
|
|
|
23
29
|
const notice = store.takeNotice();
|
|
24
30
|
pi.events.emit(SESSION_CHANGE_EVENT, { sessionId: store.sessionId, ...(notice ? { notice } : {}) });
|
|
25
31
|
};
|
|
32
|
+
foreignPublishers.set(pi, (sessionId, evidence) => {
|
|
33
|
+
if (!child && current?.sessionManager.getSessionId() === sessionId && isSessionChangeEvidence(evidence)) publish(evidence);
|
|
34
|
+
});
|
|
26
35
|
pi.on("session_start", (_event, ctx) => {
|
|
27
36
|
pending.clear(); current = ctx;
|
|
28
37
|
store = new SessionChanges(ctx.sessionManager.getSessionId(), ctx.sessionManager.getEntries());
|
|
@@ -84,5 +93,5 @@ export function installSessionChangeCapture(pi: ExtensionAPI, env: NodeJS.Proces
|
|
|
84
93
|
try { publish(item.evidence); } catch { /* Preserve the tool's outcome. */ }
|
|
85
94
|
}
|
|
86
95
|
});
|
|
87
|
-
pi.on("session_shutdown", () => { pending.clear(); current = undefined; store = undefined; off(); });
|
|
96
|
+
pi.on("session_shutdown", () => { pending.clear(); current = undefined; store = undefined; foreignPublishers.delete(pi); off(); });
|
|
88
97
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gentle-pi",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.7.0",
|
|
4
4
|
"description": "Turn Pi into el Gentleman: a senior-architect development harness with SDD/OpenSpec, subagents, strict TDD evidence, review guardrails, and skill discovery.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -1045,6 +1045,10 @@ export const NATIVE_CLI_CONTRACTS = Object.freeze({
|
|
|
1045
1045
|
// changes no review-integration schema or capability source. riskEvidence and
|
|
1046
1046
|
// hint remain dark because neither is proven in Pi's negotiated START path.
|
|
1047
1047
|
"3.6.1": Object.freeze({ start: true, finalize: true, validate: true, bindSdd: true, status: true, inventory: true, reclaim: true, recover: true, abandon: true, quarantineLegacy: true, reconcileAuthority: true, repairLegacyAlias: true, mode: true, riskEvidence: false, hint: false, delivery: true }),
|
|
1048
|
+
// v3.7.0 repeats 3.6.1: the published provider-contract tar remains SHA-256
|
|
1049
|
+
// 547b68e172cc87aa297309d61624e5fc2c24d407a494b53eeb5a2b053904352c
|
|
1050
|
+
// at contract 1.2.0. No new negotiated capability is asserted.
|
|
1051
|
+
"3.7.0": Object.freeze({ start: true, finalize: true, validate: true, bindSdd: true, status: true, inventory: true, reclaim: true, recover: true, abandon: true, quarantineLegacy: true, reconcileAuthority: true, repairLegacyAlias: true, mode: true, riskEvidence: false, hint: false, delivery: true }),
|
|
1048
1052
|
});
|
|
1049
1053
|
|
|
1050
1054
|
|
|
@@ -36,7 +36,7 @@ const WINDOWS_SYSTEM_ROOT = "C:\\Windows";
|
|
|
36
36
|
// version check below) derives from this constant instead of repeating the
|
|
37
37
|
// literal, so a pin bump cannot leave a stale copy behind. See
|
|
38
38
|
// scripts/install-gentle-ai.mjs for the incident that motivated this.
|
|
39
|
-
export const INSTALLER_VERSION = "3.
|
|
39
|
+
export const INSTALLER_VERSION = "3.7.0";
|
|
40
40
|
export const RELEASE_BASE_URL = `https://github.com/Gentleman-Programming/gentle-ai/releases/download/v${INSTALLER_VERSION}/`;
|
|
41
41
|
export const GENTLE_AI_INSTALL_METHOD = Object.freeze({
|
|
42
42
|
SIGNED_RELEASE_ASSET: "signed-release-asset",
|
|
@@ -45,10 +45,10 @@ export const GENTLE_AI_INSTALL_METHOD = Object.freeze({
|
|
|
45
45
|
export const GENTLE_AI_WINDOWS_SOURCE_PACKAGE_PATH = "github.com/gentleman-programming/gentle-ai/v3/cmd/gentle-ai";
|
|
46
46
|
export const GENTLE_AI_WINDOWS_SOURCE_MODULE = "github.com/gentleman-programming/gentle-ai/v3";
|
|
47
47
|
export const GENTLE_AI_WINDOWS_SOURCE_TAG = `v${INSTALLER_VERSION}`;
|
|
48
|
-
// `go mod download -json github.com/gentleman-programming/gentle-ai/v3@v3.
|
|
48
|
+
// `go mod download -json github.com/gentleman-programming/gentle-ai/v3@v3.7.0`
|
|
49
49
|
// with GOSUMDB=sum.golang.org reports this exact module SumDB checksum, and the
|
|
50
|
-
// tag resolves to commit
|
|
51
|
-
export const GENTLE_AI_WINDOWS_SOURCE_MODULE_CHECKSUM = "h1:
|
|
50
|
+
// tag resolves to commit 6dee8f833aec9e46015759c5065a9035795d9af1, the published v3.7.0 release head.
|
|
51
|
+
export const GENTLE_AI_WINDOWS_SOURCE_MODULE_CHECKSUM = "h1:MQbzHlLdPklUQn0rVE9Mz94UygHsN2OPe7xMfPn9aGw=";
|
|
52
52
|
export const GENTLE_AI_WINDOWS_SOURCE_PACKAGE = `${GENTLE_AI_WINDOWS_SOURCE_PACKAGE_PATH}@${GENTLE_AI_WINDOWS_SOURCE_TAG}`;
|
|
53
53
|
export const GENTLE_AI_WINDOWS_MINIMUM_GO_VERSION = "1.25.10";
|
|
54
54
|
export const GENTLE_AI_GO_TOOLCHAIN_UNAVAILABLE_CODE = "GENTLE_AI_GO_TOOLCHAIN_UNAVAILABLE";
|
|
@@ -67,7 +67,7 @@ export class GentleAiInstallerError extends Error {
|
|
|
67
67
|
// Sentinel used while a re-pinned gentle-ai release is not yet published. A
|
|
68
68
|
// sentinel digest can never match a real SHA-256, so installation fails closed,
|
|
69
69
|
// and verify-package-files.mjs refuses to pack/publish while any digest below
|
|
70
|
-
// still holds it. The v3.
|
|
70
|
+
// still holds it. The v3.7.0 digests are pinned from the published release:
|
|
71
71
|
// archive sha256 values verified against the minisign-signed checksums.txt and
|
|
72
72
|
// freshly computed hashes; binary sha256 values computed from the extracted
|
|
73
73
|
// executables.
|
|
@@ -109,15 +109,15 @@ async function downloadPinnedGentleAiAsset(asset, destination, options) {
|
|
|
109
109
|
}
|
|
110
110
|
|
|
111
111
|
// Windows is absent from signed release archives on purpose. gentle-ai stopped
|
|
112
|
-
// distributing unsigned Windows builds in c4b764d0, so v3.
|
|
112
|
+
// distributing unsigned Windows builds in c4b764d0, so v3.7.0 publishes signed
|
|
113
113
|
// Darwin/Linux archives only. Windows x64/arm64 uses the separately verified
|
|
114
114
|
// exact-tag Go SumDB source-build path below; restore archive rows only when
|
|
115
115
|
// upstream ships signed Windows assets.
|
|
116
116
|
export const GENTLE_AI_RELEASE_ASSETS = Object.freeze({
|
|
117
|
-
"darwin/amd64": asset("gentle-ai_3.
|
|
118
|
-
"darwin/arm64": asset("gentle-ai_3.
|
|
119
|
-
"linux/amd64": asset("gentle-ai_3.
|
|
120
|
-
"linux/arm64": asset("gentle-ai_3.
|
|
117
|
+
"darwin/amd64": asset("gentle-ai_3.7.0_darwin_amd64.tar.gz", "e55ff3ee06258a90e9195c0e3a593b85f6d79cc439e9e6c04b2596725120a610", "aa30a940e3b75ac218249b3f92d17afd2b972f833258e8212175a73d8e76d5bf", "gentle-ai"),
|
|
118
|
+
"darwin/arm64": asset("gentle-ai_3.7.0_darwin_arm64.tar.gz", "66eb5740c4506cb2cfd1cc5207439c61cfe07678854f4ac7c83027a95a0e666a", "ca726cf3f9a523dc0112aa113beb634cda389e8b47edc5851254c32979bee89e", "gentle-ai"),
|
|
119
|
+
"linux/amd64": asset("gentle-ai_3.7.0_linux_amd64.tar.gz", "a730a61a43758f04cc9a4ac644945cc0e8652a1e33d6997a0a3d3f0044d2fff5", "002d09fd2b9628a29986a660c1f51f8a5042ff7fd54c8ab15b7b27de96c6cccc", "gentle-ai"),
|
|
120
|
+
"linux/arm64": asset("gentle-ai_3.7.0_linux_arm64.tar.gz", "a3a3d3a974f3d9b67d935fe9e306ae83c305da4ec1baed4a5319c10b044cd0eb", "e29546c51d8d65528bf565239ed3fc174da73c5fa3e861e64f20f84b16f28f26", "gentle-ai"),
|
|
121
121
|
});
|
|
122
122
|
|
|
123
123
|
// A pinned asset is either a signed archive or, for a prerelease pin only,
|