gentle-pi 4.1.0 → 4.3.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 +352 -128
- package/assets/agents/gentle-ai-explore.md +1 -0
- package/assets/agents/gentle-ai-verify.md +6 -4
- package/assets/agents/gentle-ai-worker.md +7 -1
- package/assets/orchestrator-delegation-reference.md +163 -0
- package/assets/orchestrator-delegation.md +25 -176
- package/assets/orchestrator-tracking.md +1 -1
- package/assets/orchestrator-verification.md +14 -8
- package/assets/orchestrator-writer.md +5 -1
- package/assets/orchestrator.md +9 -9
- package/bin/engram-guard +1 -1
- package/bin/gentle-shell.mjs +188 -35
- package/contracts/review-provider-contract-mirror/provider-contract.lock.json +8 -7
- package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/README.md +7 -0
- package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/manifest.json +11 -2
- package/contracts/review-provider-contract-mirror/v1.4.0/bundle/routing/pi.md +92 -0
- package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/generated/provider-capabilities.baseline.json +8 -1
- package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/generated/provider-roles.baseline.json +1 -1
- package/docs/assets/diagrams/agent-orchestration.svg +54 -15
- package/docs/assets/diagrams/gentleman-workflow.svg +69 -11
- package/docs/assets/diagrams/measured-routing.svg +49 -0
- package/docs/assets/diagrams/native-review.svg +52 -12
- package/docs/assets/diagrams/odd-workflow.svg +128 -66
- package/docs/assets/diagrams/permission-modes.svg +51 -0
- package/docs/assets/features/audio-notifications.png +0 -0
- package/docs/assets/features/command-palette.png +0 -0
- package/docs/auto-permission-answers.md +25 -0
- package/docs/auto-permission-evidence.md +39 -0
- package/docs/bundled-install.md +40 -8
- package/docs/delegated-verification.md +10 -8
- package/docs/gentle-agents-activity.md +11 -5
- package/docs/gentle-shell-uninstall.md +8 -1
- package/docs/gentle-shell.md +20 -12
- package/docs/install-wizard.md +17 -10
- package/docs/permission-modes.md +51 -6
- package/docs/readme-reference.md +155 -31
- package/docs/session-profile-format.md +39 -0
- package/docs/yolo-mode.md +21 -7
- package/extensions/ask-user-choice.ts +4 -0
- package/extensions/ask-user-question.ts +6 -1
- package/extensions/codegraph-tools.ts +68 -6
- package/extensions/gentle-agents.ts +146 -28
- package/extensions/gentle-ai.ts +380 -68
- package/extensions/gentle-herdr-activity.ts +2 -3
- package/extensions/gentle-shell.ts +53 -43
- package/extensions/gentle-todo.ts +62 -18
- package/extensions/quiet-tools.ts +1 -1
- package/extensions/session-scratch.ts +365 -0
- package/extensions/startup-banner.ts +2 -3
- package/lib/agent-home.ts +8 -3
- package/lib/agents-protocol.ts +3 -0
- package/lib/agents-runner.ts +55 -6
- package/lib/agents-view.ts +3 -1
- package/lib/auto-permission-answers.ts +133 -0
- package/lib/auto-permission-classifier.ts +215 -36
- package/lib/auto-permission-evidence.ts +88 -0
- package/lib/auto-permission-memory.ts +69 -0
- package/lib/auto-permission-runtime.ts +251 -35
- package/lib/auto-permission-tool-context.ts +61 -0
- package/lib/bounded-writer-admission.ts +51 -0
- package/lib/command-palette-catalog.ts +0 -4
- package/lib/commit-slice-signal.ts +100 -0
- package/lib/context-backstop.ts +84 -0
- package/lib/delivered-test-lint.ts +80 -0
- package/lib/destructive-command-guard.ts +215 -28
- package/lib/gauge-theme.ts +146 -0
- package/lib/gentle-ai-binary.ts +28 -7
- package/lib/gentle-shell-bundled.ts +62 -8
- package/lib/gentle-shell-import-pi.ts +233 -0
- package/lib/gentle-shell-launcher.ts +146 -12
- package/lib/gentle-shell-uninstall.ts +18 -1
- package/lib/herdr-activity.ts +24 -0
- package/lib/model-routing-authority.ts +6 -1
- package/lib/native-review-cli.ts +9 -1
- package/lib/orchestrator-discovery.ts +5 -1
- package/lib/orchestrator-presence.ts +9 -0
- package/lib/orchestrator-scope.ts +21 -3
- package/lib/provider-contract-bundle.ts +99 -53
- package/lib/review-candidate-view-owner.ts +29 -19
- package/lib/review-candidate-view.ts +36 -2
- package/lib/review-risk-assessment.ts +43 -38
- package/lib/review-session-standing-permission.ts +3 -1
- package/lib/selection-engine.ts +4 -1
- package/lib/session-profile-freeze.ts +40 -16
- package/lib/session-profile-persistence.ts +187 -0
- package/lib/session-scratch.ts +474 -0
- package/lib/shell-bar.ts +25 -25
- package/lib/shell-changes-view.ts +80 -13
- package/lib/shell-changes.ts +2 -0
- package/lib/shell-gauge.ts +49 -6
- package/lib/shell-prompt.ts +6 -4
- package/lib/shell-todo.ts +39 -8
- package/lib/shell-usage-view.ts +5 -1
- package/lib/shell-usage.ts +7 -7
- package/lib/verifier-admission.ts +29 -0
- package/lib/vim-editor-adapter.ts +22 -4
- package/lib/yolo-temp-scratch.ts +69 -0
- package/package.json +5 -6
- package/runtime/gentle-ai-binary.mjs +28 -7
- package/runtime/gentle-shell-bundled.mjs +61 -7
- package/runtime/gentle-shell-import-pi.mjs +234 -0
- package/runtime/gentle-shell-launcher.mjs +144 -10
- package/runtime/gentle-shell-uninstall.mjs +18 -1
- package/runtime/native-review-cli.mjs +8 -0
- package/runtime/review-risk-assessment.mjs +43 -38
- package/scripts/build-runtime-modules.mjs +1 -0
- package/scripts/bundled-engram-migration.mjs +125 -0
- package/scripts/gentle-ai-installer.mjs +10 -10
- package/scripts/install-wizard-preview.mjs +2 -2
- package/scripts/installer-downloads.mjs +7 -7
- package/scripts/installer-preflight.mjs +1 -1
- package/scripts/installer-server.mjs +14 -4
- package/scripts/main-channel.mjs +3 -2
- package/scripts/pi-update.mjs +223 -0
- package/scripts/run-test-suite.mjs +5 -0
- package/scripts/verify-package-files.mjs +19 -13
- package/skills/gentle-ai/SKILL.md +3 -3
- package/skills/work-unit-commits/SKILL.md +5 -3
- package/tests/agent-risk-escalation.test.ts +37 -10
- package/tests/agents-runner.test.ts +260 -3
- package/tests/append-system-prompt-route.test.ts +18 -1
- package/tests/auto-mode-human-answers.test.ts +68 -0
- package/tests/auto-mode-invariants.test.ts +23 -5
- package/tests/auto-mode-sdk.test.ts +29 -2
- package/tests/auto-permission-answers.test.ts +170 -0
- package/tests/auto-permission-classifier.test.ts +353 -2
- package/tests/auto-permission-compound.test.ts +24 -0
- package/tests/auto-permission-evidence.test.ts +88 -0
- package/tests/auto-permission-memory.test.ts +46 -0
- package/tests/auto-permission-public-regressions.test.ts +111 -0
- package/tests/auto-permission-runtime.test.ts +270 -12
- package/tests/autonomous-guard.test.ts +208 -16
- package/tests/bounded-writer-admission.test.ts +11 -3
- package/tests/bundled-install.test.ts +3 -3
- package/tests/card-content-policy.test.ts +19 -9
- package/tests/child-safety.test.ts +39 -0
- package/tests/codegraph-tools.test.ts +123 -2
- package/tests/command-palette.test.ts +14 -4
- package/tests/commit-slice-signal.test.ts +162 -0
- package/tests/context-backstop.test.ts +159 -0
- package/tests/delegate-for-reason-contract.test.ts +91 -24
- package/tests/delivered-test-lint.test.ts +105 -0
- package/tests/destructive-command-guard.test.ts +85 -1
- package/tests/engram-safety.test.ts +35 -7
- package/tests/fixtures/primary-session-prompt/gentleman.golden.md +143 -0
- package/tests/fixtures/provider-contract-bundle/v1.4.0/README.md +29 -0
- package/tests/fixtures/provider-contract-bundle/v1.4.0/manifest.json +83 -0
- package/tests/fixtures/provider-contract-bundle/v1.4.0/orchestration/pi.md +70 -0
- package/tests/fixtures/provider-contract-bundle/v1.4.0/routing/pi.md +92 -0
- package/tests/fixtures/provider-contract-bundle/v1.4.0/schemas/lens.schema.json +16 -0
- package/tests/fixtures/provider-contract-bundle/v1.4.0/schemas/refuter.schema.json +1 -0
- package/tests/fixtures/provider-contract-bundle/v1.4.0/schemas/targeted-validator.schema.json +1 -0
- package/tests/fixtures/provider-contract-bundle/v1.4.0/vectors/lens.json +1 -0
- package/tests/fixtures/provider-contract-bundle/v1.4.0/vectors/refuter.json +1 -0
- package/tests/fixtures/provider-contract-bundle/v1.4.0/vectors/targeted-validator.json +1 -0
- package/tests/gauge-theme.test.ts +120 -0
- package/tests/gentle-agents.test.ts +443 -8
- package/tests/gentle-ai-binary.test.ts +1 -1
- package/tests/gentle-ai-child-guards.test.ts +61 -0
- package/tests/gentle-ai-doctor-herdr.test.ts +94 -0
- package/tests/gentle-ai-installer.test.ts +51 -51
- package/tests/gentle-shell-bin.test.ts +180 -17
- package/tests/gentle-shell-bundled.test.ts +235 -38
- package/tests/gentle-shell-import-pi.test.ts +262 -0
- package/tests/gentle-shell-launcher.test.ts +97 -13
- package/tests/gentle-shell-uninstall.test.ts +30 -0
- package/tests/gentle-shell.test.ts +113 -14
- package/tests/gentle-todo.test.ts +258 -27
- package/tests/headless-delivery-question.test.ts +58 -0
- package/tests/herdr-activity.test.ts +34 -1
- package/tests/install-wizard.test.ts +2 -2
- package/tests/installer-posix-bootstrap.test.ts +8 -8
- package/tests/installer-preflight.test.ts +3 -3
- package/tests/installer-probes.test.ts +8 -8
- package/tests/installer-runner.test.ts +14 -13
- package/tests/installer-server.test.ts +13 -5
- package/tests/installer-windows-bootstrap.test.ts +33 -4
- package/tests/main-channel-upgrade.test.ts +7 -1
- package/tests/native-review-capability-contract.test.ts +7 -1
- package/tests/native-review-parity.test.ts +22 -1
- package/tests/no-self-pause-contract.test.ts +5 -2
- package/tests/odd-integration.test.ts +5 -1
- package/tests/odd-routing-canonical-ratchet.test.ts +127 -162
- package/tests/odd-routing-contract.test.ts +115 -30
- package/tests/odd-runtime-delegation-gate.test.ts +4 -1
- package/tests/orchestrator-budget.test.ts +19 -8
- package/tests/orchestrator-discovery.test.ts +29 -0
- package/tests/orchestrator-rdd-ownership.test.ts +3 -1
- package/tests/orchestrator-scope.test.ts +66 -0
- package/tests/orchestrator-status-publication-contract.test.ts +12 -8
- package/tests/package-manifest.test.ts +28 -24
- package/tests/pi-update-launcher.test.ts +106 -0
- package/tests/pi-update.test.ts +186 -0
- package/tests/primary-session-prompt.test.ts +101 -0
- package/tests/provider-contract-bundle.test.ts +118 -0
- package/tests/provider-contract-mirror.test.ts +24 -0
- package/tests/quiet-tool-rendering.test.ts +23 -0
- package/tests/rdd-aware-verification-contract.test.ts +29 -11
- package/tests/rdd-status-line.test.ts +131 -0
- package/tests/review-agent-end-preflight.test.ts +6 -0
- package/tests/review-candidate-view.test.ts +49 -0
- package/tests/review-contract-prompt.test.ts +5 -2
- package/tests/review-controller-native-routing.test.ts +14 -1
- package/tests/review-controller.test.ts +1 -1
- package/tests/review-host-relay-routing.test.ts +59 -2
- package/tests/review-loadout-rdd-off-sdk.test.ts +237 -0
- package/tests/review-owner-identity.test.ts +132 -0
- package/tests/review-risk-assessment.test.ts +163 -76
- package/tests/review-session-standing-permission-controller.test.ts +14 -1
- package/tests/review-session-standing-permission.test.ts +27 -1
- package/tests/run-test-suite.test.ts +15 -0
- package/tests/runtime-harness.mjs +16 -6
- package/tests/selection-engine.test.ts +35 -5
- package/tests/session-changes-shell.test.ts +1 -1
- package/tests/session-profile-freeze.test.ts +37 -1
- package/tests/session-profile-persistence.test.ts +334 -0
- package/tests/session-scratch.test.ts +945 -0
- package/tests/session-subject-routing-contract.test.ts +2 -1
- package/tests/shell-bar.test.ts +7 -0
- package/tests/shell-changes-view.test.ts +137 -1
- package/tests/shell-changes.test.ts +18 -0
- package/tests/shell-gauge.test.ts +42 -0
- package/tests/shell-todo.test.ts +15 -12
- package/tests/small-task-inline-contract.test.ts +37 -0
- package/tests/standalone-config-home.test.ts +324 -0
- package/tests/startup-banner.test.ts +5 -4
- package/tests/startup-home-repo.test.ts +34 -0
- package/tests/support/orchestrator-modules.ts +1 -0
- package/tests/task-size-routing-contract.test.ts +69 -9
- package/tests/test-scratch-contract.test.ts +102 -0
- package/tests/test-strength-contract.test.ts +36 -1
- package/tests/tool-batching-contract.test.ts +64 -0
- package/tests/verification-proportionality.test.ts +46 -0
- package/tests/verifier-admission.test.ts +137 -0
- package/tests/verify-agent-contract.test.ts +8 -8
- package/tests/vim-editor-adapter.test.ts +41 -15
- package/tests/windows-hidden-processes.test.ts +5 -0
- package/tests/writer-edit-surface-scope.test.ts +3 -0
- package/tests/writer-feature-document-admission.test.ts +156 -0
- package/tests/yolo-mode.test.ts +24 -1
- package/tests/yolo-temp-scratch.test.ts +71 -0
- package/scripts/mirror-odd-routing.mjs +0 -242
- /package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/orchestration/pi.md +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/schemas/lens.schema.json +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/schemas/refuter.schema.json +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/schemas/targeted-validator.schema.json +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/vectors/lens.json +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/vectors/refuter.json +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/vectors/targeted-validator.json +0 -0
package/docs/yolo-mode.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# YOLO permission
|
|
2
2
|
|
|
3
|
-
> 🚀 **Full speed, destructive actions still ask.** YOLO lets the agent implement, commit, push and open PRs within the already authorized task without asking each time, which suits long autonomous runs. Destructive operations still require fresh confirmation.
|
|
3
|
+
> 🚀 **Full speed, destructive actions still ask.** YOLO lets the agent implement, commit, push and open PRs within the already authorized task without asking each time, which suits long autonomous runs. Destructive operations still require fresh confirmation; the one exception is recursive removal of the session's own temp scratch (see [Temp scratch removal](#temp-scratch-removal)).
|
|
4
4
|
|
|
5
5
|
Use `/gentle:yolo enable` in the interactive primary Pi TUI to stop repeated permission questions for ordinary work **within your already authorized task**. YOLO is **OFF** unless you select it or save YOLO as the user-global default. [Normal, Auto and global defaults](permission-modes.md) are managed separately from the live-session toggle. The active status and a separate editor widget both say **🚀 YOLO ON 🔥 — destructive confirmations remain**, even when Gentle Shell hides its status layout.
|
|
6
6
|
|
|
@@ -29,17 +29,29 @@ The live YOLO toggle does not save preferences. The separate **Default permissio
|
|
|
29
29
|
|
|
30
30
|
YOLO supplies standing human permission for ordinary already-scoped implementation, checks, commits, non-force pushes and PR creation. Its active-only system instruction qualifies the default commit/push/PR confirmation clauses; it also tells the primary agent to make ordinary reversible implementation choices without needless interviews.
|
|
31
31
|
|
|
32
|
-
|
|
32
|
+
The shell guard's routine default confirmation is skipped for reversible delivery only. Every guarded segment of the command is judged on its own, so `git commit -m "..." && git push -u origin feature`, `git push && gh pr create --fill`, `git -C <dir> push origin main` and `git fetch && git rebase origin/main && git push` run without a prompt, while the other segments keep their ordinary guard (`git push && rm -rf data` still asks for the `rm`). A push segment is eligible with plain remote/ref arguments, `src:dst` refspecs with both sides present, and the options `-u`/`--set-upstream`, `-q`/`--quiet`, `-v`/`--verbose`, `--tags`, `--follow-tags`, `--no-verify` and `--porcelain`. A rebase segment is eligible with plain refs, `--onto`, `--continue`, `--abort`, `--skip` and `--autostash`; it is recoverable from the reflog. Either segment may also carry output redirections that write nothing (`2>&1`, `>&2`, `>/dev/null`, `2>/dev/null`, `&>/dev/null`) and may be piped into the read-only filters `tail`, `head`, `cat`, `grep` and `wc` with plain or literally quoted arguments, so `git push -u origin feature 2>&1 | tail -5` runs without a prompt. Redirection to a file, a pipe into any other command (`| sh`, `| tee file`, `| xargs`) and a trailing background `&` still ask: Git output carries text the remote controls. Anything that deletes or rewrites a remote ref, publishes, or runs arbitrary commands never qualifies: `--delete`/`-d`, `:branch` and `+branch` refspecs, `--mirror`, `--prune`, `--all`, every force spelling (hard-blocked), `--receive-pack`/`--exec`, `--repo`, `-o`, `sudo`, shell wrappers such as `sh -c`, interactive or `--exec` rebases, `git branch -D`, `pi remove` and `npm publish`. Full command evaluation, including the recognized data-loss guards, runs first. Explicit push or rebase confirmations or blocks in either configuration layer remain restrictions, including when legacy persistent autonomy or its environment override would ignore them. YOLO never changes persistent `autonomousMode`.
|
|
33
33
|
|
|
34
34
|
## What still requires a decision
|
|
35
35
|
|
|
36
|
-
- Destructive operations retain fresh confirmations or hard denial: recognized SQL DROP/TRUNCATE and broad deletion, recursive removal, destructive Git and other guarded operations are not waived. Explicit configured confirmations/blocks and sensitive-path protection remain.
|
|
36
|
+
- Destructive operations retain fresh confirmations or hard denial: recognized SQL DROP/TRUNCATE and broad deletion, recursive removal, destructive Git and other guarded operations are not waived. The only exception is [temp scratch removal](#temp-scratch-removal). Explicit configured confirmations/blocks and sensitive-path protection remain.
|
|
37
37
|
- Current explicit human restrictions, repository policy, project trust and authorized scope still apply. Scope expansion, privacy-sensitive disclosure and genuinely unresolved consequential product choices require a human decision.
|
|
38
38
|
- Ambiguous destinations or credentials remain unresolved. YOLO does not invent a remote, deployment destination, account or credential, nor authorize discovery or reuse of ambient credentials.
|
|
39
39
|
- `ask_user` tools, provider consent, maintenance/recovery authorization and opaque-token choices are never automatically answered. Native review, RDD and review standing permission remain separate and unchanged.
|
|
40
40
|
|
|
41
41
|
Children retain only their bounded delegated scope. They cannot activate or inherit YOLO, and receive no independent delivery permission.
|
|
42
42
|
|
|
43
|
+
### Temp scratch removal
|
|
44
|
+
|
|
45
|
+
In the primary session with YOLO active, a recursive `rm` whose every target is the session's own temp scratch runs without the data-loss confirmation. For example, `rm -rf /tmp/tmp.8phya1BpFr /tmp/build.log; git log --oneline -1` runs directly when both targets qualify. A target qualifies only when all of these hold:
|
|
46
|
+
|
|
47
|
+
- The segment is a plain `rm` with no wrapper (`sudo`, `env`, `xargs`, assignments), not inside `sh -c` or `find -exec`, and without `--no-preserve-root`.
|
|
48
|
+
- The removal runs before anything else in the command. The command must start with one or more such `rm` segments joined by `;`, `&&`, `||` or newlines. Nothing may precede them: no `mv`, `cp`, `ln`, redirection, subshell or other command. They also may not be backgrounded with `&` or piped with `|`. Otherwise an earlier step could put other data into a target that looked like scratch when the command was checked, so `mv ~/data /tmp/tmp.abcdef1234 && rm -rf /tmp/tmp.abcdef1234` still asks. Commands after the removals, such as `cd … && git log`, are allowed.
|
|
49
|
+
- The target is an unquoted literal absolute path made only of letters, digits, `/` and `._-+@%=:,`. It has no `$`, backtick, `~`, glob, brace or redirection characters, no `.`/`..` components and no trailing `/`.
|
|
50
|
+
- The target is exactly one path component under `/tmp`, `/var/tmp` or the resolved `os.tmpdir()`. The temp root itself never qualifies, and a temp root that is `/`, your home directory, one of its ancestors or a directory inside your home is ignored.
|
|
51
|
+
- When the command is checked, the target is missing, a symlink, a regular file you own, or a directory you own named like `mktemp` output (`tmp.` followed by six or more letters or digits).
|
|
52
|
+
|
|
53
|
+
Anything else keeps asking, including another user's directory, a directory not named like `mktemp` output, an unreadable path and `$TMPDIR/...`. Every other effect in the same command keeps its ordinary guard: `rm -rf /tmp/tmp.abcdef1234 && rm -rf data` still asks, and `rm -rf /tmp/tmp.abcdef1234 ~/foo` is still hard-blocked. Normal and Auto modes still ask, and children still block. The check runs when the command is admitted; it does not lock the path until `rm` runs.
|
|
54
|
+
|
|
43
55
|
## Lifetime and limits
|
|
44
56
|
|
|
45
57
|
Live permission is held in the extension instance, bound to the exact live SessionManager, session ID and canonical Git common-directory identity, or canonical workspace directory outside Git. Sibling worktrees in one clone may retain it; an unrelated workspace or session identity cannot. Scope loss revokes rather than hiding permission until you return.
|
|
@@ -50,7 +62,7 @@ This is permission guidance and shell-guard defense in depth, **not a sandbox or
|
|
|
50
62
|
|
|
51
63
|
## Recognized data-loss boundary
|
|
52
64
|
|
|
53
|
-
Recognized destructive database and broad filesystem commands require **fresh primary confirmation** and are **blocked in package-owned children**, independently of YOLO and existing autonomous-mode configuration.
|
|
65
|
+
Recognized destructive database and broad filesystem commands require **fresh primary confirmation** and are **blocked in package-owned children**, independently of YOLO and existing autonomous-mode configuration. The only YOLO exception is [temp scratch removal](#temp-scratch-removal).
|
|
54
66
|
|
|
55
67
|
| Recognized executable form | Primary session | Delegated child |
|
|
56
68
|
|---|---|---|
|
|
@@ -58,12 +70,13 @@ Recognized destructive database and broad filesystem commands require **fresh pr
|
|
|
58
70
|
| SQL `DELETE FROM` without `WHERE`, or a trailing literal `WHERE 1=1` / `WHERE TRUE` | Fresh confirmation | Block |
|
|
59
71
|
| Recursive `rm` (`-r`, `-R`, combined short flags, `--recursive`), `find -delete`, recognized destructive `find -exec/-execdir` and `xargs` invocations | Fresh confirmation | Block |
|
|
60
72
|
| Recursive `rm` targeting `/`, `/*`, `.`, `..`, home or home descendants | Hard block | Block |
|
|
73
|
+
| Temp scratch recursive removal ([conditions](#temp-scratch-removal)) | Waived under YOLO (primary only); fresh confirmation otherwise | Block |
|
|
61
74
|
| Git force push, hard reset, forced clean | Hard block | Block |
|
|
62
75
|
| Git forced branch deletion, reset/clean/restore/rebase, checkout with `--` or force, stash drop/clear | Existing primary policy remains | Block |
|
|
63
76
|
|
|
64
77
|
SQL recognition is limited to literal arguments passed to `psql`, `mysql`, `mariadb`, or `sqlite3`, literal `echo`/`printf` pipelines into those clients, and simple named heredocs. SQL string literals and comments are excluded from the new SQL recognition; printing or searching destructive prose alone does not trigger it.
|
|
65
78
|
|
|
66
|
-
Command recognition follows simple shell separators, pipelines and parenthesized command groups. It handles executable paths, leading assignments, common `env`, `sudo`, `command`, `exec`, `nohup`, `timeout` and `xargs` wrappers, and literal `sh/bash/zsh/dash -c` payloads (up to five recognition levels). Every recognized operation in a compound command participates; an earlier allowed delivery action cannot hide later data loss.
|
|
79
|
+
Command recognition follows simple shell separators, pipelines and parenthesized command groups. It handles executable paths, leading assignments, common `env`, `sudo`, `command`, `exec`, `nohup`, `timeout` and `xargs` wrappers, and literal `sh/bash/zsh/dash -c` payloads (up to five recognition levels). A command after the unquoted reserved words `then`, `else`, `elif`, `do`, `{`, `!`, `if`, `while` and `until` is recognized like a command at the start of a segment, so `if true; then rm -rf data; fi` and `for d in a b; do rm -rf "$d"; done` are guarded like `rm -rf data`, and a root or home target behind them is still hard-blocked. Backtick substitutions outside single quotes (also inside double quotes) and literal `eval` payloads, quoted or bare words, are recognized like literal `sh -c` payloads within the same recognition levels; `eval "$x"` and other expanded payloads are not resolved. A recursive `rm` behind a reserved word, in backticks or in `eval` never qualifies for [temp scratch removal](#temp-scratch-removal). Every recognized operation in a compound command participates; an earlier allowed delivery action cannot hide later data loss.
|
|
67
80
|
|
|
68
81
|
Ordinary builds, tests, read-only SQL, predicate-bounded literal deletes, Git status/log and plain pushes are unchanged by the shared data-loss classifier. A build command that explicitly includes recursive deletion is still guarded.
|
|
69
82
|
|
|
@@ -71,14 +84,15 @@ Ordinary builds, tests, read-only SQL, predicate-bounded literal deletes, Git st
|
|
|
71
84
|
|
|
72
85
|
1. Existing hard denies and newly recognized hard-deny forms win across the command.
|
|
73
86
|
2. Explicit configured blocks retain their existing semantics and win before data-loss confirmation. Active YOLO additionally honors explicit push restrictions in either configuration layer.
|
|
74
|
-
3. Recognized database/filesystem data loss always requires fresh confirmation, even when autonomous mode or a matched delivery action is configured to allow.
|
|
87
|
+
3. Recognized database/filesystem data loss always requires fresh confirmation, even when autonomous mode or a matched delivery action is configured to allow. The only exception is [temp scratch removal](#temp-scratch-removal) under active YOLO in the primary session.
|
|
75
88
|
4. No UI, cancellation, a non-true answer or a dialog error cannot authorize execution. Approval is not cached; permission and Herdr blocker lifecycle events remain balanced.
|
|
76
89
|
|
|
77
90
|
Children receive the lightweight `child-safety.ts` entry alongside `child-context.ts`; they do not need the full primary extension. The safety entry registers nothing outside `GENTLE_PI_AGENTS_CHILD=1`, preventing duplicate primary prompts during package auto-discovery. If the full primary extension is explicitly loaded in a child, its destructive-command path also blocks instead of prompting.
|
|
78
91
|
|
|
79
92
|
### Detection limitations
|
|
80
93
|
|
|
81
|
-
- This is deterministic lexical recognition, **not** shell or SQL evaluation. Aliases, functions, dynamically assembled commands, variable/command expansion inside quoted payloads, unusual wrapper flags, complex heredocs and deeply nested wrappers can evade recognition. Simple recursive removal of a variable target is guarded, but its expanded target cannot reliably be identified as a hard-deny path.
|
|
94
|
+
- This is deterministic lexical recognition, **not** shell or SQL evaluation. Aliases, functions, other reserved words (`time`, `function`, `coproc`), commands inside double-quoted `$(...)`, dynamically assembled commands, variable/command expansion inside quoted payloads, unusual wrapper flags, complex heredocs and deeply nested wrappers can evade recognition. Simple recursive removal of a variable target is guarded, but its expanded target cannot reliably be identified as a hard-deny path.
|
|
95
|
+
- Guarded-command confirmations and hard-denied patterns ignore text that only appears as an argument of a known data command (`grep`, `rg`, `cat`, `echo`, `printf`, `head`, `tail`, `jq`, `sort`, `uniq`, `wc` and similar), including the body of a real heredoc fed to one. Searching a file for a push or printing a reset command does not open a dialog. The exemption applies only when every command in the line is such a data command or an inert one (`cd`, `pwd`, `test`, `mkdir`, `touch`), optionally after plain `VAR=value` assignments or a flagless `env`. Plain assignment segments (`f=x;`) and simple command substitutions whose body is itself only data or inert commands (`f=$(ls -t *.jsonl | head -40); grep -l "git push" $f`) keep the exemption. Anything else anywhere in the line keeps the full lexical match: any other command or wrapper (`sed`, `sh`, `python`, `awk`, `xargs`, `sudo`, `exec`, `at`, `./script`), a command substitution that runs anything else or nests quotes, expansions or another substitution, a command substitution together with a heredoc, process substitution, backticks, a `<<` that is not an unquoted heredoc, and quoting the tokenizer cannot account for. `sed` is excluded because GNU sed can execute its script.
|
|
82
96
|
- Scripts, migration files, redirected SQL files, arbitrary interpreter programs (Python/Node/etc.), encoded payloads and remote execution are not inspected. Non-recursive wildcard deletion, file truncation, other database clients and nontrivial tautological SQL predicates are not comprehensively classified. A `WHERE` clause is not proof of bounded impact.
|
|
83
97
|
- Only `bash` tool calls reach this data-loss boundary, including nested calls dispatched through Pi's tool-event pipeline. Direct process execution, user shell commands, MCP tools and other tools are not covered by these hooks.
|
|
84
98
|
- The older primary regex safeguards remain authoritative and may be more conservative about text than the shared recognizer. Missing or explicitly overridden child extension paths remove the lightweight boundary; a valid package installation must include the safety entry.
|
|
@@ -2,6 +2,7 @@ import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-a
|
|
|
2
2
|
import { DynamicBorder } from "@earendil-works/pi-coding-agent";
|
|
3
3
|
import { Container, Input, isKeyRelease, matchesKey, Text, type KeybindingsManager, type TuiMouseEvent } from "@earendil-works/pi-tui";
|
|
4
4
|
import { type Static, Type } from "typebox";
|
|
5
|
+
import { captureChoiceAnswer } from "../lib/auto-permission-answers.ts";
|
|
5
6
|
import { NativeChoiceList } from "../lib/native-choice-list.ts";
|
|
6
7
|
import { createNativeFullscreenInteraction } from "../lib/native-fullscreen-interaction.ts";
|
|
7
8
|
import { isInteractiveMode, isInteractiveRpcHost } from "../lib/rpc-host.ts";
|
|
@@ -226,6 +227,7 @@ export default function askUserChoice(pi: ExtensionAPI): void {
|
|
|
226
227
|
parameters: ChoiceParamsSchema,
|
|
227
228
|
executionMode: "sequential",
|
|
228
229
|
async execute(_toolCallId, params: ChoiceParams, _signal, _onUpdate, ctx) {
|
|
230
|
+
const captureAnswer = captureChoiceAnswer(ctx, _toolCallId, params, _signal);
|
|
229
231
|
if (ctx.mode !== "tui") {
|
|
230
232
|
if (!isInteractiveRpcHost(ctx.mode, process.env)) {
|
|
231
233
|
throw new Error("ask_user_choice is unavailable outside the interactive TUI");
|
|
@@ -238,6 +240,7 @@ export default function askUserChoice(pi: ExtensionAPI): void {
|
|
|
238
240
|
finally {
|
|
239
241
|
pi.events.emit(ASK_USER_CHOICE_BLOCKED_EVENT, { active: false });
|
|
240
242
|
}
|
|
243
|
+
captureAnswer(rpcSelection);
|
|
241
244
|
return choiceToolResult(params, rpcSelection);
|
|
242
245
|
}
|
|
243
246
|
|
|
@@ -302,6 +305,7 @@ export default function askUserChoice(pi: ExtensionAPI): void {
|
|
|
302
305
|
pi.events.emit(ASK_USER_CHOICE_BLOCKED_EVENT, { active: false });
|
|
303
306
|
}
|
|
304
307
|
|
|
308
|
+
captureAnswer(selection);
|
|
305
309
|
return choiceToolResult(params, selection);
|
|
306
310
|
},
|
|
307
311
|
renderCall(args, theme) {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import { DynamicBorder } from "@earendil-works/pi-coding-agent";
|
|
3
3
|
import { Text } from "@earendil-works/pi-tui";
|
|
4
|
+
import { captureQuestionnaireAnswers } from "../lib/auto-permission-answers.ts";
|
|
4
5
|
import { createNativeFullscreenInteraction } from "../lib/native-fullscreen-interaction.ts";
|
|
5
6
|
import { type QuestionData, type QuestionParams, QuestionParamsSchema } from "../lib/questionnaire/schema.ts";
|
|
6
7
|
import {
|
|
@@ -264,11 +265,14 @@ export default function askUserQuestion(pi: ExtensionAPI): void {
|
|
|
264
265
|
): Promise<QuestionnaireToolResult> {
|
|
265
266
|
const error = validateQuestionnaire(params);
|
|
266
267
|
if (error) return invalidQuestionnaireResult(error);
|
|
268
|
+
const captureAnswers = captureQuestionnaireAnswers(ctx, _toolCallId, params, _signal);
|
|
267
269
|
if (ctx.mode !== "tui") {
|
|
268
270
|
if (!isInteractiveRpcHost(ctx.mode, process.env)) return unavailableResult();
|
|
269
271
|
try {
|
|
270
272
|
pi.events.emit(ASK_USER_QUESTION_BLOCKED_EVENT, { active: true });
|
|
271
|
-
|
|
273
|
+
const result = await askThroughDialogs(ctx, params);
|
|
274
|
+
if (!result.details.cancelled) captureAnswers(result.details.answers);
|
|
275
|
+
return result;
|
|
272
276
|
}
|
|
273
277
|
finally {
|
|
274
278
|
pi.events.emit(ASK_USER_QUESTION_BLOCKED_EVENT, { active: false });
|
|
@@ -307,6 +311,7 @@ export default function askUserQuestion(pi: ExtensionAPI): void {
|
|
|
307
311
|
details: { cancelled: true },
|
|
308
312
|
};
|
|
309
313
|
}
|
|
314
|
+
captureAnswers(selection.answers);
|
|
310
315
|
return {
|
|
311
316
|
content: [{ type: "text", text: answersText(selection.answers) }],
|
|
312
317
|
details: { answers: selection.answers },
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { execFile, execFileSync } from "node:child_process";
|
|
2
|
-
import { existsSync, lstatSync, readFileSync, realpathSync, statSync } from "node:fs";
|
|
2
|
+
import { appendFileSync, existsSync, lstatSync, mkdirSync, readFileSync, realpathSync, statSync } from "node:fs";
|
|
3
3
|
import { homedir, tmpdir } from "node:os";
|
|
4
|
-
import { dirname, join } from "node:path";
|
|
4
|
+
import { basename, dirname, join, resolve } from "node:path";
|
|
5
5
|
import { promisify } from "node:util";
|
|
6
6
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
7
7
|
|
|
@@ -38,11 +38,18 @@ export interface CodeGraphRunOptions {
|
|
|
38
38
|
maxBuffer: number;
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
+
interface CodeGraphLocalExclude {
|
|
42
|
+
status: "added" | "present" | "failed";
|
|
43
|
+
path?: string;
|
|
44
|
+
reason?: string;
|
|
45
|
+
}
|
|
46
|
+
|
|
41
47
|
interface CodeGraphFallbackDetails {
|
|
42
48
|
status: CodeGraphStatus;
|
|
43
49
|
operation: CodeGraphOperation;
|
|
44
50
|
cwd: string;
|
|
45
51
|
fallback: string;
|
|
52
|
+
localExclude?: CodeGraphLocalExclude;
|
|
46
53
|
}
|
|
47
54
|
|
|
48
55
|
export type CodeGraphRunner = (
|
|
@@ -109,6 +116,55 @@ function assertSafeIndexDirectory(cwd: string): void {
|
|
|
109
116
|
}
|
|
110
117
|
}
|
|
111
118
|
|
|
119
|
+
function indexExists(cwd: string): boolean {
|
|
120
|
+
try {
|
|
121
|
+
return lstatSync(join(cwd, ".codegraph")).isDirectory();
|
|
122
|
+
} catch {
|
|
123
|
+
return false;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function gitOutput(cwd: string, args: readonly string[]): string {
|
|
128
|
+
return execFileSync("git", [...args], {
|
|
129
|
+
cwd,
|
|
130
|
+
encoding: "utf8",
|
|
131
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
132
|
+
windowsHide: true,
|
|
133
|
+
}).trim();
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// CodeGraph writes .codegraph/.gitignore with `*` and `!.gitignore`, so that file
|
|
137
|
+
// stays untracked and enters every diff. Keep the whole index out of git through
|
|
138
|
+
// the worktree's local exclude file; tracked files and .gitignore are never edited.
|
|
139
|
+
const LOCAL_EXCLUDE_PATTERN = ".codegraph/";
|
|
140
|
+
const EQUIVALENT_EXCLUDE_PATTERNS = new Set([".codegraph", ".codegraph/", "/.codegraph", "/.codegraph/"]);
|
|
141
|
+
|
|
142
|
+
function excludeIndexLocally(cwd: string): CodeGraphLocalExclude | undefined {
|
|
143
|
+
let path: string | undefined;
|
|
144
|
+
try {
|
|
145
|
+
if (gitOutput(cwd, ["rev-parse", "--is-inside-work-tree"]) !== "true") return undefined;
|
|
146
|
+
// --git-path resolves the exclude file a linked worktree actually reads.
|
|
147
|
+
path = resolve(cwd, gitOutput(cwd, ["rev-parse", "--git-path", "info/exclude"]));
|
|
148
|
+
const current = existsSync(path) ? readFileSync(path, "utf8") : "";
|
|
149
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
150
|
+
path = join(realpathSync(dirname(path)), basename(path));
|
|
151
|
+
if (current.split(/\r?\n/).some((line) => EQUIVALENT_EXCLUDE_PATTERNS.has(line.trim()))) {
|
|
152
|
+
return { status: "present", path };
|
|
153
|
+
}
|
|
154
|
+
const separator = current.length === 0 || current.endsWith("\n") ? "" : "\n";
|
|
155
|
+
appendFileSync(path, `${separator}${LOCAL_EXCLUDE_PATTERN}\n`);
|
|
156
|
+
return { status: "added", path };
|
|
157
|
+
} catch (error: unknown) {
|
|
158
|
+
const reason = error instanceof Error ? error.message.split("\n")[0] : String(error);
|
|
159
|
+
return { status: "failed", ...(path ? { path } : {}), reason };
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function withLocalExcludeNote(text: string, localExclude: CodeGraphLocalExclude | undefined): string {
|
|
164
|
+
if (localExclude?.status !== "failed") return text;
|
|
165
|
+
return `${text}\n\n.codegraph/ is not excluded from git locally (${localExclude.reason}). Keep .codegraph/ out of the change.`;
|
|
166
|
+
}
|
|
167
|
+
|
|
112
168
|
function resolveLimit(limit: number | undefined): number {
|
|
113
169
|
if (limit === undefined) return DEFAULT_LIMIT;
|
|
114
170
|
if (!Number.isInteger(limit) || limit < 1 || limit > MAX_LIMIT) {
|
|
@@ -304,7 +360,7 @@ export function createCodeGraphTool(
|
|
|
304
360
|
signal: AbortSignal | undefined,
|
|
305
361
|
_onUpdate: undefined,
|
|
306
362
|
ctx: ExtensionContext,
|
|
307
|
-
): Promise<{ content: { type: "text"; text: string }[]; details: { operation: CodeGraphOperation; cwd: string; args: string[] } | CodeGraphFallbackDetails }> {
|
|
363
|
+
): Promise<{ content: { type: "text"; text: string }[]; details: { operation: CodeGraphOperation; cwd: string; args: string[]; localExclude?: CodeGraphLocalExclude } | CodeGraphFallbackDetails }> {
|
|
308
364
|
const cwd = resolveWorkspaceCwd(ctx.cwd);
|
|
309
365
|
assertSafeIndexDirectory(cwd);
|
|
310
366
|
const args = commandArguments(parameters, cwd);
|
|
@@ -313,18 +369,24 @@ export function createCodeGraphTool(
|
|
|
313
369
|
try {
|
|
314
370
|
const result = await runner(args, { cwd, signal: runSignal, maxBuffer: PROCESS_MAX_BUFFER });
|
|
315
371
|
const output = truncateOutput([result.stdout, result.stderr].filter(Boolean).join("\n"));
|
|
372
|
+
const localExclude = parameters.operation === CODEGRAPH_OPERATION.INIT || indexExists(cwd)
|
|
373
|
+
? excludeIndexLocally(cwd)
|
|
374
|
+
: undefined;
|
|
316
375
|
return {
|
|
317
|
-
content: [{ type: "text" as const, text: output || "CodeGraph completed without output." }],
|
|
318
|
-
details: { operation: parameters.operation, cwd, args },
|
|
376
|
+
content: [{ type: "text" as const, text: withLocalExcludeNote(output || "CodeGraph completed without output.", localExclude) }],
|
|
377
|
+
details: { operation: parameters.operation, cwd, args, ...(localExclude ? { localExclude } : {}) },
|
|
319
378
|
};
|
|
320
379
|
} catch (error: unknown) {
|
|
321
380
|
const details = codeGraphFailureDetails(error, parameters.operation, cwd);
|
|
381
|
+
// A failed init can still leave a partial index behind.
|
|
382
|
+
const localExclude = indexExists(cwd) ? excludeIndexLocally(cwd) : undefined;
|
|
383
|
+
if (localExclude) details.localExclude = localExclude;
|
|
322
384
|
const timedOut = timeout?.aborted === true && signal?.aborted !== true;
|
|
323
385
|
const text = timedOut
|
|
324
386
|
? `CodeGraph timed out after ${queryTimeoutMs} ms (a slow filesystem such as a WSL /mnt mount, or a very large workspace). ${FALLBACK_INSTRUCTIONS}`
|
|
325
387
|
: codeGraphFailureMessage(details.status);
|
|
326
388
|
return {
|
|
327
|
-
content: [{ type: "text" as const, text }],
|
|
389
|
+
content: [{ type: "text" as const, text: withLocalExcludeNote(text, localExclude) }],
|
|
328
390
|
details,
|
|
329
391
|
};
|
|
330
392
|
}
|
|
@@ -26,8 +26,9 @@ import { VISUAL_SETTINGS_CHANGED } from "../lib/shell-sidebar.ts";
|
|
|
26
26
|
import { resolveVisualSettings } from "../lib/visual-customization-policy.ts";
|
|
27
27
|
import { createCompletionQueue } from "../lib/agents-completion-delivery.ts";
|
|
28
28
|
import { createAgentMessageQueue, type PendingAgentMessage } from "../lib/agents-message-delivery.ts";
|
|
29
|
-
import { AGENT_MODE, discoverAgents, formatModelRef, loadAgentsConfig, resolveAgentProfile, withPinnedModelProfiles, type AgentDefinition, type AgentMode } from "../lib/agents-config.ts";
|
|
30
|
-
import {
|
|
29
|
+
import { AGENT_MODE, PROFILE_SOURCE, discoverAgents, formatModelRef, loadAgentsConfig, resolveAgentProfile, withPinnedModelProfiles, type AgentDefinition, type AgentMode, type ProfileSource } from "../lib/agents-config.ts";
|
|
30
|
+
import { freezeSessionProfileAtStartup, inheritedProfileDriftNotice, resolveSessionProfile, sessionProfileRoutingAt, type InheritedProfileOptions, type SessionProfileResolution } from "../lib/session-profile-freeze.ts";
|
|
31
|
+
import { resolveProfilePin, resolveUnversionedProjectProfile } from "../lib/agent-profile-pin.ts";
|
|
31
32
|
import { resolveBackgroundSubagentsPolicy } from "../lib/background-subagents-policy.ts";
|
|
32
33
|
import { installBackgroundCacheWarming } from "../lib/background-cache-warming.ts";
|
|
33
34
|
import { isFinished, MISSING_TOOLS_NOTE_PREFIX, TASK_EVENT, TASK_STATUS, TaskStore, type AskRequest, type TaskRecord } from "../lib/agents-protocol.ts";
|
|
@@ -49,6 +50,7 @@ import { discoverOrchestrators } from "../lib/orchestrator-discovery.ts";
|
|
|
49
50
|
import { consultPublishedMetadata, unavailableMetadata, type MetadataReceipt } from "../lib/orchestrator-consultation.ts";
|
|
50
51
|
import { HelperCostPermission } from "../lib/orchestrator-helper-consent.ts";
|
|
51
52
|
import { OrchestratorStateCache } from "../lib/orchestrator-state.ts";
|
|
53
|
+
import { TODO_PROGRESS_EVENT, type TodoProgress } from "../lib/shell-todo.ts";
|
|
52
54
|
import { normalizeTaskSubject } from "../lib/orchestrator-alias.ts";
|
|
53
55
|
import { decodeWorkDescriptor } from "../lib/orchestrator-work.ts";
|
|
54
56
|
import { validateWorkFilter, searchPublishedWork } from "../lib/orchestrator-work-search.ts";
|
|
@@ -61,7 +63,6 @@ import { CARD_TONE, renderCard } from "../lib/shell-card.ts";
|
|
|
61
63
|
import { openInExternalEditor } from "./gentle-shell.ts";
|
|
62
64
|
import { gentlePiConfigHome } from "../lib/agent-home.ts";
|
|
63
65
|
import { resolveAgentHomeDirectory } from "../lib/agent-model-resolution.ts";
|
|
64
|
-
import { resolveProfilePin, resolveUnversionedProjectProfile } from "../lib/agent-profile-pin.ts";
|
|
65
66
|
import { allowedEditSurfaces, inheritAllowedEditSurfaces, isBoundedWriter, isDevelopmentSurface, isGenericBoundedWriter, prepareBoundSessionRepository, rejectUnscopedBoundedWriterDispatch, safeBootstrapDirectory, sessionRepositoryAuthority } from "../lib/bounded-writer-admission.ts";
|
|
66
67
|
import { CHILD_METRICS_EVENT, CHILD_METRICS_REVOKED, childEvent, launchSelection, type LaunchSelection } from "../lib/runtime-metrics-children.ts";
|
|
67
68
|
import { runtimeMetricsEnvAllows, type RuntimeMetricsPolicyDeps } from "../lib/runtime-metrics-policy.ts";
|
|
@@ -187,6 +188,44 @@ function childExtensionRequest(env: NodeJS.ProcessEnv, curated: string[] | undef
|
|
|
187
188
|
return curated && curated.length > 0 ? { extensionPaths: [...curated] } : {};
|
|
188
189
|
}
|
|
189
190
|
|
|
191
|
+
// gentle-shell#2180: a short, stable token for the profile layer that supplied
|
|
192
|
+
// the launch's model, recorded next to the model in the TaskRecord so every
|
|
193
|
+
// task card shows which layer won and stale disk edits below a frozen session
|
|
194
|
+
// become visible. Derived from the resolution the request already performed;
|
|
195
|
+
// the only profile reads are the pin/declaration lookups in the paths where
|
|
196
|
+
// `sessionProfileRoutingAt` itself reads the target's own layers without
|
|
197
|
+
// carrying the origin (a foreign target under a session frozen without a
|
|
198
|
+
// profile, and `follow`/child launches), so no layer is resolved twice to route.
|
|
199
|
+
function modelSourceToken(
|
|
200
|
+
source: ProfileSource,
|
|
201
|
+
resolution: SessionProfileResolution,
|
|
202
|
+
routingTarget: InheritedProfileOptions & { foreignRepository?: boolean },
|
|
203
|
+
): string | undefined {
|
|
204
|
+
if (source === PROFILE_SOURCE.DEFINITION) return "definition";
|
|
205
|
+
// Unresolved means no layer supplied a model at all: the card already shows
|
|
206
|
+
// `default`, and naming a layer next to it would only be noise.
|
|
207
|
+
if (source === PROFILE_SOURCE.UNRESOLVED) return undefined;
|
|
208
|
+
if (source !== PROFILE_SOURCE.PROFILE) return "default";
|
|
209
|
+
if (resolution.kind === "explicit") return "binding";
|
|
210
|
+
if (resolution.kind === "inherited") {
|
|
211
|
+
if (resolution.profile !== undefined) return `frozen:${resolution.profile.origin}`;
|
|
212
|
+
// Frozen without a profile: a foreign target keeps only its own pin
|
|
213
|
+
// layers, so a winning pin there is named from the same read
|
|
214
|
+
// `sessionProfileRoutingAt` just performed; otherwise the session keeps
|
|
215
|
+
// the materialized config routing.
|
|
216
|
+
if (routingTarget.foreignRepository === true) {
|
|
217
|
+
const pin = resolveProfilePin(routingTarget);
|
|
218
|
+
return pin === undefined ? "profile" : `pin:${pin.source}`;
|
|
219
|
+
}
|
|
220
|
+
return "profile";
|
|
221
|
+
}
|
|
222
|
+
// `follow` and live child launches read the target's own defaults
|
|
223
|
+
// (pin → declaration → global active); name the layer the same read chose.
|
|
224
|
+
const pin = resolveProfilePin(routingTarget);
|
|
225
|
+
if (pin !== undefined) return `pin:${pin.source}`;
|
|
226
|
+
return resolveUnversionedProjectProfile(routingTarget.cwd, routingTarget.configHome) !== undefined ? "declaration" : "profile";
|
|
227
|
+
}
|
|
228
|
+
|
|
190
229
|
export function agentRuntimePaths(home: string, agentHome = join(home, ".pi", "agent")): { sessions: string; transcripts: string } {
|
|
191
230
|
const root = join(agentHome, "gentle-agents");
|
|
192
231
|
return { sessions: join(root, "sessions"), transcripts: join(root, "transcripts") };
|
|
@@ -550,6 +589,28 @@ export default function gentleAgents(pi: ExtensionAPI, env: NodeJS.ProcessEnv =
|
|
|
550
589
|
scopeCache.clear();
|
|
551
590
|
publishActivity();
|
|
552
591
|
});
|
|
592
|
+
// The todo list is the session's progress: publish it for peers so the model
|
|
593
|
+
// spends no turn on orchestrator_session_id. Merged into the owner's curated
|
|
594
|
+
// state; a publication that cannot be made never fails the todo write.
|
|
595
|
+
// Counts only: a todo title is private work text, never published to peers.
|
|
596
|
+
// The last automatic line survives an owner publication that omits `progress`.
|
|
597
|
+
let automaticProgress: { sessionId: string; text: string } | undefined;
|
|
598
|
+
const unsubscribeTodoProgress = pi.events.on(TODO_PROGRESS_EVENT, (event) => {
|
|
599
|
+
const progress = event as Partial<TodoProgress> | undefined;
|
|
600
|
+
if (!sessions || progress?.sessionId !== sessions.getSessionId() || typeof progress.done !== "number" || typeof progress.total !== "number") return;
|
|
601
|
+
const text = `${progress.done}/${progress.total} done`;
|
|
602
|
+
try {
|
|
603
|
+
const owned = stateCache.get(sessions);
|
|
604
|
+
// A state the owner withdrew stays withdrawn until the owner publishes again.
|
|
605
|
+
if (owned?.state === null) return;
|
|
606
|
+
const { progress: previous, ...curated } = owned?.state ?? {};
|
|
607
|
+
// A cleared list has no progress to report: drop the line, publish nothing new.
|
|
608
|
+
if (progress.total === 0 && previous === undefined) return;
|
|
609
|
+
stateCache.publish(sessions, progress.total === 0 ? curated : { ...curated, progress: text }, (type, data) => pi.appendEntry(type, data));
|
|
610
|
+
automaticProgress = progress.total === 0 ? undefined : { sessionId: progress.sessionId, text };
|
|
611
|
+
publishActivity();
|
|
612
|
+
} catch { /* stale session or over the state budget: keep the previous publication */ }
|
|
613
|
+
});
|
|
553
614
|
let worktrees: SessionWorktreeRegistry | undefined;
|
|
554
615
|
let worktreeManager: ExtensionContext["sessionManager"] | undefined;
|
|
555
616
|
let worktreeAuthority: (() => boolean) | undefined;
|
|
@@ -595,6 +656,7 @@ export default function gentleAgents(pi: ExtensionAPI, env: NodeJS.ProcessEnv =
|
|
|
595
656
|
clearTaskMetrics();
|
|
596
657
|
unsubscribeMetrics();
|
|
597
658
|
unsubscribeScope();
|
|
659
|
+
unsubscribeTodoProgress();
|
|
598
660
|
scopeCache.clear();
|
|
599
661
|
});
|
|
600
662
|
let stopAllConfirmation: Promise<void> | undefined;
|
|
@@ -1570,21 +1632,26 @@ export default function gentleAgents(pi: ExtensionAPI, env: NodeJS.ProcessEnv =
|
|
|
1570
1632
|
if (isGenericBoundedWriter(agent.name) && !current()) throw new Error("Writer session Git authority changed before admission.");
|
|
1571
1633
|
let admittedModel: string | undefined;
|
|
1572
1634
|
const surfaces = allowedEditSurfaces(prompt, context);
|
|
1573
|
-
// gentle-shell#1064 slice 2: the
|
|
1574
|
-
// before admission, so the admitted model and the launch routing
|
|
1575
|
-
// the same session layer and can never disagree about it (#1558: a
|
|
1576
|
-
// session used to kill its own non-git writer mid-preparation because
|
|
1577
|
-
// admission still read only the pin/global layers).
|
|
1578
|
-
|
|
1635
|
+
// gentle-shell#1064 slice 2: the session profile is resolved once per task
|
|
1636
|
+
// request, before admission, so the admitted model and the launch routing
|
|
1637
|
+
// resolve the same session layer and can never disagree about it (#1558: a
|
|
1638
|
+
// bound session used to kill its own non-git writer mid-preparation because
|
|
1639
|
+
// admission still read only the pin/global layers). Slice 3b-ii: the same
|
|
1640
|
+
// read covers the frozen inherited profile and `follow` mode.
|
|
1641
|
+
const profileConfigHome = gentlePiConfigHome(deps.env);
|
|
1642
|
+
const sessionProfile = resolveSessionProfile({
|
|
1643
|
+
sessionId: ctx.sessionManager.getSessionId(),
|
|
1644
|
+
cwd: originalCwd,
|
|
1645
|
+
configHome: profileConfigHome,
|
|
1646
|
+
resolveWorktree: deps.resolveWorktree,
|
|
1647
|
+
env: deps.env,
|
|
1648
|
+
});
|
|
1579
1649
|
if (!resume && isGenericBoundedWriter(agent.name) && surfaces?.some(isDevelopmentSurface) && !deps.resolveWorktree(originalCwd, originalCwd) && repositoryRoot === undefined) {
|
|
1580
1650
|
const root = safeBootstrapDirectory(originalCwd);
|
|
1581
1651
|
if (!root || (workspaceRoot !== undefined && (!isAbsolute(workspaceRoot) || safeBootstrapDirectory(workspaceRoot) !== root))) throw new Error("Writer bootstrap requires the original safe project root.");
|
|
1582
1652
|
const config = withPinnedModelProfiles(
|
|
1583
1653
|
loadAgentsConfig(roots(ctx)),
|
|
1584
|
-
|
|
1585
|
-
sessionBinding?.modelProfiles,
|
|
1586
|
-
resolveUnversionedProjectProfile(root, gentlePiConfigHome(deps.env))?.modelProfiles,
|
|
1587
|
-
),
|
|
1654
|
+
sessionProfileRoutingAt(sessionProfile, { cwd: root, configHome: profileConfigHome, resolveWorktree: deps.resolveWorktree }),
|
|
1588
1655
|
);
|
|
1589
1656
|
const model = resolveAgentProfile(agent, config).model ?? ctx.model;
|
|
1590
1657
|
const catalogModel = model?.provider ? ctx.modelRegistry?.find(model.provider, model.id) : ctx.modelRegistry?.getAll().find(candidate => candidate.id === model?.id);
|
|
@@ -1630,23 +1697,41 @@ export default function gentleAgents(pi: ExtensionAPI, env: NodeJS.ProcessEnv =
|
|
|
1630
1697
|
const pinIdentity: WorktreeResolver = foreign ? resolveSessionWorktree : target !== undefined && parentIdentity !== undefined && target === parentIdentity.root
|
|
1631
1698
|
? () => parentIdentity
|
|
1632
1699
|
: deps.resolveWorktree;
|
|
1633
|
-
// gentle-shell#1064
|
|
1634
|
-
//
|
|
1635
|
-
//
|
|
1700
|
+
// gentle-shell#1064: the session profile outranks the shared defaults for
|
|
1701
|
+
// launches from that session (`session → frozen p → P → G`), with the same
|
|
1702
|
+
// wholesale-replacement contract as the pin. Explicit and frozen profiles
|
|
1703
|
+
// belong to the session and apply to every target, a foreign repository
|
|
1704
|
+
// included; only `follow` reads the defaults of the target. The profile is
|
|
1636
1705
|
// resolved here, at task-request creation, so queued and running children
|
|
1637
1706
|
// keep the routing frozen into their requests even if the session rebinds.
|
|
1707
|
+
// The routing target this launch reads (#1064), reused to name the winning
|
|
1708
|
+
// layer (#2180) so both answers come from one resolution.
|
|
1709
|
+
const routingTarget = {
|
|
1710
|
+
cwd: target ?? parentCwd,
|
|
1711
|
+
configHome: profileConfigHome,
|
|
1712
|
+
resolveWorktree: pinIdentity,
|
|
1713
|
+
foreignRepository: foreign,
|
|
1714
|
+
};
|
|
1638
1715
|
const config = withPinnedModelProfiles(
|
|
1639
1716
|
loadAgentsConfig(roots(ctx)),
|
|
1640
|
-
|
|
1641
|
-
sessionBinding?.modelProfiles,
|
|
1642
|
-
resolveProfilePin({
|
|
1643
|
-
cwd: target ?? parentCwd,
|
|
1644
|
-
configHome: gentlePiConfigHome(deps.env),
|
|
1645
|
-
resolveWorktree: pinIdentity,
|
|
1646
|
-
})?.modelProfiles,
|
|
1647
|
-
),
|
|
1717
|
+
sessionProfileRoutingAt(sessionProfile, routingTarget),
|
|
1648
1718
|
);
|
|
1719
|
+
// Drift is measured against the defaults of the session's own directory,
|
|
1720
|
+
// where the profile was frozen, never against a delegated target.
|
|
1721
|
+
if (ctx.hasUI) {
|
|
1722
|
+
const drift = inheritedProfileDriftNotice({
|
|
1723
|
+
sessionId: ctx.sessionManager.getSessionId(),
|
|
1724
|
+
cwd: parentCwd,
|
|
1725
|
+
configHome: profileConfigHome,
|
|
1726
|
+
resolveWorktree: deps.resolveWorktree,
|
|
1727
|
+
env: deps.env,
|
|
1728
|
+
});
|
|
1729
|
+
if (drift !== undefined) ctx.ui.notify(drift, "info");
|
|
1730
|
+
}
|
|
1649
1731
|
const profile = resolveAgentProfile(agent, config);
|
|
1732
|
+
// #2180: the winning layer is named from the resolution and routing this
|
|
1733
|
+
// request already performed, never by re-resolving them.
|
|
1734
|
+
const modelSource = modelSourceToken(profile.source.model, sessionProfile, routingTarget);
|
|
1650
1735
|
if (admittedModel !== undefined) {
|
|
1651
1736
|
const model = profile.model ?? ctx.model;
|
|
1652
1737
|
const catalogModel = model?.provider ? ctx.modelRegistry?.find(model.provider, model.id) : ctx.modelRegistry?.getAll().find(candidate => candidate.id === model?.id);
|
|
@@ -1687,6 +1772,7 @@ export default function gentleAgents(pi: ExtensionAPI, env: NodeJS.ProcessEnv =
|
|
|
1687
1772
|
} } : {}),
|
|
1688
1773
|
...(target === undefined || foreign ? {} : { onLaunch: () => { registry.register(target, "subagent:spawn"); } }),
|
|
1689
1774
|
model: profile.model,
|
|
1775
|
+
...(modelSource === undefined ? {} : { modelSource }),
|
|
1690
1776
|
thinking: profile.thinking,
|
|
1691
1777
|
sessionDir,
|
|
1692
1778
|
resumeSessionPath: resume,
|
|
@@ -1826,7 +1912,7 @@ export default function gentleAgents(pi: ExtensionAPI, env: NodeJS.ProcessEnv =
|
|
|
1826
1912
|
pi.registerTool({
|
|
1827
1913
|
name: "orchestrator_session_id",
|
|
1828
1914
|
label: "Orchestrator session ID",
|
|
1829
|
-
description: "Return this host session's stable routing ID, Initial alias (FIRST TASK), Current alias (CURRENT TASK), and separate human Session name. Explicit subjects update Current even when named; Initial stays the first declared branch topic, unknown for legacy history. Before delegation or cross-session coordination, declare a short recognizable subject here. Do not require a subject declaration for small direct tasks; do not query all peers. Names never authenticate. Existing Pi names and human renames are preserved. Use a concise non-sensitive label, not a prompt.
|
|
1915
|
+
description: "Return this host session's stable routing ID, Initial alias (FIRST TASK), Current alias (CURRENT TASK), and separate human Session name. Explicit subjects update Current even when named; Initial stays the first declared branch topic, unknown for legacy history. Before delegation or cross-session coordination, declare a short recognizable subject here. Do not require a subject declaration for small direct tasks; do not query all peers. Names never authenticate. Existing Pi names and human renames are preserved. Use a concise non-sensitive label, not a prompt. Progress is published automatically from the todo list; publish state here only for objective, decisions, blockers, or work, never just to report progress. Owner-curated state (2048 UTF-8 bytes total): null withdraws, omission leaves unchanged. Never include credentials, internal instructions, or raw prompts. Historical notes are not consent or an owner reply.",
|
|
1830
1916
|
...orchestratorToolRenderers("session", expandHint),
|
|
1831
1917
|
parameters: { type: "object", additionalProperties: false, properties: {
|
|
1832
1918
|
subject: { type: "string", maxLength: 120, description: "Optional short task subject; updates task aliases independently of human names. Also names an unnamed Pi session." },
|
|
@@ -1848,7 +1934,20 @@ export default function gentleAgents(pi: ExtensionAPI, env: NodeJS.ProcessEnv =
|
|
|
1848
1934
|
if (!transport) return text("Error: session messaging is not ready.", { error: "not ready" });
|
|
1849
1935
|
const { subject, state } = params as { subject?: unknown; state?: unknown };
|
|
1850
1936
|
const declared = normalizeTaskSubject(subject);
|
|
1851
|
-
|
|
1937
|
+
// An owner object without `progress` keeps the automatic todo progress still on
|
|
1938
|
+
// display; null and an explicit `progress` win, and an over-budget merge does not fail the call.
|
|
1939
|
+
const shown = stateCache.get(ctx.sessionManager)?.state?.progress;
|
|
1940
|
+
const merged = state && typeof state === "object" && !Array.isArray(state) && !Object.hasOwn(state, "progress")
|
|
1941
|
+
&& automaticProgress?.sessionId === ctx.sessionManager.getSessionId() && shown === automaticProgress.text
|
|
1942
|
+
? { ...state, progress: shown } : undefined;
|
|
1943
|
+
const append = (type: string, data: unknown) => pi.appendEntry(type, data);
|
|
1944
|
+
if (merged) {
|
|
1945
|
+
try { stateCache.publish(ctx.sessionManager, merged, append, Date.now(), declared); }
|
|
1946
|
+
catch (error) {
|
|
1947
|
+
if ((error as Error).message !== "invalid-published-state") throw error;
|
|
1948
|
+
stateCache.publish(ctx.sessionManager, state, append, Date.now(), declared);
|
|
1949
|
+
}
|
|
1950
|
+
} else if (state !== undefined || declared) stateCache.publish(ctx.sessionManager, state, append, Date.now(), declared);
|
|
1852
1951
|
if (activeTransportFor(ctx) !== transport) throw new Error("stale-published-state");
|
|
1853
1952
|
if (declared && !ctx.sessionManager.getSessionName?.()) pi.setSessionName(declared);
|
|
1854
1953
|
const alias = sanitizeDisplayLabel(ctx.sessionManager.getSessionName?.() ?? "");
|
|
@@ -1961,13 +2060,17 @@ export default function gentleAgents(pi: ExtensionAPI, env: NodeJS.ProcessEnv =
|
|
|
1961
2060
|
? `repository: ${fact.root} · clone: ${fact.cloneHash} · Git resolved at: ${fact.resolvedAt} (${fact.source})`
|
|
1962
2061
|
: "repository: unknown";
|
|
1963
2062
|
const rows = peers.map(peer => {
|
|
1964
|
-
const context = peer.freshness === "recent" ? ` · ${peer.aliases?.currentAlias || peer.label || "unnamed"} · Session name: ${peer.label || "unnamed"} · Current alias (CURRENT TASK): ${peer.aliases?.currentAlias ?? "unknown"} · Initial alias (FIRST TASK): ${peer.aliases?.initialAlias ?? "unknown"} · recorded workspace: ${peer.workspace || "unknown"}` :
|
|
2063
|
+
const context = peer.freshness === "recent" ? ` · ${peer.aliases?.currentAlias || peer.label || "unnamed"} · Session name: ${peer.label || "unnamed"} · Current alias (CURRENT TASK): ${peer.aliases?.currentAlias ?? "unknown"} · Initial alias (FIRST TASK): ${peer.aliases?.initialAlias ?? "unknown"} · recorded workspace: ${peer.workspace || "unknown"}` : peer.legacyLabel !== undefined
|
|
2064
|
+
? ` · ${peer.legacyLabel || "unnamed"} · context: legacy (this session predates discovery publication; restart it to publish its workspace and repository)`
|
|
2065
|
+
: " · context: unknown";
|
|
1965
2066
|
const tasks = peer.tasks?.map(task => `\n - ${task.label || task.id} [${task.status}] · launch workspace: ${task.workspace || "unknown"} · ${repository(peer.scope?.tasks.find(t => t.id === task.id)?.repository)}`).join("") ?? "";
|
|
1966
2067
|
const registered = peer.scope?.registered.map(fact => `\n registered: ${repository(fact)}`).join("") ?? "";
|
|
1967
2068
|
const gaps = peer.scope && !peer.scope.complete ? `\n (scope incomplete: ${peer.scope.omittedTasks} tasks, ${peer.scope.omittedRegistered} registered roots omitted)` : "";
|
|
1968
2069
|
const catalog = peer.catalog ? `\n recorded catalog (not Git identity): ${JSON.stringify(peer.catalog)}` : "\n recorded catalog: unknown";
|
|
1969
2070
|
const note = params.recipient_session_id ? `\n owner-curated recorded state (not consent or owner reply; authority none): ${peer.state ? JSON.stringify(peer.state) : "unknown"}` : "";
|
|
1970
|
-
|
|
2071
|
+
// A legacy peer never publishes its scope: say so instead of reporting an unknown.
|
|
2072
|
+
const legacy = peer.legacyLabel !== undefined;
|
|
2073
|
+
return `- ${peer.sessionId}${context} · ${legacy ? "repository: not published" : repository(peer.scope?.host)} · metadata: ${legacy ? "legacy" : peer.freshness}${tasks}${registered}${gaps}${catalog}${note}${peer.omitted ? `\n (${peer.omitted} more tasks omitted)` : ""}`;
|
|
1971
2074
|
});
|
|
1972
2075
|
return peers.length === 0 ? text("No other sessions are currently advertised. Advertisements have unknown reachability and do not guarantee a live session.") : text(`Advertised sessions (reachability is unknown):\n${rows.join("\n")}`, { gentleAgents: { candidates: peers } });
|
|
1973
2076
|
} catch {
|
|
@@ -2243,6 +2346,21 @@ export default function gentleAgents(pi: ExtensionAPI, env: NodeJS.ProcessEnv =
|
|
|
2243
2346
|
publishActivity();
|
|
2244
2347
|
});
|
|
2245
2348
|
pi.on("session_start", async (event, ctx) => {
|
|
2349
|
+
// gentle-shell#1064 slice 3b-ii: a parent session that has no explicit
|
|
2350
|
+
// profile binding freezes `p → P → G` now. This handler is parent-only (a
|
|
2351
|
+
// child returns before registering it) and runs for every reason Pi
|
|
2352
|
+
// reports, including `startup` for --resume/--continue/--session. The
|
|
2353
|
+
// freeze is in-memory and idempotent per session id: /reload keeps it, a
|
|
2354
|
+
// new process freezes again until slice 3b-i persists it as an entry.
|
|
2355
|
+
try {
|
|
2356
|
+
freezeSessionProfileAtStartup({
|
|
2357
|
+
sessionId: ctx.sessionManager.getSessionId(),
|
|
2358
|
+
cwd: ctx.sessionManager.getCwd(),
|
|
2359
|
+
configHome: gentlePiConfigHome(deps.env),
|
|
2360
|
+
resolveWorktree: deps.resolveWorktree,
|
|
2361
|
+
env: deps.env,
|
|
2362
|
+
});
|
|
2363
|
+
} catch { /* A launch freezes lazily through the same resolver. */ }
|
|
2246
2364
|
stateCache.load(ctx.sessionManager);
|
|
2247
2365
|
// A resumed, reloaded, or replaced session starts with an empty completion
|
|
2248
2366
|
// queue so nothing pending from another session can replay here.
|