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
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { appendSystemPromptOnce, type AppendableSystemPromptOptions } from "./append-system-prompt.ts";
|
|
3
|
+
import { captureReviewSessionIdentity, sameReviewSessionIdentity, type ReviewSessionIdentity } from "./review-session-standing-permission.ts";
|
|
4
|
+
|
|
5
|
+
export const YOLO_STATUS_KEY = "gentle:yolo";
|
|
6
|
+
export const YOLO_STATUS_TEXT = "🚀 YOLO ON 🔥 — destructive confirmations remain";
|
|
7
|
+
export const YOLO_DIRECTIVE = `<gentle-yolo-session>
|
|
8
|
+
YOLO session standing permission is ON, explicitly activated by the human for this live primary session and Git clone.
|
|
9
|
+
Qualify the default commit, push and PR confirmation clauses: for ordinary already-scoped implementation, checks, commits, non-force pushes and PR creation, this activation supplies standing permission instead of repeated permission questions. Make ordinary reversible implementation choices without needless interviews.
|
|
10
|
+
This does NOT override explicit human restrictions, repository policy, project trust, configured confirmations or blocks, or the authorized task scope. Ask before scope expansion, privacy-sensitive disclosure, genuinely unresolved consequential product choices, or ambiguous destinations or credentials. Never invent destinations or credentials.
|
|
11
|
+
Destructive/data-loss confirmations remain mandatory and independent. Never auto-answer ask_user tools, provider consent, maintenance/recovery authorization or opaque-token decisions. Review standing permission and RDD are unchanged. Children remain bounded by their delegated task; they receive no independent delivery grant or YOLO inheritance.
|
|
12
|
+
</gentle-yolo-session>`;
|
|
13
|
+
|
|
14
|
+
/** Separate from review grants; no registry, entries, configuration or environment enable switch. */
|
|
15
|
+
export class YoloSessionPolicy {
|
|
16
|
+
private grant: ReviewSessionIdentity | undefined;
|
|
17
|
+
private generation = 0;
|
|
18
|
+
|
|
19
|
+
get epoch(): number { return this.generation; }
|
|
20
|
+
get enabled(): boolean { return this.grant !== undefined; }
|
|
21
|
+
|
|
22
|
+
reset(): void {
|
|
23
|
+
this.generation += 1;
|
|
24
|
+
this.grant = undefined;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
set(enabled: boolean, identity: ReviewSessionIdentity | undefined, expectedEpoch = this.epoch): boolean {
|
|
28
|
+
if (expectedEpoch !== this.epoch) return false;
|
|
29
|
+
this.reset();
|
|
30
|
+
if (!enabled) return true;
|
|
31
|
+
if (identity === undefined) return false;
|
|
32
|
+
this.grant = identity;
|
|
33
|
+
return true;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
active(identity: ReviewSessionIdentity | undefined): boolean {
|
|
37
|
+
if (!this.grant) return false;
|
|
38
|
+
if (identity !== undefined && sameReviewSessionIdentity(this.grant, identity)) return true;
|
|
39
|
+
this.reset();
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Remove our exact block as well as adding it: bridge options can be reused. */
|
|
45
|
+
export function updateYoloPrompt(options: AppendableSystemPromptOptions | null | undefined, active: boolean): void {
|
|
46
|
+
if (!options) return;
|
|
47
|
+
options.appendSystemPrompt = (options.appendSystemPrompt ?? "")
|
|
48
|
+
.replace(`\n\n${YOLO_DIRECTIVE}`, "").replace(YOLO_DIRECTIVE, "");
|
|
49
|
+
if (active) appendSystemPromptOnce(options, YOLO_DIRECTIVE);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const HUMAN_ACTION = { on: "on", off: "off", toggle: "" } as const;
|
|
53
|
+
type HumanAction = (typeof HUMAN_ACTION)[keyof typeof HUMAN_ACTION];
|
|
54
|
+
export const YOLO_DISPLAY = { on: "ON", off: "OFF", unavailable: "UNAVAILABLE" } as const;
|
|
55
|
+
export type YoloDisplay = (typeof YOLO_DISPLAY)[keyof typeof YOLO_DISPLAY];
|
|
56
|
+
|
|
57
|
+
/** Private trusted-extension discovery, never a model/tool or serialized authority event. */
|
|
58
|
+
const HOST_UI_CHANNEL = "gentle:yolo:host-ui";
|
|
59
|
+
const DISCOVERY_TIMEOUT_MS = 100;
|
|
60
|
+
export interface YoloUiAdapter {
|
|
61
|
+
read(): Promise<YoloDisplay>;
|
|
62
|
+
toggle(): Promise<void>;
|
|
63
|
+
observe(refresh: () => void): () => void;
|
|
64
|
+
dispose(): void;
|
|
65
|
+
}
|
|
66
|
+
interface HostUiRequest {
|
|
67
|
+
context: ExtensionContext;
|
|
68
|
+
respond(adapter: YoloUiAdapter): void;
|
|
69
|
+
}
|
|
70
|
+
function isUiAdapter(value: unknown): value is YoloUiAdapter {
|
|
71
|
+
if (!value || typeof value !== "object") return false;
|
|
72
|
+
const adapter = value as Partial<YoloUiAdapter>;
|
|
73
|
+
return [adapter.read, adapter.toggle, adapter.observe, adapter.dispose].every(fn => typeof fn === "function");
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** emit is void (and listeners may be wrapped async); explicitly bound the callback wait. */
|
|
77
|
+
export function discoverYoloUiAdapter(pi: ExtensionAPI, context: ExtensionContext): Promise<YoloUiAdapter | undefined> {
|
|
78
|
+
return new Promise(resolve => {
|
|
79
|
+
let settled = false;
|
|
80
|
+
const timer = setTimeout(() => { settled = true; resolve(undefined); }, DISCOVERY_TIMEOUT_MS);
|
|
81
|
+
const respond = (value: unknown) => {
|
|
82
|
+
if (!isUiAdapter(value)) return;
|
|
83
|
+
if (settled) { value.dispose(); return; }
|
|
84
|
+
settled = true;
|
|
85
|
+
clearTimeout(timer);
|
|
86
|
+
resolve(value);
|
|
87
|
+
};
|
|
88
|
+
try { pi.events.emit(HOST_UI_CHANNEL, { context, respond } satisfies HostUiRequest); }
|
|
89
|
+
catch { settled = true; clearTimeout(timer); resolve(undefined); }
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export interface YoloSessionController {
|
|
94
|
+
active(context: ExtensionContext): Promise<boolean>;
|
|
95
|
+
humanAction(action: HumanAction, context: ExtensionContext, current?: () => boolean): Promise<boolean>;
|
|
96
|
+
reset(context: ExtensionContext): void;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Register only a human slash command, never a tool, flag or persisted setting. */
|
|
100
|
+
export function registerYoloSessionPolicy(pi: ExtensionAPI, env: NodeJS.ProcessEnv): YoloSessionController {
|
|
101
|
+
const policy = new YoloSessionPolicy();
|
|
102
|
+
let ownerGeneration = 0;
|
|
103
|
+
let publishedEpoch = policy.epoch;
|
|
104
|
+
const observers = new Set<() => void>();
|
|
105
|
+
const publish = (context: ExtensionContext, active: boolean): void => {
|
|
106
|
+
// Display is not authority. Widgets survive shells that hide the status rail.
|
|
107
|
+
try { context.ui.setStatus(YOLO_STATUS_KEY, active ? YOLO_STATUS_TEXT : undefined); } catch { /* nonblocking */ }
|
|
108
|
+
try { context.ui.setWidget(YOLO_STATUS_KEY, active ? [YOLO_STATUS_TEXT] : undefined); } catch { /* nonblocking */ }
|
|
109
|
+
// Repeated state reads publish indicators, but only a policy transition
|
|
110
|
+
// wakes observers. active -> publish -> observer -> read thus terminates.
|
|
111
|
+
if (publishedEpoch === policy.epoch) return;
|
|
112
|
+
publishedEpoch = policy.epoch;
|
|
113
|
+
for (const refresh of [...observers]) { try { refresh(); } catch { /* display only */ } }
|
|
114
|
+
};
|
|
115
|
+
const capture = async (context: ExtensionContext): Promise<ReviewSessionIdentity | undefined> => {
|
|
116
|
+
try {
|
|
117
|
+
const cwd = context.cwd;
|
|
118
|
+
const identity = await captureReviewSessionIdentity(context, env);
|
|
119
|
+
// Git lookup awaits: a replacement manager ID must not activate stale scope.
|
|
120
|
+
return cwd === context.cwd && identity?.sessionManager === context.sessionManager &&
|
|
121
|
+
identity?.sessionId === context.sessionManager.getSessionId() ? identity : undefined;
|
|
122
|
+
} catch { return undefined; }
|
|
123
|
+
};
|
|
124
|
+
const active = async (context: ExtensionContext): Promise<boolean> => {
|
|
125
|
+
const epoch = policy.epoch;
|
|
126
|
+
const identity = policy.enabled ? await capture(context) : undefined;
|
|
127
|
+
if (epoch !== policy.epoch) return false;
|
|
128
|
+
const enabled = policy.active(identity);
|
|
129
|
+
publish(context, enabled);
|
|
130
|
+
return enabled;
|
|
131
|
+
};
|
|
132
|
+
// The owning extension calls this from its existing lifecycle hooks, including
|
|
133
|
+
// reload. Keep their cardinality and ordering intact for SDK and legacy hosts.
|
|
134
|
+
const reset = (context: ExtensionContext): void => {
|
|
135
|
+
ownerGeneration += 1;
|
|
136
|
+
policy.reset();
|
|
137
|
+
publish(context, false);
|
|
138
|
+
observers.clear();
|
|
139
|
+
};
|
|
140
|
+
// Binds an interaction to the session that opened it: a reload, replacement,
|
|
141
|
+
// or cwd change while the human is deciding must not act on the new scope.
|
|
142
|
+
const bindCurrent = (context: ExtensionContext, disposed = () => false): (() => boolean) => {
|
|
143
|
+
const generation = ownerGeneration;
|
|
144
|
+
const manager = context.sessionManager, sessionId = manager.getSessionId(), cwd = context.cwd;
|
|
145
|
+
return () => {
|
|
146
|
+
try { return !disposed() && generation === ownerGeneration && manager === context.sessionManager &&
|
|
147
|
+
sessionId === manager.getSessionId() && cwd === context.cwd && context.hasUI; }
|
|
148
|
+
catch { return false; } // SDK contexts throw after runtime replacement.
|
|
149
|
+
};
|
|
150
|
+
};
|
|
151
|
+
// Both entry points execute this operation, including off/pending-on races.
|
|
152
|
+
// Resolves false only when the interaction stopped being current, so the
|
|
153
|
+
// caller can report the discarded choice; a superseding action stays silent.
|
|
154
|
+
const humanAction = async (action: HumanAction, context: ExtensionContext, current = () => true): Promise<boolean> => {
|
|
155
|
+
if (!current()) return false;
|
|
156
|
+
if (action === HUMAN_ACTION.off) {
|
|
157
|
+
policy.reset(); publish(context, false); context.ui.notify("YOLO OFF", "info");
|
|
158
|
+
return true;
|
|
159
|
+
}
|
|
160
|
+
const wasEnabled = policy.enabled;
|
|
161
|
+
policy.reset();
|
|
162
|
+
publish(context, false);
|
|
163
|
+
const epoch = policy.epoch;
|
|
164
|
+
const identity = await capture(context);
|
|
165
|
+
const enable = action === HUMAN_ACTION.on || !wasEnabled;
|
|
166
|
+
if (!current()) return false;
|
|
167
|
+
if (epoch !== policy.epoch) return true;
|
|
168
|
+
const changed = policy.set(enable, identity, epoch);
|
|
169
|
+
const enabled = changed && policy.active(identity);
|
|
170
|
+
publish(context, enabled);
|
|
171
|
+
context.ui.notify(enabled ? YOLO_STATUS_TEXT : enable
|
|
172
|
+
? "YOLO OFF — activation requires an interactive primary TUI session and an identifiable Git clone."
|
|
173
|
+
: "YOLO OFF", enabled || !enable ? "info" : "warning");
|
|
174
|
+
return true;
|
|
175
|
+
};
|
|
176
|
+
// Legacy minimal hosts can run the command without an inter-extension bus;
|
|
177
|
+
// they simply cannot expose the menu adapter.
|
|
178
|
+
pi.events?.on?.(HOST_UI_CHANNEL, (payload: unknown) => {
|
|
179
|
+
if (!payload || typeof payload !== "object") return;
|
|
180
|
+
const request = payload as Partial<HostUiRequest>;
|
|
181
|
+
if (typeof request.respond !== "function" || !request.context) return;
|
|
182
|
+
const context = request.context;
|
|
183
|
+
try {
|
|
184
|
+
let disposed = false;
|
|
185
|
+
const subscriptions = new Set<() => void>();
|
|
186
|
+
const current = bindCurrent(context, () => disposed);
|
|
187
|
+
const read = async (): Promise<YoloDisplay> => {
|
|
188
|
+
if (!current()) return YOLO_DISPLAY.unavailable;
|
|
189
|
+
const epoch = policy.epoch;
|
|
190
|
+
const identity = await capture(context);
|
|
191
|
+
if (!current()) return YOLO_DISPLAY.unavailable;
|
|
192
|
+
if (epoch !== policy.epoch) return read(); // observe the winning off/on, never an old ON.
|
|
193
|
+
const enabled = policy.active(identity);
|
|
194
|
+
publish(context, enabled);
|
|
195
|
+
return identity ? enabled ? YOLO_DISPLAY.on : YOLO_DISPLAY.off : YOLO_DISPLAY.unavailable;
|
|
196
|
+
};
|
|
197
|
+
request.respond({
|
|
198
|
+
read,
|
|
199
|
+
toggle: async () => { await humanAction(HUMAN_ACTION.toggle, context, current); },
|
|
200
|
+
observe: (refresh) => {
|
|
201
|
+
if (!current()) return () => {};
|
|
202
|
+
subscriptions.add(refresh); observers.add(refresh);
|
|
203
|
+
return () => { subscriptions.delete(refresh); observers.delete(refresh); };
|
|
204
|
+
},
|
|
205
|
+
dispose: () => {
|
|
206
|
+
disposed = true;
|
|
207
|
+
for (const refresh of subscriptions) observers.delete(refresh);
|
|
208
|
+
subscriptions.clear();
|
|
209
|
+
},
|
|
210
|
+
});
|
|
211
|
+
} catch { /* Missing or obsolete trusted host context: discovery expires closed. */ }
|
|
212
|
+
});
|
|
213
|
+
pi.registerCommand("gentle:yolo", {
|
|
214
|
+
description: "Session-only ordinary development/delivery permission (enable|disable|status); no argument opens a menu. Destructive confirmations remain.",
|
|
215
|
+
handler: async (args, context) => {
|
|
216
|
+
let action = args.trim() || "status";
|
|
217
|
+
let current = () => true;
|
|
218
|
+
// Headless callers keep the status fallback: an empty argument never grants.
|
|
219
|
+
if (!args.trim() && context.hasUI && typeof context.ui.select === "function") {
|
|
220
|
+
current = bindCurrent(context);
|
|
221
|
+
const state = await active(context) ? YOLO_DISPLAY.on : YOLO_DISPLAY.off;
|
|
222
|
+
const selected = await context.ui.select(`🚀 Gentle YOLO 🔥 — full speed, destructive actions still ask (current: ${state})`, ["enable", "disable", "status"]);
|
|
223
|
+
if (selected === undefined) return;
|
|
224
|
+
action = selected;
|
|
225
|
+
}
|
|
226
|
+
if (action !== "enable" && action !== "disable" && action !== "status") {
|
|
227
|
+
context.ui.notify("Use /gentle:yolo enable|disable|status. State unchanged.", "warning");
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
if (action === "status") {
|
|
231
|
+
context.ui.notify(await active(context) ? YOLO_STATUS_TEXT : "YOLO OFF", "info");
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
if (await humanAction(action === "enable" ? HUMAN_ACTION.on : HUMAN_ACTION.off, context, current)) return;
|
|
235
|
+
try { context.ui.notify("YOLO unchanged — the session changed while the menu was open.", "warning"); }
|
|
236
|
+
catch { /* SDK contexts throw after runtime replacement. */ }
|
|
237
|
+
},
|
|
238
|
+
});
|
|
239
|
+
return { active, humanAction, reset };
|
|
240
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gentle-pi",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "Turn Pi into el Gentleman:
|
|
3
|
+
"version": "4.0.0",
|
|
4
|
+
"description": "Turn Pi into el Gentleman: an ODD development harness with focused subagents, configured TDD evidence, native review, and skill discovery.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"bin": {
|
|
@@ -13,8 +13,7 @@
|
|
|
13
13
|
"pi-coding-agent",
|
|
14
14
|
"gentle-pi",
|
|
15
15
|
"gentle-ai",
|
|
16
|
-
"
|
|
17
|
-
"openspec",
|
|
16
|
+
"odd",
|
|
18
17
|
"subagents",
|
|
19
18
|
"tdd",
|
|
20
19
|
"code-review",
|
|
@@ -37,6 +36,7 @@
|
|
|
37
36
|
"skills/",
|
|
38
37
|
"scripts/",
|
|
39
38
|
"tests/",
|
|
39
|
+
"!tests/fixtures/legacy/sdd-research-v2.5.0.md",
|
|
40
40
|
"themes/",
|
|
41
41
|
"README.md"
|
|
42
42
|
],
|
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
"check:provider-contract": "node scripts/check-provider-contract.mjs",
|
|
45
45
|
"mirror:odd-routing": "node scripts/mirror-odd-routing.mjs",
|
|
46
46
|
"postinstall": "node scripts/install-gentle-ai.mjs",
|
|
47
|
-
"test": "node
|
|
47
|
+
"test": "node scripts/run-test-suite.mjs",
|
|
48
48
|
"test:harness": "node --experimental-strip-types tests/runtime-harness.mjs",
|
|
49
49
|
"test:dev-binary": "node --experimental-strip-types --test tests/devbinary/*.devtest.ts",
|
|
50
50
|
"test:cross-lane": "node tests/crosslane/cross-lane.mjs",
|
|
@@ -72,22 +72,34 @@
|
|
|
72
72
|
]
|
|
73
73
|
},
|
|
74
74
|
"dependencies": {
|
|
75
|
-
"@earendil-works/pi-tui": "0.85.1",
|
|
76
75
|
"@heyhuynhgiabuu/pi-pretty": "0.6.27"
|
|
77
76
|
},
|
|
78
77
|
"peerDependencies": {
|
|
79
|
-
"@earendil-works/pi-coding-agent": ">=0.
|
|
78
|
+
"@earendil-works/pi-coding-agent": ">=0.99.1",
|
|
79
|
+
"@earendil-works/pi-ai": "*",
|
|
80
|
+
"@earendil-works/pi-tui": "*",
|
|
80
81
|
"typebox": "*"
|
|
81
82
|
},
|
|
82
83
|
"peerDependenciesMeta": {
|
|
83
84
|
"@earendil-works/pi-coding-agent": {
|
|
84
85
|
"optional": true
|
|
86
|
+
},
|
|
87
|
+
"@earendil-works/pi-ai": {
|
|
88
|
+
"optional": true
|
|
89
|
+
},
|
|
90
|
+
"@earendil-works/pi-tui": {
|
|
91
|
+
"optional": true
|
|
85
92
|
}
|
|
86
93
|
},
|
|
87
94
|
"devDependencies": {
|
|
88
|
-
"@earendil-works/pi-coding-agent": "0.
|
|
95
|
+
"@earendil-works/pi-coding-agent": ">=1.0.0",
|
|
96
|
+
"@earendil-works/pi-ai": ">=1.0.0",
|
|
97
|
+
"@earendil-works/pi-tui": ">=1.0.0",
|
|
89
98
|
"@types/node": "^24.13.3",
|
|
90
99
|
"typescript": "^5.9.3"
|
|
91
100
|
},
|
|
101
|
+
"engines": {
|
|
102
|
+
"node": ">=22.19.0"
|
|
103
|
+
},
|
|
92
104
|
"packageManager": "pnpm@11.1.1"
|
|
93
105
|
}
|
|
@@ -195,6 +195,16 @@ function linkDir(env , homedir )
|
|
|
195
195
|
return env.PI_CODING_AGENT_DIR || join(homedir, ".pi", "agent");
|
|
196
196
|
}
|
|
197
197
|
|
|
198
|
+
// The isolated home replaces PI_CODING_AGENT_DIR for the whole session, so the
|
|
199
|
+
// user's own Pi home travels in this variable for read-only features such as
|
|
200
|
+
// /gentle:stats. An inherited value wins: a gentle-shell launched from inside
|
|
201
|
+
// a Gentle Shell session sees the outer isolated home as PI_CODING_AGENT_DIR.
|
|
202
|
+
export const USER_PI_HOME_ENV = "GENTLE_SHELL_USER_PI_HOME";
|
|
203
|
+
|
|
204
|
+
export function userPiHome(env , homedir ) {
|
|
205
|
+
return env[USER_PI_HOME_ENV] || linkDir(env, homedir);
|
|
206
|
+
}
|
|
207
|
+
|
|
198
208
|
function isolatedDir(env , homedir ) {
|
|
199
209
|
return env.GENTLE_SHELL_HOME || join(homedir, ".gentle-shell", "agent");
|
|
200
210
|
}
|
|
@@ -380,7 +390,7 @@ export function missingPiMessage() {
|
|
|
380
390
|
|
|
381
391
|
// --- pi version gate ---------------------------------------------------------
|
|
382
392
|
|
|
383
|
-
export const MIN_PI_VERSION = "0.
|
|
393
|
+
export const MIN_PI_VERSION = "0.99.1";
|
|
384
394
|
|
|
385
395
|
|
|
386
396
|
|
|
@@ -875,18 +885,13 @@ export function discoverLooseExtensionEntries(dir , fs )
|
|
|
875
885
|
|
|
876
886
|
|
|
877
887
|
|
|
878
|
-
function packageRootAssetArgs(packageRoot ) {
|
|
879
|
-
return ["--theme", join(packageRoot, "themes"), "--skill", join(packageRoot, "skills"), "--prompt-template", join(packageRoot, "prompts")];
|
|
880
|
-
}
|
|
881
888
|
|
|
882
|
-
|
|
883
|
-
return ["-e", packageRoot, ...packageRootAssetArgs(packageRoot)];
|
|
884
|
-
}
|
|
889
|
+
|
|
885
890
|
|
|
886
891
|
// Four cases, checked in this order — `piSubcommand` first, then `takeOver`:
|
|
887
892
|
// - piSubcommand: pi dispatches install/remove/uninstall/update/list/
|
|
888
893
|
// config/auth on argv[0] before it even parses flags, so any injected
|
|
889
|
-
// -e
|
|
894
|
+
// -e flag ahead of it stops pi from
|
|
890
895
|
// recognising its subcommand at all — this is exactly the observed
|
|
891
896
|
// 2026-09-22 bug where `gentle-shell install npm:x` opened an
|
|
892
897
|
// interactive pi session instead of running the package manager. No
|
|
@@ -909,7 +914,8 @@ function packageRootInjectionArgs(packageRoot ) {
|
|
|
909
914
|
// (R3-001): a loose entry that duplicates an other-package path, or
|
|
910
915
|
// repeats within looseExtensionEntries itself, is skipped rather than
|
|
911
916
|
// loaded twice.
|
|
912
|
-
// - Not takeOver, no declaration: inject this launcher's own packageRoot
|
|
917
|
+
// - Not takeOver, no declaration: inject this launcher's own packageRoot
|
|
918
|
+
// once via -e; Pi discovers its extensions, skills, prompts and themes,
|
|
913
919
|
// exactly as when nothing else in settings loads gentle-pi.
|
|
914
920
|
// - Not takeOver, with a declaration: no injection at all — the target
|
|
915
921
|
// settings already load a gentle-pi the launcher accepts as-is (the
|
|
@@ -940,9 +946,9 @@ export function buildPiInvocation(input ) {
|
|
|
940
946
|
injected.add(input.packageRoot);
|
|
941
947
|
args.push("-e", input.packageRoot);
|
|
942
948
|
}
|
|
943
|
-
|
|
949
|
+
|
|
944
950
|
} else if (input.declaration === undefined) {
|
|
945
|
-
args.push(
|
|
951
|
+
args.push("-e", input.packageRoot);
|
|
946
952
|
}
|
|
947
953
|
|
|
948
954
|
args.push(...input.passthrough);
|
|
@@ -950,7 +956,12 @@ export function buildPiInvocation(input ) {
|
|
|
950
956
|
return {
|
|
951
957
|
command: input.runtime.command,
|
|
952
958
|
args,
|
|
953
|
-
env: {
|
|
959
|
+
env: {
|
|
960
|
+
...input.baseEnv,
|
|
961
|
+
PI_CODING_AGENT_DIR: input.home.dir,
|
|
962
|
+
GENTLE_PI_AGENT_HOME: input.home.dir,
|
|
963
|
+
[USER_PI_HOME_ENV]: userPiHome(input.baseEnv, input.homedir),
|
|
964
|
+
},
|
|
954
965
|
};
|
|
955
966
|
}
|
|
956
967
|
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
// Generated by scripts/build-runtime-modules.mjs. Do not edit.
|
|
2
|
+
// Pure logic behind the gentle-shell resume hint. On interactive quit pi
|
|
3
|
+
// prints "To resume this session: pi --session <id>" (interactive-mode
|
|
4
|
+
// formatResumeCommand). Under gentle-shell that command cannot find the
|
|
5
|
+
// session: pi resolves sessions from PI_CODING_AGENT_DIR, which the launcher
|
|
6
|
+
// points at the gentle-shell home, but the hint names the bare `pi` binary
|
|
7
|
+
// and never mentions that directory, so running it looks in ~/.pi/agent.
|
|
8
|
+
//
|
|
9
|
+
// The clean fix belongs in pi (earendil-works/pi#8048, #9750). Until then,
|
|
10
|
+
// gentle-shell appends its own line below pi's, leaving pi's output as is:
|
|
11
|
+
// extensions/resume-hint.ts writes a ResumeHandoff on session_shutdown, and
|
|
12
|
+
// bin/gentle-shell.mjs prints the planResumeHint line after pi exits.
|
|
13
|
+
import { basename, dirname, isAbsolute, join, resolve as resolvePath } from "node:path";
|
|
14
|
+
import { shellQuote } from "./gentle-shell-launcher.mjs";
|
|
15
|
+
|
|
16
|
+
export const RESUME_HANDOFF_ENV = "GENTLE_SHELL_RESUME_HANDOFF";
|
|
17
|
+
|
|
18
|
+
const HINT_LABEL = "To resume in gentle-shell:";
|
|
19
|
+
|
|
20
|
+
// pi's assertValidSessionId charset. The handoff ends up on the terminal, so
|
|
21
|
+
// anything outside it (or any C0/C1 control in a session dir) is refused
|
|
22
|
+
// rather than escaped.
|
|
23
|
+
const SESSION_ID_PATTERN = /^[A-Za-z0-9](?:[A-Za-z0-9._-]*[A-Za-z0-9])?$/;
|
|
24
|
+
const CONTROL_CHARS = /[\u0000-\u001f\u007f-\u009f]/;
|
|
25
|
+
|
|
26
|
+
// The launcher creates <tmpdir>/<RESUME_HANDOFF_DIR_PREFIX>XXXXXX/<RESUME_HANDOFF_FILE>.
|
|
27
|
+
export const RESUME_HANDOFF_DIR_PREFIX = "gentle-shell-resume-";
|
|
28
|
+
export const RESUME_HANDOFF_FILE = "handoff.json";
|
|
29
|
+
|
|
30
|
+
// The extension only writes to a path shaped like the launcher's private
|
|
31
|
+
// handoff, so an inherited or foreign env value cannot aim it at another file.
|
|
32
|
+
export function isResumeHandoffPath(path ) {
|
|
33
|
+
return (
|
|
34
|
+
isAbsolute(path) &&
|
|
35
|
+
basename(path) === RESUME_HANDOFF_FILE &&
|
|
36
|
+
basename(dirname(path)).startsWith(RESUME_HANDOFF_DIR_PREFIX) &&
|
|
37
|
+
!CONTROL_CHARS.test(path)
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
// Mirror of pi's getDefaultSessionDirPath (core/session-manager), which pi
|
|
54
|
+
// does not export. Kept byte-for-byte so usesDefaultSessionDir agrees.
|
|
55
|
+
export function piDefaultSessionDir(cwd , agentDir ) {
|
|
56
|
+
const resolvedCwd = resolvePath(cwd);
|
|
57
|
+
const safePath = `--${resolvedCwd.replace(/^[/\\]/, "").replace(/[/\\:]/g, "-")}--`;
|
|
58
|
+
return join(resolvePath(agentDir), "sessions", safePath);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
// Mirrors the guards in pi's formatResumeCommand: no hint for an
|
|
73
|
+
// unpersisted session or one whose file was never written.
|
|
74
|
+
export function resumeHandoffFromSession(snapshot ) {
|
|
75
|
+
const { sessionId, sessionDir, sessionFile, cwd, launchCwd, agentDir, fileExists } = snapshot;
|
|
76
|
+
if (!sessionFile || !fileExists(sessionFile)) return undefined;
|
|
77
|
+
if (resolvePath(cwd) !== resolvePath(launchCwd)) return { sessionId, sessionFile: resolvePath(sessionFile) };
|
|
78
|
+
if (sessionDir === piDefaultSessionDir(cwd, agentDir)) return { sessionId };
|
|
79
|
+
return { sessionId, sessionDir };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function serializeResumeHandoff(handoff ) {
|
|
83
|
+
return JSON.stringify(handoff);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Tolerant on purpose: a missing, stale, or foreign handoff file just means
|
|
87
|
+
// "print nothing".
|
|
88
|
+
export function parseResumeHandoff(text ) {
|
|
89
|
+
let parsed ;
|
|
90
|
+
try {
|
|
91
|
+
parsed = JSON.parse(text);
|
|
92
|
+
} catch {
|
|
93
|
+
return undefined;
|
|
94
|
+
}
|
|
95
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return undefined;
|
|
96
|
+
const { sessionId, sessionDir, sessionFile } = parsed ;
|
|
97
|
+
if (typeof sessionId !== "string" || !SESSION_ID_PATTERN.test(sessionId)) return undefined;
|
|
98
|
+
if (sessionFile !== undefined) {
|
|
99
|
+
if (sessionDir !== undefined || typeof sessionFile !== "string") return undefined;
|
|
100
|
+
if (!isAbsolute(sessionFile) || !sessionFile.endsWith(".jsonl") || CONTROL_CHARS.test(sessionFile)) return undefined;
|
|
101
|
+
return { sessionId, sessionFile };
|
|
102
|
+
}
|
|
103
|
+
if (sessionDir === undefined) return { sessionId };
|
|
104
|
+
if (typeof sessionDir !== "string" || sessionDir.length === 0 || CONTROL_CHARS.test(sessionDir)) return undefined;
|
|
105
|
+
return { sessionId, sessionDir };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Values that need no quoting in any shell a user might paste the hint into.
|
|
109
|
+
const PLAIN_ARG = /^[A-Za-z0-9_\-.:/\\=]+$/;
|
|
110
|
+
// Characters that stay live inside double quotes: cmd.exe expands %VAR% (and
|
|
111
|
+
// !VAR! under delayed expansion), PowerShell expands $var and `escapes, and
|
|
112
|
+
// an inner " ends the quoted argument in both. A trailing backslash would
|
|
113
|
+
// escape the closing quote under the Windows argv rules. cmd.exe operators
|
|
114
|
+
// are refused too: gentle-shell is installed as a .cmd shim, and PowerShell
|
|
115
|
+
// drops the quotes of a space-free argument when it calls one, so cmd.exe
|
|
116
|
+
// would read & | < > ^ ( ) as operators.
|
|
117
|
+
const WINDOWS_UNQUOTABLE = /["%!$`&|<>^()]|\\$/;
|
|
118
|
+
|
|
119
|
+
// Quotes one argument for the shell the user is likely to paste into:
|
|
120
|
+
// POSIX single quotes elsewhere, double quotes on win32, where cmd.exe and
|
|
121
|
+
// PowerShell do not treat single quotes as quoting. Returns undefined when
|
|
122
|
+
// the value cannot be quoted safely there.
|
|
123
|
+
function quoteArg(value , platform ) {
|
|
124
|
+
if (platform !== "win32") return shellQuote(value);
|
|
125
|
+
if (PLAIN_ARG.test(value)) return value;
|
|
126
|
+
if (WINDOWS_UNQUOTABLE.test(value) || CONTROL_CHARS.test(value)) return undefined;
|
|
127
|
+
return `"${value}"`;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Returns the gentle-shell command that resumes the handed-off session, or
|
|
131
|
+
// undefined when some argument cannot be quoted safely for the platform's
|
|
132
|
+
// shell (the caller then prints no hint and leaves pi's own line alone).
|
|
133
|
+
export function gentleShellResumeCommand(
|
|
134
|
+
handoff ,
|
|
135
|
+
homeFlags ,
|
|
136
|
+
platform ,
|
|
137
|
+
) {
|
|
138
|
+
const values = [...homeFlags];
|
|
139
|
+
// pi treats a --session value containing a path separator as a file path
|
|
140
|
+
// and opens it directly, whatever the launch directory.
|
|
141
|
+
if (handoff.sessionFile !== undefined) {
|
|
142
|
+
values.push("--session", handoff.sessionFile);
|
|
143
|
+
} else {
|
|
144
|
+
if (handoff.sessionDir !== undefined) values.push("--session-dir", handoff.sessionDir);
|
|
145
|
+
values.push("--session", handoff.sessionId);
|
|
146
|
+
}
|
|
147
|
+
const args = ["gentle-shell"];
|
|
148
|
+
for (const value of values) {
|
|
149
|
+
const quoted = quoteArg(value, platform);
|
|
150
|
+
if (quoted === undefined) return undefined;
|
|
151
|
+
args.push(quoted);
|
|
152
|
+
}
|
|
153
|
+
return args.join(" ");
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
// Returns the line to print after pi exits, or undefined to print nothing.
|
|
169
|
+
// Like pi, it only prints to a TTY, and never after the terminal hung up.
|
|
170
|
+
export function planResumeHint(input ) {
|
|
171
|
+
const { handoff, homeFlags, stdoutIsTTY, terminalHungUp, platform, color } = input;
|
|
172
|
+
if (!handoff || !stdoutIsTTY || terminalHungUp) return undefined;
|
|
173
|
+
const command = gentleShellResumeCommand(handoff, homeFlags, platform);
|
|
174
|
+
if (command === undefined) return undefined;
|
|
175
|
+
const label = color ? `\u001b[2m${HINT_LABEL}\u001b[22m` : HINT_LABEL;
|
|
176
|
+
return `${label} ${command}\n`;
|
|
177
|
+
}
|