gentle-pi 3.7.0 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +37 -6
- package/assets/agents/gentle-ai-explore.md +4 -4
- package/assets/agents/gentle-ai-verify.md +6 -4
- package/assets/agents/gentle-ai-worker.md +7 -9
- package/assets/orchestrator-delegation.md +40 -41
- package/assets/orchestrator-memory.md +1 -22
- package/assets/orchestrator-skills.md +1 -1
- package/assets/orchestrator.md +12 -24
- package/assets/support/strict-tdd-verify.md +4 -266
- package/assets/support/strict-tdd.md +8 -360
- package/bin/gentle-shell.mjs +254 -29
- package/docs/delegated-verification.md +26 -1
- package/docs/gentle-agents-activity.md +24 -0
- package/docs/gentle-shell.md +92 -28
- package/docs/native-authority-architecture.md +2 -2
- package/docs/prompt-history.md +280 -0
- package/docs/readme-reference.md +112 -206
- package/docs/telemetry.md +1 -1
- package/docs/yolo-mode.md +86 -0
- package/extensions/child-context.ts +26 -0
- package/extensions/child-safety.ts +23 -0
- package/extensions/gentle-agents.ts +429 -262
- package/extensions/gentle-ai.ts +781 -683
- package/extensions/gentle-shell.ts +1375 -88
- package/extensions/gentle-stats.ts +101 -0
- package/extensions/gentle-todo.ts +18 -7
- package/extensions/history/atomic-write.ts +38 -0
- package/extensions/history/hide-prompts.ts +183 -0
- package/extensions/history/index.ts +1419 -0
- package/extensions/history/load-shared-history.ts +39 -0
- package/extensions/history/selector-helpers.ts +538 -0
- package/extensions/history/session-scan.ts +233 -0
- package/extensions/history/store.ts +1119 -0
- package/extensions/nan-provider.ts +6 -0
- package/extensions/quiet-tools.ts +179 -87
- package/extensions/resume-hint.ts +60 -0
- package/extensions/skill-registry.ts +16 -12
- package/extensions/startup-banner.ts +60 -31
- package/lib/agent-assets.ts +604 -0
- package/lib/agent-profile-pin.ts +12 -0
- package/lib/agents-message-delivery.ts +181 -0
- package/lib/agents-protocol.ts +60 -0
- package/lib/agents-runner.ts +96 -98
- package/lib/agents-view.ts +26 -3
- package/lib/agents-widget.ts +16 -9
- package/lib/append-system-prompt.ts +21 -0
- package/lib/bounded-writer-admission.ts +147 -0
- package/lib/card-style-policy.ts +60 -0
- package/lib/child-context-files.ts +166 -0
- package/lib/codemode-renderer.ts +185 -0
- package/lib/command-palette-catalog.ts +3 -9
- package/lib/command-palette.ts +25 -14
- package/lib/destructive-command-guard.ts +144 -0
- package/lib/gentle-ai-elapsed-store.ts +87 -0
- package/lib/gentle-ai-renderer.ts +196 -39
- package/lib/gentle-shell-launcher.ts +24 -13
- package/lib/gentle-shell-resume-hint.ts +176 -0
- package/lib/history-capture-policy.ts +95 -0
- package/lib/model-routing-authority.ts +5 -1
- package/lib/nan-provider.ts +227 -0
- package/lib/native-review-cli.ts +49 -108
- package/lib/odd-phase-inference.ts +231 -0
- package/lib/odd-phase.ts +141 -0
- package/lib/overlay-repaint.ts +26 -0
- package/lib/pi-tui-keys.ts +53 -0
- package/lib/review-candidate-view-owner.ts +67 -17
- package/lib/review-candidate-view.ts +112 -25
- package/lib/review-reminder-receipt.ts +48 -8
- package/lib/review-risk-assessment.ts +156 -11
- package/lib/review-sidebar-state.ts +223 -0
- package/lib/selection-engine.ts +515 -0
- package/lib/session-messaging-grants.ts +135 -0
- package/lib/session-worktree-registry.ts +14 -2
- package/lib/shell-bar.ts +179 -24
- package/lib/shell-card.ts +287 -18
- package/lib/shell-changes-view.ts +2 -1
- package/lib/shell-prompt.ts +102 -6
- package/lib/shell-sidebar-layout.ts +78 -14
- package/lib/shell-sidebar.ts +15 -1
- package/lib/shell-todo.ts +23 -13
- package/lib/shell-usage-view.ts +9 -4
- package/lib/shell-usage.ts +66 -10
- package/lib/stats-collector.ts +381 -0
- package/lib/stats-view.ts +431 -0
- package/lib/theme-customization.ts +52 -0
- package/lib/vim-editor-adapter.ts +379 -0
- package/lib/vim-normal-engine.ts +154 -0
- package/lib/vim-operator-engine.ts +416 -0
- package/lib/vim-policy.ts +49 -0
- package/lib/vim-visual-engine.ts +107 -0
- package/lib/visual-customization-policy.ts +108 -0
- package/lib/visual-customize-view.ts +330 -0
- package/lib/visual-profiles.ts +228 -0
- package/lib/yolo-session-policy.ts +240 -0
- package/package.json +20 -8
- package/runtime/gentle-shell-launcher.mjs +23 -12
- package/runtime/gentle-shell-resume-hint.mjs +177 -0
- package/runtime/native-review-cli.mjs +49 -108
- package/runtime/review-risk-assessment.mjs +154 -9
- package/scripts/build-runtime-modules.mjs +1 -0
- package/scripts/gentle-ai-installer.mjs +14 -13
- package/scripts/mirror-odd-routing.mjs +2 -2
- package/scripts/run-test-suite.mjs +76 -0
- package/scripts/test-packed-runner.mjs +31 -14
- package/scripts/verify-package-files.mjs +11 -22
- package/skills/branch-pr/SKILL.md +24 -52
- package/skills/chained-pr/SKILL.md +31 -15
- package/skills/chained-pr/references/chaining-details.md +31 -20
- package/skills/gentle-ai/SKILL.md +9 -15
- package/skills/issue-creation/SKILL.md +8 -2
- package/skills/issue-creation/references/delegated-workflow-actions.md +19 -0
- package/skills/work-unit-commits/SKILL.md +4 -3
- package/tests/agent-profiles.test.ts +18 -0
- package/tests/agents-fake-child.ts +2 -2
- package/tests/agents-message-delivery.test.ts +106 -0
- package/tests/agents-protocol.test.ts +40 -0
- package/tests/agents-runner.test.ts +378 -89
- package/tests/agents-view-thread-identity.test.ts +169 -0
- package/tests/agents-view.test.ts +8 -2
- package/tests/agents-widget.test.ts +154 -15
- package/tests/append-system-prompt-route.test.ts +160 -0
- package/tests/append-system-prompt.test.ts +46 -0
- package/tests/artifact-language.test.ts +19 -213
- package/tests/ask-user-question.test.ts +44 -1
- package/tests/asset-installation-runtime.test.ts +5 -16
- package/tests/autonomous-guard.test.ts +69 -1
- package/tests/bounded-writer-admission.test.ts +95 -0
- package/tests/branch-pr-skill.test.ts +43 -0
- package/tests/card-style-policy.test.ts +55 -0
- package/tests/chained-pr-skill.test.ts +124 -0
- package/tests/child-context-files.test.ts +255 -0
- package/tests/child-safety.test.ts +82 -0
- package/tests/codemode-rendering.test.ts +491 -0
- package/tests/command-palette.test.ts +39 -3
- package/tests/delegated-key-learnings-contract.test.ts +0 -76
- package/tests/destructive-command-guard.test.ts +84 -0
- package/tests/devbinary/native-review-parity.devtest.ts +170 -2
- package/tests/devbinary/non-git-subagent-bootstrap.devtest.ts +193 -0
- package/tests/fixtures/stats/sessions/--work-alpha--/2026-09-28T10-00-00-000Z_aaa.jsonl +7 -0
- package/tests/fixtures/stats/sessions/--work-alpha--/2026-09-29T23-00-00-000Z_bbb.jsonl +3 -0
- package/tests/fixtures/stats/sessions/--work-alpha--/2026-09-30T08-00-00-000Z_ddd.jsonl +2 -0
- package/tests/fixtures/stats/sessions/--work-alpha--/2026-09-30T09-00-00-000Z_eee.jsonl +3 -0
- package/tests/fixtures/stats/sessions/--work-alpha--/run-1/session.jsonl +2 -0
- package/tests/fixtures/stats/sessions/--work-beta--/2026-09-01T12-00-00-000Z_ccc.jsonl +2 -0
- package/tests/fixtures/stats/user-pi/sessions/--work-alpha--/2026-09-28T10-00-00-000Z_aaa.jsonl +2 -0
- package/tests/fixtures/stats/user-pi/sessions/--work-alpha--/2026-09-29T23-00-00-000Z_bbb.jsonl +4 -0
- package/tests/fixtures/stats/user-pi/sessions/--work-gamma--/2026-09-20T09-00-00-000Z_fff.jsonl +3 -0
- package/tests/generic-agent-tools.test.ts +54 -0
- package/tests/gentle-agents.test.ts +1047 -252
- package/tests/gentle-ai-binary.test.ts +3 -3
- package/tests/gentle-ai-elapsed-store.test.ts +68 -0
- package/tests/gentle-ai-installer.test.ts +68 -54
- package/tests/gentle-ai-renderer.test.ts +487 -8
- package/tests/gentle-ai.test.ts +288 -65
- package/tests/gentle-card-text.ts +2 -1
- package/tests/gentle-shell-bin.test.ts +651 -114
- package/tests/gentle-shell-launcher.test.ts +99 -52
- package/tests/gentle-shell-resume-hint.test.ts +270 -0
- package/tests/gentle-shell.test.ts +3839 -187
- package/tests/gentle-stats.test.ts +152 -0
- package/tests/gentle-theme.test.ts +4 -1
- package/tests/gentle-todo.test.ts +80 -8
- package/tests/history-atomic-write.test.ts +57 -0
- package/tests/history-capture-policy.test.ts +102 -0
- package/tests/history-command-registration.test.ts +164 -0
- package/tests/history-dedupe-entries.test.ts +123 -0
- package/tests/history-delete-backfill.test.ts +190 -0
- package/tests/history-delete-confirm.test.ts +460 -0
- package/tests/history-dispatch.test.ts +180 -0
- package/tests/history-drain-hidden.test.ts +110 -0
- package/tests/history-drain-order.test.ts +98 -0
- package/tests/history-expanded-globals.test.ts +62 -0
- package/tests/history-gc.test.ts +832 -0
- package/tests/history-header-layout.test.ts +265 -0
- package/tests/history-hide-prompts.test.ts +275 -0
- package/tests/history-lazy-windowing.test.ts +508 -0
- package/tests/history-legacy-migrate-v2.test.ts +297 -0
- package/tests/history-load-shared-history.test.ts +53 -0
- package/tests/history-max-results-cap.test.ts +76 -0
- package/tests/history-multi-reader.test.ts +203 -0
- package/tests/history-off-path.test.ts +170 -0
- package/tests/history-openflow-integration.test.ts +173 -0
- package/tests/history-overlay-margin.test.ts +326 -0
- package/tests/history-preview-layout.test.ts +93 -0
- package/tests/history-registry.test.ts +143 -0
- package/tests/history-scope-delete.test.ts +411 -0
- package/tests/history-search-caret-keys.test.ts +142 -0
- package/tests/history-seed-bootstrap.test.ts +170 -0
- package/tests/history-seed-regen.test.ts +129 -0
- package/tests/history-selector-windowing.test.ts +94 -0
- package/tests/history-session-scan-directory.test.ts +87 -0
- package/tests/history-session-scan-extract.test.ts +583 -0
- package/tests/history-session-writer.test.ts +351 -0
- package/tests/history-store-paths.test.ts +79 -0
- package/tests/history-tombstone-exact.test.ts +139 -0
- package/tests/history-wheel-mouse.test.ts +242 -0
- package/tests/inprocess-reviewer.test.ts +29 -19
- package/tests/issue-creation-skill.test.ts +61 -0
- package/tests/model-routing-authority.test.ts +16 -0
- package/tests/nan-provider.test.ts +471 -0
- package/tests/native-review-capability-contract.test.ts +7 -1
- package/tests/native-review-cli.test.ts +6 -120
- package/tests/native-review-parity-runtime.test.ts +100 -3
- package/tests/odd-integration.test.ts +67 -0
- package/tests/odd-phase-inference.test.ts +213 -0
- package/tests/odd-phase-loader.test.ts +253 -0
- package/tests/odd-phase.test.ts +307 -0
- package/tests/odd-routing-canonical-ratchet.test.ts +11 -6
- package/tests/odd-routing-contract.test.ts +86 -35
- package/tests/orchestrator-budget.test.ts +14 -39
- package/tests/orchestrator-rdd-ownership.test.ts +3 -3
- package/tests/overlay-repaint.test.ts +74 -0
- package/tests/package-manifest.test.ts +252 -115
- package/tests/packed-runner-owned-path.test.ts +46 -0
- package/tests/persona-single-channel.test.ts +6 -6
- package/tests/provider-defect-handoff.test.ts +3 -11
- package/tests/quiet-bash-runtime.test.ts +76 -0
- package/tests/quiet-tool-rendering.test.ts +409 -184
- package/tests/rdd-aware-verification-contract.test.ts +76 -1
- package/tests/rdd-status-line.test.ts +9 -4
- package/tests/resume-hint-extension.test.ts +122 -0
- package/tests/review-agent-end-preflight.test.ts +176 -12
- package/tests/review-candidate-owner-retry.test.ts +22 -1
- package/tests/review-candidate-view.test.ts +298 -0
- package/tests/review-contract-prompt.test.ts +108 -43
- package/tests/review-controller-lock-status.test.ts +0 -1
- package/tests/review-controller-native-routing.test.ts +611 -5
- package/tests/review-controller-workspace-root.test.ts +163 -4
- package/tests/review-host-relay-routing.test.ts +338 -2
- package/tests/review-integration-v2-forward.test.ts +200 -0
- package/tests/review-ledger-contract.test.ts +10 -34
- package/tests/review-reminder-receipt.test.ts +47 -1
- package/tests/review-risk-assessment.test.ts +498 -6
- package/tests/review-sidebar-state.test.ts +402 -0
- package/tests/run-test-suite.test.ts +124 -0
- package/tests/runtime-harness.mjs +145 -786
- package/tests/runtime-metrics-children.test.ts +16 -23
- package/tests/selection-engine.test.ts +421 -0
- package/tests/session-messaging-grants.test.ts +255 -0
- package/tests/session-worktree-registry.test.ts +77 -0
- package/tests/shell-bar.test.ts +382 -1
- package/tests/shell-card.test.ts +353 -1
- package/tests/shell-changes-view.test.ts +52 -0
- package/tests/shell-prompt.test.ts +94 -2
- package/tests/shell-sidebar-layout.test.ts +325 -21
- package/tests/shell-sidebar-scroll-benchmark.test.ts +255 -0
- package/tests/shell-todo.test.ts +87 -1
- package/tests/shell-usage-view.test.ts +27 -0
- package/tests/shell-usage.test.ts +73 -0
- package/tests/skill-registry.test.ts +50 -1
- package/tests/startup-banner.test.ts +130 -2
- package/tests/stats-collector.test.ts +195 -0
- package/tests/stats-view.test.ts +202 -0
- package/tests/telemetry-trigger.test.ts +81 -20
- package/tests/theme-customization.test.ts +72 -0
- package/tests/vim-editor-adapter-host-resolution.test.ts +37 -0
- package/tests/vim-editor-adapter.test.ts +804 -0
- package/tests/vim-normal-engine.test.ts +101 -0
- package/tests/vim-operator-engine.test.ts +215 -0
- package/tests/vim-policy.test.ts +19 -0
- package/tests/vim-visual-engine.test.ts +52 -0
- package/tests/visual-customization-policy.test.ts +110 -0
- package/tests/visual-customize-view.test.ts +418 -0
- package/tests/visual-profiles.test.ts +87 -0
- package/tests/yolo-customize.test.ts +256 -0
- package/tests/yolo-mode-runtime.test.ts +161 -0
- package/tests/yolo-mode.test.ts +261 -0
- package/tests/yolo-session-policy.test.ts +59 -0
- package/themes/Gentle.json +2 -1
- package/themes/Gentleman-Cute.json +2 -1
- package/themes/Gentleman-Sexy.json +2 -1
- package/assets/agents/sdd-apply.md +0 -159
- package/assets/agents/sdd-archive.md +0 -228
- package/assets/agents/sdd-design.md +0 -49
- package/assets/agents/sdd-explore.md +0 -48
- package/assets/agents/sdd-init.md +0 -56
- package/assets/agents/sdd-onboard.md +0 -52
- package/assets/agents/sdd-proposal.md +0 -64
- package/assets/agents/sdd-remediate.md +0 -37
- package/assets/agents/sdd-research.md +0 -49
- package/assets/agents/sdd-spec.md +0 -192
- package/assets/agents/sdd-status.md +0 -54
- package/assets/agents/sdd-tasks.md +0 -108
- package/assets/agents/sdd-verify.md +0 -124
- package/assets/chains/sdd-full.chain.md +0 -83
- package/assets/chains/sdd-plan.chain.md +0 -56
- package/assets/chains/sdd-verify.chain.md +0 -43
- package/assets/sdd-orchestrator-workflow.md +0 -319
- package/assets/support/sdd-status-contract.md +0 -77
- package/docs/assets/diagrams/sdd-cycle.svg +0 -14
- package/extensions/sdd-init.ts +0 -816
- package/lib/openspec-deltas.ts +0 -156
- package/lib/sdd-preflight.ts +0 -1066
- package/lib/sdd-research-capabilities.ts +0 -94
- package/lib/sdd-status.ts +0 -26
- package/tests/fixtures/legacy/sdd-research-v2.5.0.md +0 -54
- package/tests/fixtures/native-review-cli/v2.1.3/bind-sdd.json +0 -25
- package/tests/fixtures/v0.10.7/assets/agents/sdd-apply.md +0 -132
- package/tests/openspec-deltas.test.ts +0 -209
- package/tests/sdd-agent-tools.test.ts +0 -156
- package/tests/sdd-archive-replay.test.ts +0 -82
- package/tests/sdd-classical-continuation.test.ts +0 -74
- package/tests/sdd-execution-routing-contract.test.ts +0 -44
- package/tests/sdd-managed-runtime-settlement.test.ts +0 -155
- package/tests/sdd-native-managed-uptake.test.ts +0 -243
- package/tests/sdd-no-attempts-contract.test.ts +0 -15
- package/tests/sdd-odd-integration.test.ts +0 -33
- package/tests/sdd-optional-research.test.ts +0 -124
- package/tests/sdd-planning-routing-contract.test.ts +0 -45
- package/tests/sdd-preflight-rpc-input.test.ts +0 -125
- package/tests/sdd-preflight.test.ts +0 -541
- package/tests/sdd-research-capabilities.test.ts +0 -114
- package/tests/sdd-research-live.test.ts +0 -241
- package/tests/sdd-selection-transport.test.ts +0 -653
- package/tests/sdd-status.test.ts +0 -9
- package/tests/sdd-task-truth.test.ts +0 -43
|
@@ -32,6 +32,8 @@ import {
|
|
|
32
32
|
quoteForCmdExe,
|
|
33
33
|
recordProvisioned,
|
|
34
34
|
resolveHome,
|
|
35
|
+
USER_PI_HOME_ENV,
|
|
36
|
+
userPiHome,
|
|
35
37
|
resolvePiRuntime,
|
|
36
38
|
restoreJsonField,
|
|
37
39
|
settingsDeclareGentlePi,
|
|
@@ -620,20 +622,37 @@ test("checkPeerVersionPin passes for a matching pin", () => {
|
|
|
620
622
|
assert.deepEqual(result, { ok: true, pinned: ">=0.85.1" });
|
|
621
623
|
});
|
|
622
624
|
|
|
625
|
+
test("Pi 0.99.1 baseline and host peers follow the open development range policy", () => {
|
|
626
|
+
const pkg = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"));
|
|
627
|
+
assert.equal(MIN_PI_VERSION, "0.99.1");
|
|
628
|
+
assert.equal(pkg.engines.node, ">=22.19.0");
|
|
629
|
+
for (const name of ["@earendil-works/pi-ai", "@earendil-works/pi-tui"]) {
|
|
630
|
+
assert.equal(pkg.peerDependencies[name], "*");
|
|
631
|
+
assert.equal(pkg.peerDependenciesMeta[name].optional, true);
|
|
632
|
+
assert.equal(pkg.dependencies[name], undefined);
|
|
633
|
+
}
|
|
634
|
+
// Development ranges are policy specifiers; their floor never drops below
|
|
635
|
+
// the runtime minimum the launcher enforces.
|
|
636
|
+
for (const name of ["@earendil-works/pi-coding-agent", "@earendil-works/pi-ai", "@earendil-works/pi-tui"]) {
|
|
637
|
+
assert.equal(pkg.devDependencies[name], ">=1.0.0", name);
|
|
638
|
+
assert.equal(checkPiVersion(pkg.devDependencies[name].slice(2)).ok, true, name);
|
|
639
|
+
}
|
|
640
|
+
});
|
|
641
|
+
|
|
623
642
|
test("checkPiVersion accepts a version equal to the minimum", () => {
|
|
624
|
-
assert.deepEqual(checkPiVersion("0.
|
|
643
|
+
assert.deepEqual(checkPiVersion("0.99.1"), { ok: true, version: "0.99.1" });
|
|
625
644
|
});
|
|
626
645
|
|
|
627
646
|
test("checkPiVersion accepts a version above the minimum", () => {
|
|
628
|
-
assert.deepEqual(checkPiVersion("0.
|
|
647
|
+
assert.deepEqual(checkPiVersion("0.100.0"), { ok: true, version: "0.100.0" });
|
|
629
648
|
});
|
|
630
649
|
|
|
631
650
|
test("checkPiVersion accepts a v-prefixed version", () => {
|
|
632
|
-
assert.deepEqual(checkPiVersion("v0.
|
|
651
|
+
assert.deepEqual(checkPiVersion("v0.99.1"), { ok: true, version: "0.99.1" });
|
|
633
652
|
});
|
|
634
653
|
|
|
635
654
|
test("checkPiVersion accepts a prerelease suffix at the minimum", () => {
|
|
636
|
-
assert.deepEqual(checkPiVersion("pi version 0.
|
|
655
|
+
assert.deepEqual(checkPiVersion("pi version 0.99.1-rc.2"), { ok: true, version: "0.99.1" });
|
|
637
656
|
});
|
|
638
657
|
|
|
639
658
|
test("checkPiVersion rejects a version below the minimum and names both versions", () => {
|
|
@@ -642,7 +661,7 @@ test("checkPiVersion rejects a version below the minimum and names both versions
|
|
|
642
661
|
if (result.ok) throw new Error("expected a failing result");
|
|
643
662
|
assert.equal(result.version, "0.85.0");
|
|
644
663
|
assert.match(result.message, /0\.85\.0/);
|
|
645
|
-
assert.match(result.message, /0\.
|
|
664
|
+
assert.match(result.message, /0\.99\.1/);
|
|
646
665
|
});
|
|
647
666
|
|
|
648
667
|
test("checkPiVersion rejects a prerelease below the minimum", () => {
|
|
@@ -656,7 +675,7 @@ test("checkPiVersion reports unparsable output with the raw text and the minimum
|
|
|
656
675
|
if (result.ok) throw new Error("expected a failing result");
|
|
657
676
|
assert.equal(result.version, undefined);
|
|
658
677
|
assert.match(result.message, /not a version/);
|
|
659
|
-
assert.match(result.message, /0\.
|
|
678
|
+
assert.match(result.message, /0\.99\.1/);
|
|
660
679
|
});
|
|
661
680
|
|
|
662
681
|
test("checkPiVersion accepts a custom minimum", () => {
|
|
@@ -1103,6 +1122,23 @@ test("otherPackageInjections defaults to including every declared package when i
|
|
|
1103
1122
|
|
|
1104
1123
|
// --- buildPiInvocation -------------------------------------------------------
|
|
1105
1124
|
|
|
1125
|
+
test("package injection relies on Pi's -e resource discovery in isolated and takeover modes", () => {
|
|
1126
|
+
for (const takeOver of [false, true]) {
|
|
1127
|
+
const built = buildPiInvocation({
|
|
1128
|
+
runtime: { kind: "path", command: "/usr/bin/pi", args: [] },
|
|
1129
|
+
home: { mode: "isolated", dir: "/gentle-shell/agent", source: "default" },
|
|
1130
|
+
packageRoot: "/pkg",
|
|
1131
|
+
declaration: undefined,
|
|
1132
|
+
takeOver,
|
|
1133
|
+
otherPackagePaths: [],
|
|
1134
|
+
passthrough: [],
|
|
1135
|
+
baseEnv: {},
|
|
1136
|
+
homedir: "/home/u",
|
|
1137
|
+
});
|
|
1138
|
+
assert.deepEqual(built.args, takeOver ? ["--no-extensions", "-e", "/pkg"] : ["-e", "/pkg"]);
|
|
1139
|
+
}
|
|
1140
|
+
});
|
|
1141
|
+
|
|
1106
1142
|
const linkHome: ResolvedHome = { mode: "link", dir: "/pi/agent", source: "flag" };
|
|
1107
1143
|
const isolatedHomeResolved: ResolvedHome = { mode: "isolated", dir: "/gentle-shell/agent", source: "default" };
|
|
1108
1144
|
|
|
@@ -1116,8 +1152,9 @@ test("buildPiInvocation injects the launcher env into baseEnv", () => {
|
|
|
1116
1152
|
otherPackagePaths: [],
|
|
1117
1153
|
passthrough: [],
|
|
1118
1154
|
baseEnv: { PATH: "/usr/bin" },
|
|
1155
|
+
homedir: "/home/u",
|
|
1119
1156
|
});
|
|
1120
|
-
assert.deepEqual(built.env, { PATH: "/usr/bin", PI_CODING_AGENT_DIR: "/pi/agent", GENTLE_PI_AGENT_HOME: "/pi/agent" });
|
|
1157
|
+
assert.deepEqual(built.env, { PATH: "/usr/bin", PI_CODING_AGENT_DIR: "/pi/agent", GENTLE_PI_AGENT_HOME: "/pi/agent", GENTLE_SHELL_USER_PI_HOME: join("/home/u", ".pi", "agent") });
|
|
1121
1158
|
});
|
|
1122
1159
|
|
|
1123
1160
|
test("buildPiInvocation skips injection when there is a declaration and no takeover (npm matches the launcher's own install)", () => {
|
|
@@ -1130,6 +1167,7 @@ test("buildPiInvocation skips injection when there is a declaration and no takeo
|
|
|
1130
1167
|
otherPackagePaths: [],
|
|
1131
1168
|
passthrough: ["--mode", "rpc"],
|
|
1132
1169
|
baseEnv: {},
|
|
1170
|
+
homedir: "/home/u",
|
|
1133
1171
|
});
|
|
1134
1172
|
assert.deepEqual(built.command, "/usr/bin/pi");
|
|
1135
1173
|
assert.deepEqual(built.args, ["--mode", "rpc"]);
|
|
@@ -1145,16 +1183,11 @@ test("buildPiInvocation adds the gentle-pi injection flags when there is no decl
|
|
|
1145
1183
|
otherPackagePaths: [],
|
|
1146
1184
|
passthrough: ["--mode", "rpc"],
|
|
1147
1185
|
baseEnv: {},
|
|
1186
|
+
homedir: "/home/u",
|
|
1148
1187
|
});
|
|
1149
1188
|
assert.deepEqual(built.args, [
|
|
1150
1189
|
"-e",
|
|
1151
1190
|
"/pkg",
|
|
1152
|
-
"--theme",
|
|
1153
|
-
join("/pkg", "themes"),
|
|
1154
|
-
"--skill",
|
|
1155
|
-
join("/pkg", "skills"),
|
|
1156
|
-
"--prompt-template",
|
|
1157
|
-
join("/pkg", "prompts"),
|
|
1158
1191
|
"--mode",
|
|
1159
1192
|
"rpc",
|
|
1160
1193
|
]);
|
|
@@ -1171,6 +1204,7 @@ test("buildPiInvocation emits only runtime args and passthrough when a pi subcom
|
|
|
1171
1204
|
passthrough: ["install", "npm:x"],
|
|
1172
1205
|
piSubcommand: "install",
|
|
1173
1206
|
baseEnv: {},
|
|
1207
|
+
homedir: "/home/u",
|
|
1174
1208
|
});
|
|
1175
1209
|
assert.deepEqual(built.args, ["install", "npm:x"]);
|
|
1176
1210
|
assert.equal(built.command, "/usr/bin/pi");
|
|
@@ -1187,11 +1221,13 @@ test("buildPiInvocation still injects the launcher env for a pi subcommand", ()
|
|
|
1187
1221
|
passthrough: ["list"],
|
|
1188
1222
|
piSubcommand: "list",
|
|
1189
1223
|
baseEnv: { PATH: "/usr/bin" },
|
|
1224
|
+
homedir: "/home/u",
|
|
1190
1225
|
});
|
|
1191
1226
|
assert.deepEqual(built.env, {
|
|
1192
1227
|
PATH: "/usr/bin",
|
|
1193
1228
|
PI_CODING_AGENT_DIR: "/gentle-shell/agent",
|
|
1194
1229
|
GENTLE_PI_AGENT_HOME: "/gentle-shell/agent",
|
|
1230
|
+
GENTLE_SHELL_USER_PI_HOME: join("/home/u", ".pi", "agent"),
|
|
1195
1231
|
});
|
|
1196
1232
|
});
|
|
1197
1233
|
|
|
@@ -1205,11 +1241,49 @@ test("buildPiInvocation in link mode with a pi subcommand is exactly pi <subcomm
|
|
|
1205
1241
|
otherPackagePaths: [join("/agent", "npm", "node_modules", "some-other")],
|
|
1206
1242
|
passthrough: ["auth", "status"],
|
|
1207
1243
|
piSubcommand: "auth",
|
|
1208
|
-
baseEnv: {},
|
|
1244
|
+
baseEnv: { PI_CODING_AGENT_DIR: "/pi/agent" },
|
|
1245
|
+
homedir: "/home/u",
|
|
1209
1246
|
});
|
|
1210
1247
|
assert.equal(built.command, "/usr/bin/pi");
|
|
1211
1248
|
assert.deepEqual(built.args, ["auth", "status"]);
|
|
1212
|
-
assert.deepEqual(built.env, { PI_CODING_AGENT_DIR: "/pi/agent", GENTLE_PI_AGENT_HOME: "/pi/agent" });
|
|
1249
|
+
assert.deepEqual(built.env, { PI_CODING_AGENT_DIR: "/pi/agent", GENTLE_PI_AGENT_HOME: "/pi/agent", GENTLE_SHELL_USER_PI_HOME: "/pi/agent" });
|
|
1250
|
+
});
|
|
1251
|
+
|
|
1252
|
+
// The isolated home overrides PI_CODING_AGENT_DIR for the session, so the
|
|
1253
|
+
// user's own Pi home travels separately for read-only features such as
|
|
1254
|
+
// /gentle:stats. Isolation itself is unchanged.
|
|
1255
|
+
test("buildPiInvocation carries the user's original Pi home without weakening isolation", () => {
|
|
1256
|
+
const build = (baseEnv: Record<string, string | undefined>, home: ResolvedHome = isolatedHomeResolved) => buildPiInvocation({
|
|
1257
|
+
runtime: { kind: "path", command: "/usr/bin/pi", args: [] },
|
|
1258
|
+
home,
|
|
1259
|
+
packageRoot: "/pkg",
|
|
1260
|
+
declaration: undefined,
|
|
1261
|
+
takeOver: false,
|
|
1262
|
+
otherPackagePaths: [],
|
|
1263
|
+
passthrough: [],
|
|
1264
|
+
baseEnv,
|
|
1265
|
+
homedir: "/home/u",
|
|
1266
|
+
}).env;
|
|
1267
|
+
const conventional = build({});
|
|
1268
|
+
assert.equal(conventional.PI_CODING_AGENT_DIR, "/gentle-shell/agent");
|
|
1269
|
+
assert.equal(conventional.GENTLE_PI_AGENT_HOME, "/gentle-shell/agent");
|
|
1270
|
+
assert.equal(conventional.GENTLE_SHELL_USER_PI_HOME, join("/home/u", ".pi", "agent"));
|
|
1271
|
+
// A custom Pi home is preserved; an empty value falls through like Pi itself.
|
|
1272
|
+
const custom = build({ PI_CODING_AGENT_DIR: "/custom/pi" });
|
|
1273
|
+
assert.equal(custom.PI_CODING_AGENT_DIR, "/gentle-shell/agent");
|
|
1274
|
+
assert.equal(custom.GENTLE_SHELL_USER_PI_HOME, "/custom/pi");
|
|
1275
|
+
assert.equal(build({ PI_CODING_AGENT_DIR: "" }).GENTLE_SHELL_USER_PI_HOME, join("/home/u", ".pi", "agent"));
|
|
1276
|
+
// A nested launch inherits the outer isolated PI_CODING_AGENT_DIR; the
|
|
1277
|
+
// original home the outer launcher recorded must win over it.
|
|
1278
|
+
const nested = build({ PI_CODING_AGENT_DIR: "/gentle-shell/agent", GENTLE_SHELL_USER_PI_HOME: "/custom/pi" });
|
|
1279
|
+
assert.equal(nested.PI_CODING_AGENT_DIR, "/gentle-shell/agent");
|
|
1280
|
+
assert.equal(nested.GENTLE_SHELL_USER_PI_HOME, "/custom/pi");
|
|
1281
|
+
// --link: the active and original homes are the same directory.
|
|
1282
|
+
const linked = build({ PI_CODING_AGENT_DIR: "/pi/agent" }, linkHome);
|
|
1283
|
+
assert.equal(linked.PI_CODING_AGENT_DIR, "/pi/agent");
|
|
1284
|
+
assert.equal(linked.GENTLE_SHELL_USER_PI_HOME, "/pi/agent");
|
|
1285
|
+
assert.equal(USER_PI_HOME_ENV, "GENTLE_SHELL_USER_PI_HOME");
|
|
1286
|
+
assert.equal(userPiHome({ GENTLE_SHELL_USER_PI_HOME: "/kept" }, "/home/u"), "/kept");
|
|
1213
1287
|
});
|
|
1214
1288
|
|
|
1215
1289
|
test("buildPiInvocation keeps the runtime's own args ahead of the injection and passthrough", () => {
|
|
@@ -1222,6 +1296,7 @@ test("buildPiInvocation keeps the runtime's own args ahead of the injection and
|
|
|
1222
1296
|
otherPackagePaths: [],
|
|
1223
1297
|
passthrough: [],
|
|
1224
1298
|
baseEnv: {},
|
|
1299
|
+
homedir: "/home/u",
|
|
1225
1300
|
});
|
|
1226
1301
|
assert.equal(built.args[0], "/bundled/cli.js");
|
|
1227
1302
|
assert.equal(built.command, "/usr/bin/node");
|
|
@@ -1237,6 +1312,7 @@ test("buildPiInvocation takes over a conflicting path declaration: --no-extensio
|
|
|
1237
1312
|
otherPackagePaths: [join("/agent", "npm", "node_modules", "some-other")],
|
|
1238
1313
|
passthrough: ["--mode", "rpc"],
|
|
1239
1314
|
baseEnv: {},
|
|
1315
|
+
homedir: "/home/u",
|
|
1240
1316
|
});
|
|
1241
1317
|
assert.deepEqual(built.args, [
|
|
1242
1318
|
"--no-extensions",
|
|
@@ -1244,12 +1320,6 @@ test("buildPiInvocation takes over a conflicting path declaration: --no-extensio
|
|
|
1244
1320
|
join("/agent", "npm", "node_modules", "some-other"),
|
|
1245
1321
|
"-e",
|
|
1246
1322
|
"/pkg",
|
|
1247
|
-
"--theme",
|
|
1248
|
-
join("/pkg", "themes"),
|
|
1249
|
-
"--skill",
|
|
1250
|
-
join("/pkg", "skills"),
|
|
1251
|
-
"--prompt-template",
|
|
1252
|
-
join("/pkg", "prompts"),
|
|
1253
1323
|
"--mode",
|
|
1254
1324
|
"rpc",
|
|
1255
1325
|
]);
|
|
@@ -1265,17 +1335,12 @@ test("buildPiInvocation takes over with --package-root even for a matching npm d
|
|
|
1265
1335
|
otherPackagePaths: [],
|
|
1266
1336
|
passthrough: [],
|
|
1267
1337
|
baseEnv: {},
|
|
1338
|
+
homedir: "/home/u",
|
|
1268
1339
|
});
|
|
1269
1340
|
assert.deepEqual(built.args, [
|
|
1270
1341
|
"--no-extensions",
|
|
1271
1342
|
"-e",
|
|
1272
1343
|
"/forced/root",
|
|
1273
|
-
"--theme",
|
|
1274
|
-
join("/forced/root", "themes"),
|
|
1275
|
-
"--skill",
|
|
1276
|
-
join("/forced/root", "skills"),
|
|
1277
|
-
"--prompt-template",
|
|
1278
|
-
join("/forced/root", "prompts"),
|
|
1279
1344
|
]);
|
|
1280
1345
|
});
|
|
1281
1346
|
|
|
@@ -1289,6 +1354,7 @@ test("buildPiInvocation takes over with --package-root even when there is no dec
|
|
|
1289
1354
|
otherPackagePaths: [join("/agent", "npm", "node_modules", "some-other")],
|
|
1290
1355
|
passthrough: [],
|
|
1291
1356
|
baseEnv: {},
|
|
1357
|
+
homedir: "/home/u",
|
|
1292
1358
|
});
|
|
1293
1359
|
assert.deepEqual(built.args, [
|
|
1294
1360
|
"--no-extensions",
|
|
@@ -1296,12 +1362,6 @@ test("buildPiInvocation takes over with --package-root even when there is no dec
|
|
|
1296
1362
|
join("/agent", "npm", "node_modules", "some-other"),
|
|
1297
1363
|
"-e",
|
|
1298
1364
|
"/forced/root",
|
|
1299
|
-
"--theme",
|
|
1300
|
-
join("/forced/root", "themes"),
|
|
1301
|
-
"--skill",
|
|
1302
|
-
join("/forced/root", "skills"),
|
|
1303
|
-
"--prompt-template",
|
|
1304
|
-
join("/forced/root", "prompts"),
|
|
1305
1365
|
]);
|
|
1306
1366
|
});
|
|
1307
1367
|
|
|
@@ -1316,6 +1376,7 @@ test("buildPiInvocation injects loose extension entries during a takeover, after
|
|
|
1316
1376
|
looseExtensionEntries: [join("/agent", "extensions", "a.ts"), join("/project", ".pi", "extensions", "b.js")],
|
|
1317
1377
|
passthrough: ["--mode", "rpc"],
|
|
1318
1378
|
baseEnv: {},
|
|
1379
|
+
homedir: "/home/u",
|
|
1319
1380
|
});
|
|
1320
1381
|
assert.deepEqual(built.args, [
|
|
1321
1382
|
"--no-extensions",
|
|
@@ -1327,12 +1388,6 @@ test("buildPiInvocation injects loose extension entries during a takeover, after
|
|
|
1327
1388
|
join("/project", ".pi", "extensions", "b.js"),
|
|
1328
1389
|
"-e",
|
|
1329
1390
|
"/pkg",
|
|
1330
|
-
"--theme",
|
|
1331
|
-
join("/pkg", "themes"),
|
|
1332
|
-
"--skill",
|
|
1333
|
-
join("/pkg", "skills"),
|
|
1334
|
-
"--prompt-template",
|
|
1335
|
-
join("/pkg", "prompts"),
|
|
1336
1391
|
"--mode",
|
|
1337
1392
|
"rpc",
|
|
1338
1393
|
]);
|
|
@@ -1348,8 +1403,9 @@ test("buildPiInvocation omits loose extension entry flags when the list is empty
|
|
|
1348
1403
|
otherPackagePaths: [],
|
|
1349
1404
|
passthrough: [],
|
|
1350
1405
|
baseEnv: {},
|
|
1406
|
+
homedir: "/home/u",
|
|
1351
1407
|
});
|
|
1352
|
-
assert.deepEqual(withoutField.args, ["--no-extensions", "-e", "/pkg"
|
|
1408
|
+
assert.deepEqual(withoutField.args, ["--no-extensions", "-e", "/pkg"]);
|
|
1353
1409
|
|
|
1354
1410
|
const withEmptyField = buildPiInvocation({
|
|
1355
1411
|
runtime: { kind: "path", command: "/usr/bin/pi", args: [] },
|
|
@@ -1361,6 +1417,7 @@ test("buildPiInvocation omits loose extension entry flags when the list is empty
|
|
|
1361
1417
|
looseExtensionEntries: [],
|
|
1362
1418
|
passthrough: [],
|
|
1363
1419
|
baseEnv: {},
|
|
1420
|
+
homedir: "/home/u",
|
|
1364
1421
|
});
|
|
1365
1422
|
assert.deepEqual(withEmptyField.args, withoutField.args);
|
|
1366
1423
|
});
|
|
@@ -1381,6 +1438,7 @@ test("buildPiInvocation dedupes loose extension entries against other-package pa
|
|
|
1381
1438
|
looseExtensionEntries: ["/shared/dup.ts", "/agent/extensions/a.ts", "/agent/extensions/a.ts"],
|
|
1382
1439
|
passthrough: [],
|
|
1383
1440
|
baseEnv: {},
|
|
1441
|
+
homedir: "/home/u",
|
|
1384
1442
|
});
|
|
1385
1443
|
assert.deepEqual(built.args, [
|
|
1386
1444
|
"--no-extensions",
|
|
@@ -1392,12 +1450,6 @@ test("buildPiInvocation dedupes loose extension entries against other-package pa
|
|
|
1392
1450
|
"/agent/extensions/a.ts",
|
|
1393
1451
|
"-e",
|
|
1394
1452
|
"/pkg",
|
|
1395
|
-
"--theme",
|
|
1396
|
-
join("/pkg", "themes"),
|
|
1397
|
-
"--skill",
|
|
1398
|
-
join("/pkg", "skills"),
|
|
1399
|
-
"--prompt-template",
|
|
1400
|
-
join("/pkg", "prompts"),
|
|
1401
1453
|
]);
|
|
1402
1454
|
});
|
|
1403
1455
|
|
|
@@ -1415,6 +1467,7 @@ test("buildPiInvocation dedupes the launcher's own package root against an other
|
|
|
1415
1467
|
otherPackagePaths: [join("/agent", "npm", "node_modules", "some-other"), "/pkg"],
|
|
1416
1468
|
passthrough: [],
|
|
1417
1469
|
baseEnv: {},
|
|
1470
|
+
homedir: "/home/u",
|
|
1418
1471
|
});
|
|
1419
1472
|
const eFlags = built.args.filter((arg, index) => built.args[index - 1] === "-e");
|
|
1420
1473
|
assert.deepEqual(eFlags, [join("/agent", "npm", "node_modules", "some-other"), "/pkg"]);
|
|
@@ -1424,12 +1477,6 @@ test("buildPiInvocation dedupes the launcher's own package root against an other
|
|
|
1424
1477
|
join("/agent", "npm", "node_modules", "some-other"),
|
|
1425
1478
|
"-e",
|
|
1426
1479
|
"/pkg",
|
|
1427
|
-
"--theme",
|
|
1428
|
-
join("/pkg", "themes"),
|
|
1429
|
-
"--skill",
|
|
1430
|
-
join("/pkg", "skills"),
|
|
1431
|
-
"--prompt-template",
|
|
1432
|
-
join("/pkg", "prompts"),
|
|
1433
1480
|
]);
|
|
1434
1481
|
});
|
|
1435
1482
|
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { existsSync, mkdtempSync, readFileSync, rmSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { dirname, join, resolve } from "node:path";
|
|
5
|
+
import test from "node:test";
|
|
6
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
7
|
+
import { getAgentDir, SessionManager } from "@earendil-works/pi-coding-agent";
|
|
8
|
+
import {
|
|
9
|
+
RESUME_HANDOFF_ENV,
|
|
10
|
+
gentleShellResumeCommand,
|
|
11
|
+
isResumeHandoffPath,
|
|
12
|
+
parseResumeHandoff,
|
|
13
|
+
piDefaultSessionDir,
|
|
14
|
+
planResumeHint,
|
|
15
|
+
resumeHandoffFromSession,
|
|
16
|
+
serializeResumeHandoff,
|
|
17
|
+
} from "../lib/gentle-shell-resume-hint.ts";
|
|
18
|
+
|
|
19
|
+
const ID = "01a0e0a0-6d7b-7314-89c1-537d47bbf4f3";
|
|
20
|
+
const AGENT_DIR = resolve("/home/u/.gentle-shell/agent");
|
|
21
|
+
const CWD = resolve("/home/u/project");
|
|
22
|
+
const DEFAULT_DIR = join(AGENT_DIR, "sessions", `--${CWD.replace(/^[/\\]/, "").replace(/[/\\:]/g, "-")}--`);
|
|
23
|
+
|
|
24
|
+
test("handoff env name is stable", () => {
|
|
25
|
+
assert.equal(RESUME_HANDOFF_ENV, "GENTLE_SHELL_RESUME_HANDOFF");
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test("piDefaultSessionDir mirrors pi's encoded per-cwd session dir", () => {
|
|
29
|
+
assert.equal(piDefaultSessionDir(CWD, AGENT_DIR), DEFAULT_DIR);
|
|
30
|
+
if (process.platform !== "win32") assert.equal(piDefaultSessionDir("/home/u/project", "/a"), "/a/sessions/--home-u-project--");
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test("piDefaultSessionDir matches pi's own SessionManager", () => {
|
|
34
|
+
const agentDir = mkdtempSync(join(tmpdir(), "resume-hint-agent-"));
|
|
35
|
+
const previous = process.env.PI_CODING_AGENT_DIR;
|
|
36
|
+
process.env.PI_CODING_AGENT_DIR = agentDir;
|
|
37
|
+
try {
|
|
38
|
+
const byDefault = SessionManager.create(CWD);
|
|
39
|
+
assert.equal(byDefault.usesDefaultSessionDir(), true);
|
|
40
|
+
assert.equal(byDefault.getSessionDir(), piDefaultSessionDir(CWD, getAgentDir()));
|
|
41
|
+
const custom = SessionManager.create(CWD, join(agentDir, "elsewhere"));
|
|
42
|
+
assert.equal(custom.usesDefaultSessionDir(), false);
|
|
43
|
+
assert.notEqual(custom.getSessionDir(), piDefaultSessionDir(CWD, getAgentDir()));
|
|
44
|
+
} finally {
|
|
45
|
+
if (previous === undefined) delete process.env.PI_CODING_AGENT_DIR;
|
|
46
|
+
else process.env.PI_CODING_AGENT_DIR = previous;
|
|
47
|
+
rmSync(agentDir, { recursive: true, force: true });
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test("resumeHandoffFromSession omits the session dir when it is pi's default", () => {
|
|
52
|
+
const handoff = resumeHandoffFromSession({
|
|
53
|
+
sessionId: ID,
|
|
54
|
+
sessionDir: DEFAULT_DIR,
|
|
55
|
+
sessionFile: join(DEFAULT_DIR, "x.jsonl"),
|
|
56
|
+
cwd: CWD,
|
|
57
|
+
launchCwd: CWD,
|
|
58
|
+
agentDir: AGENT_DIR,
|
|
59
|
+
fileExists: () => true,
|
|
60
|
+
});
|
|
61
|
+
assert.deepEqual(handoff, { sessionId: ID });
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test("resumeHandoffFromSession keeps a custom session dir", () => {
|
|
65
|
+
const handoff = resumeHandoffFromSession({
|
|
66
|
+
sessionId: ID,
|
|
67
|
+
sessionDir: "/tmp/my sessions",
|
|
68
|
+
sessionFile: "/tmp/my sessions/x.jsonl",
|
|
69
|
+
cwd: CWD,
|
|
70
|
+
launchCwd: CWD,
|
|
71
|
+
agentDir: AGENT_DIR,
|
|
72
|
+
fileExists: () => true,
|
|
73
|
+
});
|
|
74
|
+
assert.deepEqual(handoff, { sessionId: ID, sessionDir: "/tmp/my sessions" });
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test("resumeHandoffFromSession carries the session file for a session from another project", () => {
|
|
78
|
+
const other = resolve("/home/u/other");
|
|
79
|
+
const handoff = resumeHandoffFromSession({
|
|
80
|
+
sessionId: ID,
|
|
81
|
+
sessionDir: DEFAULT_DIR,
|
|
82
|
+
sessionFile: join(DEFAULT_DIR, "x.jsonl"),
|
|
83
|
+
cwd: CWD,
|
|
84
|
+
launchCwd: other,
|
|
85
|
+
agentDir: AGENT_DIR,
|
|
86
|
+
fileExists: () => true,
|
|
87
|
+
});
|
|
88
|
+
assert.deepEqual(handoff, { sessionId: ID, sessionFile: join(DEFAULT_DIR, "x.jsonl") });
|
|
89
|
+
assert.equal(gentleShellResumeCommand(handoff!, ["--link"], "linux"), `gentle-shell --link --session ${join(DEFAULT_DIR, "x.jsonl")}`);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test("resumeHandoffFromSession returns undefined when pi would not print a hint", () => {
|
|
93
|
+
const base = { sessionId: ID, sessionDir: DEFAULT_DIR, cwd: CWD, launchCwd: CWD, agentDir: AGENT_DIR };
|
|
94
|
+
assert.equal(resumeHandoffFromSession({ ...base, sessionFile: undefined, fileExists: () => true }), undefined);
|
|
95
|
+
assert.equal(resumeHandoffFromSession({ ...base, sessionFile: join(DEFAULT_DIR, "x.jsonl"), fileExists: () => false }), undefined);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test("handoff round-trips and rejects malformed input", () => {
|
|
99
|
+
const handoff = { sessionId: ID, sessionDir: "/tmp/s" };
|
|
100
|
+
assert.deepEqual(parseResumeHandoff(serializeResumeHandoff(handoff)), handoff);
|
|
101
|
+
assert.deepEqual(parseResumeHandoff(serializeResumeHandoff({ sessionId: ID })), { sessionId: ID });
|
|
102
|
+
for (const text of ["", "not json", "null", "[]", '{"sessionId":""}', '{"sessionId":1}', '{"sessionId":"a","sessionDir":3}', '{"sessionId":"a","sessionDir":""}']) {
|
|
103
|
+
assert.equal(parseResumeHandoff(text), undefined, text);
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test("handoff accepts only an absolute, control-free session file on its own", () => {
|
|
108
|
+
const file = resolve("/tmp/s/x.jsonl");
|
|
109
|
+
assert.deepEqual(parseResumeHandoff(JSON.stringify({ sessionId: ID, sessionFile: file })), { sessionId: ID, sessionFile: file });
|
|
110
|
+
for (const value of [
|
|
111
|
+
{ sessionId: ID, sessionFile: "relative/x.jsonl" },
|
|
112
|
+
{ sessionId: ID, sessionFile: resolve("/tmp/s/x.txt") },
|
|
113
|
+
{ sessionId: ID, sessionFile: resolve("/tmp/s/\u001b[2J.jsonl") },
|
|
114
|
+
{ sessionId: ID, sessionFile: 3 },
|
|
115
|
+
{ sessionId: ID, sessionFile: file, sessionDir: "/tmp/s" },
|
|
116
|
+
]) {
|
|
117
|
+
assert.equal(parseResumeHandoff(JSON.stringify(value)), undefined, JSON.stringify(value));
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
test("isResumeHandoffPath accepts only the launcher's private handoff shape", () => {
|
|
122
|
+
const tmp = tmpdir();
|
|
123
|
+
assert.equal(isResumeHandoffPath(join(tmp, "gentle-shell-resume-Ab12Cd", "handoff.json")), true);
|
|
124
|
+
assert.equal(isResumeHandoffPath(join(tmp, "gentle-shell-resume-Ab12Cd", "other.json")), false);
|
|
125
|
+
assert.equal(isResumeHandoffPath(join(tmp, "elsewhere", "handoff.json")), false);
|
|
126
|
+
assert.equal(isResumeHandoffPath(join(resolve("/home/u"), ".bashrc")), false);
|
|
127
|
+
assert.equal(isResumeHandoffPath("gentle-shell-resume-Ab12Cd/handoff.json"), false);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
test("handoff rejects values that could inject terminal control sequences", () => {
|
|
131
|
+
const reject = (value: object) => assert.equal(parseResumeHandoff(JSON.stringify(value)), undefined, JSON.stringify(value));
|
|
132
|
+
// Session ids follow pi's assertValidSessionId charset.
|
|
133
|
+
reject({ sessionId: "abc\u001b[2J" });
|
|
134
|
+
reject({ sessionId: "abc def" });
|
|
135
|
+
reject({ sessionId: "-abc" });
|
|
136
|
+
reject({ sessionId: "abc$(id)" });
|
|
137
|
+
reject({ sessionId: ID, sessionDir: "/tmp/\u001b]0;pwned\u0007" });
|
|
138
|
+
reject({ sessionId: ID, sessionDir: "/tmp/a\u009bb" });
|
|
139
|
+
assert.deepEqual(parseResumeHandoff(JSON.stringify({ sessionId: "a.b_c-1" })), { sessionId: "a.b_c-1" });
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
test("gentleShellResumeCommand keeps home selectors and a custom session dir", () => {
|
|
143
|
+
assert.equal(gentleShellResumeCommand({ sessionId: ID }, [], "linux"), `gentle-shell --session ${ID}`);
|
|
144
|
+
assert.equal(gentleShellResumeCommand({ sessionId: ID }, ["--link"], "linux"), `gentle-shell --link --session ${ID}`);
|
|
145
|
+
assert.equal(
|
|
146
|
+
gentleShellResumeCommand({ sessionId: ID, sessionDir: "/tmp/my sessions" }, ["--home", "/x y"], "linux"),
|
|
147
|
+
`gentle-shell --home '/x y' --session-dir '/tmp/my sessions' --session ${ID}`,
|
|
148
|
+
);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
test("the gentle-shell command matches pi's real exit hint with the binary swapped", async () => {
|
|
152
|
+
// formatResumeCommand is not in pi's exports map; load the module file.
|
|
153
|
+
const piDist = dirname(fileURLToPath(import.meta.resolve("@earendil-works/pi-coding-agent")));
|
|
154
|
+
const modulePath = join(piDist, "modes", "interactive", "interactive-mode.js");
|
|
155
|
+
const { formatResumeCommand } = await import(pathToFileURL(modulePath).href);
|
|
156
|
+
// The appended line is only needed while pi prints this hint.
|
|
157
|
+
assert.ok(readFileSync(modulePath, "utf8").includes('chalk.dim("To resume this session:")'), "pi's hint label changed");
|
|
158
|
+
|
|
159
|
+
const agentDir = mkdtempSync(join(tmpdir(), "resume-hint-agent-"));
|
|
160
|
+
const previousDir = process.env.PI_CODING_AGENT_DIR;
|
|
161
|
+
const previousTTY = Object.getOwnPropertyDescriptor(process.stdout, "isTTY");
|
|
162
|
+
process.env.PI_CODING_AGENT_DIR = agentDir;
|
|
163
|
+
Object.defineProperty(process.stdout, "isTTY", { value: true, configurable: true });
|
|
164
|
+
try {
|
|
165
|
+
for (const sessionDir of [undefined, join(agentDir, "my sessions")]) {
|
|
166
|
+
const manager = SessionManager.create(CWD, sessionDir);
|
|
167
|
+
manager.appendMessage({ role: "user", content: "hi", timestamp: Date.now() } as never);
|
|
168
|
+
manager.appendMessage({ role: "assistant", content: [{ type: "text", text: "ok" }], timestamp: Date.now() } as never);
|
|
169
|
+
const handoff = resumeHandoffFromSession({
|
|
170
|
+
sessionId: manager.getSessionId(),
|
|
171
|
+
sessionDir: manager.getSessionDir(),
|
|
172
|
+
sessionFile: manager.getSessionFile(),
|
|
173
|
+
cwd: manager.getCwd(),
|
|
174
|
+
launchCwd: manager.getCwd(),
|
|
175
|
+
agentDir: getAgentDir(),
|
|
176
|
+
fileExists: existsSync,
|
|
177
|
+
});
|
|
178
|
+
assert.ok(handoff);
|
|
179
|
+
const piCommand: string = formatResumeCommand(manager);
|
|
180
|
+
assert.ok(piCommand.startsWith("pi "), piCommand);
|
|
181
|
+
// pi quotes the POSIX way on every platform; compare with the same quoting.
|
|
182
|
+
assert.equal(gentleShellResumeCommand(handoff, [], "linux"), `gentle-shell ${piCommand.slice("pi ".length)}`);
|
|
183
|
+
}
|
|
184
|
+
} finally {
|
|
185
|
+
if (previousTTY) Object.defineProperty(process.stdout, "isTTY", previousTTY);
|
|
186
|
+
else delete (process.stdout as { isTTY?: boolean }).isTTY;
|
|
187
|
+
if (previousDir === undefined) delete process.env.PI_CODING_AGENT_DIR;
|
|
188
|
+
else process.env.PI_CODING_AGENT_DIR = previousDir;
|
|
189
|
+
rmSync(agentDir, { recursive: true, force: true });
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
test("on win32 the command uses double quotes that cmd.exe and PowerShell honor", () => {
|
|
194
|
+
assert.equal(gentleShellResumeCommand({ sessionId: ID }, ["--link"], "win32"), `gentle-shell --link --session ${ID}`);
|
|
195
|
+
assert.equal(
|
|
196
|
+
gentleShellResumeCommand({ sessionId: ID, sessionDir: "C:\\Users\\Name With Space\\sessions" }, ["--home", "C:\\Users\\Name With Space\\home"], "win32"),
|
|
197
|
+
`gentle-shell --home "C:\\Users\\Name With Space\\home" --session-dir "C:\\Users\\Name With Space\\sessions" --session ${ID}`,
|
|
198
|
+
);
|
|
199
|
+
// Plain paths need no quotes at all.
|
|
200
|
+
assert.equal(
|
|
201
|
+
gentleShellResumeCommand({ sessionId: ID, sessionFile: "C:\\s\\x.jsonl" }, [], "win32"),
|
|
202
|
+
"gentle-shell --session C:\\s\\x.jsonl",
|
|
203
|
+
);
|
|
204
|
+
// Spaces alone are safe inside double quotes.
|
|
205
|
+
assert.equal(
|
|
206
|
+
gentleShellResumeCommand({ sessionId: ID, sessionFile: "C:\\My Sessions\\x.jsonl" }, [], "win32"),
|
|
207
|
+
'gentle-shell --session "C:\\My Sessions\\x.jsonl"',
|
|
208
|
+
);
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
test("on win32 no command is produced when a value cannot be quoted safely", () => {
|
|
212
|
+
// PowerShell passes a space-free argument to the .cmd shim unquoted, so
|
|
213
|
+
// cmd.exe operators are refused even though double quotes would cover them.
|
|
214
|
+
for (const home of ["C:\\%USERPROFILE%\\h", "C:\\a!b\\h", "C:\\$env\\h", "C:\\a`b\\h", 'C:\\a"b\\h', "C:\\a b\\", "C:\\R&D\\h", "C:\\a|b\\h", "C:\\a<b\\h", "C:\\a>b\\h", "C:\\a^b\\h", "C:\\a(b)\\h"]) {
|
|
215
|
+
assert.equal(gentleShellResumeCommand({ sessionId: ID }, ["--home", home], "win32"), undefined, home);
|
|
216
|
+
}
|
|
217
|
+
assert.equal(
|
|
218
|
+
planResumeHint({ handoff: { sessionId: ID }, homeFlags: ["--home", "C:\\%TEMP%\\h"], stdoutIsTTY: true, terminalHungUp: false, platform: "win32", color: true }),
|
|
219
|
+
undefined,
|
|
220
|
+
);
|
|
221
|
+
// The same characters are harmless inside POSIX single quotes elsewhere.
|
|
222
|
+
assert.equal(gentleShellResumeCommand({ sessionId: ID }, ["--home", "/tmp/$h"], "linux"), `gentle-shell --home '/tmp/$h' --session ${ID}`);
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
test("a cross-project session file reopens the original session in pi", () => {
|
|
226
|
+
const agentDir = mkdtempSync(join(tmpdir(), "resume-hint-agent-"));
|
|
227
|
+
const previousDir = process.env.PI_CODING_AGENT_DIR;
|
|
228
|
+
process.env.PI_CODING_AGENT_DIR = agentDir;
|
|
229
|
+
try {
|
|
230
|
+
const manager = SessionManager.create(CWD);
|
|
231
|
+
manager.appendMessage({ role: "user", content: "hi", timestamp: Date.now() } as never);
|
|
232
|
+
manager.appendMessage({ role: "assistant", content: [{ type: "text", text: "ok" }], timestamp: Date.now() } as never);
|
|
233
|
+
const handoff = resumeHandoffFromSession({
|
|
234
|
+
sessionId: manager.getSessionId(),
|
|
235
|
+
sessionDir: manager.getSessionDir(),
|
|
236
|
+
sessionFile: manager.getSessionFile(),
|
|
237
|
+
cwd: manager.getCwd(),
|
|
238
|
+
launchCwd: resolve("/home/u/other"),
|
|
239
|
+
agentDir: getAgentDir(),
|
|
240
|
+
fileExists: existsSync,
|
|
241
|
+
});
|
|
242
|
+
assert.ok(handoff?.sessionFile);
|
|
243
|
+
// pi's --session takes any value with a path separator as a file path.
|
|
244
|
+
assert.ok(/[/\\]/.test(handoff.sessionFile));
|
|
245
|
+
const reopened = SessionManager.open(handoff.sessionFile);
|
|
246
|
+
assert.equal(reopened.getSessionId(), manager.getSessionId());
|
|
247
|
+
assert.equal(reopened.getCwd(), manager.getCwd());
|
|
248
|
+
} finally {
|
|
249
|
+
if (previousDir === undefined) delete process.env.PI_CODING_AGENT_DIR;
|
|
250
|
+
else process.env.PI_CODING_AGENT_DIR = previousDir;
|
|
251
|
+
rmSync(agentDir, { recursive: true, force: true });
|
|
252
|
+
}
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
test("planResumeHint prints the gentle-shell line with pi's dim label style", () => {
|
|
256
|
+
const hint = planResumeHint({ handoff: { sessionId: ID }, homeFlags: ["--link"], stdoutIsTTY: true, terminalHungUp: false, platform: "linux", color: true });
|
|
257
|
+
assert.equal(hint, `\u001b[2mTo resume in gentle-shell:\u001b[22m gentle-shell --link --session ${ID}\n`);
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
test("planResumeHint drops the ANSI style when stdout has no colors", () => {
|
|
261
|
+
const hint = planResumeHint({ handoff: { sessionId: ID }, homeFlags: [], stdoutIsTTY: true, terminalHungUp: false, platform: "linux", color: false });
|
|
262
|
+
assert.equal(hint, `To resume in gentle-shell: gentle-shell --session ${ID}\n`);
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
test("planResumeHint prints nothing without a handoff, a TTY, or after a hang-up", () => {
|
|
266
|
+
const base = { handoff: { sessionId: ID } as { sessionId: string } | undefined, homeFlags: [], stdoutIsTTY: true, terminalHungUp: false, platform: "linux" as NodeJS.Platform, color: true };
|
|
267
|
+
assert.equal(planResumeHint({ ...base, handoff: undefined }), undefined);
|
|
268
|
+
assert.equal(planResumeHint({ ...base, stdoutIsTTY: false }), undefined);
|
|
269
|
+
assert.equal(planResumeHint({ ...base, terminalHungUp: true }), undefined);
|
|
270
|
+
});
|