gentle-pi 4.2.0 → 4.4.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 +340 -138
- package/assets/agents/gentle-ai-explore.md +1 -0
- package/assets/agents/gentle-ai-verify.md +6 -4
- package/assets/agents/gentle-ai-worker.md +7 -1
- package/assets/design-canvas/canvas.css +648 -0
- package/assets/design-canvas/canvas.js +1497 -0
- package/assets/design-canvas/index.html +88 -0
- package/assets/design-canvas/inspector.js +290 -0
- package/assets/orchestrator-delegation-reference.md +163 -0
- package/assets/orchestrator-delegation.md +25 -176
- package/assets/orchestrator-tracking.md +1 -1
- package/assets/orchestrator-verification.md +14 -8
- package/assets/orchestrator-writer.md +5 -1
- package/assets/orchestrator.md +11 -11
- package/bin/engram-guard +1 -1
- package/bin/gentle-shell.mjs +237 -139
- package/contracts/review-provider-contract-mirror/provider-contract.lock.json +8 -7
- package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/README.md +7 -0
- package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/manifest.json +11 -2
- package/contracts/review-provider-contract-mirror/v1.4.0/bundle/routing/pi.md +92 -0
- package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/generated/provider-capabilities.baseline.json +8 -1
- package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/generated/provider-roles.baseline.json +1 -1
- package/docs/assets/diagrams/agent-orchestration.svg +54 -15
- package/docs/assets/diagrams/gentleman-workflow.svg +69 -11
- package/docs/assets/diagrams/measured-routing.svg +49 -0
- package/docs/assets/diagrams/native-review.svg +52 -12
- package/docs/assets/diagrams/odd-workflow.svg +128 -66
- package/docs/assets/diagrams/permission-modes.svg +51 -0
- package/docs/assets/features/audio-notifications.png +0 -0
- package/docs/assets/features/command-palette.png +0 -0
- package/docs/bundled-install.md +65 -30
- package/docs/delegated-verification.md +16 -8
- package/docs/gentle-agents-activity.md +16 -6
- package/docs/gentle-music.md +61 -0
- package/docs/gentle-shell-uninstall.md +8 -1
- package/docs/gentle-shell.md +80 -9
- package/docs/install-wizard.md +37 -6
- package/docs/permission-modes.md +8 -3
- package/docs/readme-reference.md +120 -28
- package/docs/session-profile-format.md +26 -0
- package/docs/voice.md +259 -0
- package/docs/yolo-mode.md +38 -10
- package/extensions/ask-user-choice.ts +4 -1
- package/extensions/codegraph-tools.ts +68 -6
- package/extensions/gentle-agents.ts +270 -48
- package/extensions/gentle-ai.ts +490 -113
- package/extensions/gentle-design.ts +158 -0
- package/extensions/gentle-herdr-activity.ts +2 -3
- package/extensions/gentle-music.ts +289 -0
- package/extensions/gentle-shell.ts +359 -85
- package/extensions/gentle-todo.ts +43 -9
- package/extensions/gentle-voice.ts +11 -0
- package/extensions/quiet-tools.ts +1 -1
- package/extensions/session-scratch.ts +384 -0
- package/extensions/skill-registry.ts +63 -8
- package/extensions/startup-banner.ts +2 -3
- package/lib/agent-home.ts +8 -3
- package/lib/agent-profiles.ts +26 -0
- package/lib/agents-config.ts +35 -0
- package/lib/agents-protocol.ts +60 -1
- package/lib/agents-runner.ts +107 -9
- package/lib/agents-session-transport.ts +101 -6
- package/lib/agents-view.ts +3 -1
- package/lib/agents-widget.ts +14 -1
- package/lib/auto-permission-memory.ts +48 -18
- package/lib/auto-permission-runtime.ts +21 -6
- package/lib/bounded-writer-admission.ts +51 -0
- package/lib/clipboard-image.ts +147 -0
- package/lib/command-palette-catalog.ts +2 -4
- package/lib/commit-slice-signal.ts +100 -0
- package/lib/context-backstop.ts +120 -0
- package/lib/delivered-test-lint.ts +80 -0
- package/lib/design-canvas-server.ts +947 -0
- package/lib/design-comments.ts +437 -0
- package/lib/design-handoffs.ts +313 -0
- package/lib/design-open-browser.ts +58 -0
- package/lib/design-sources.ts +120 -0
- package/lib/destructive-command-guard.ts +120 -9
- package/lib/external-editor.ts +31 -0
- package/lib/gauge-theme.ts +146 -0
- package/lib/gentle-ai-binary.ts +28 -7
- package/lib/gentle-design.ts +513 -0
- package/lib/gentle-shell-bundled.ts +146 -36
- package/lib/gentle-shell-import-pi.ts +233 -0
- package/lib/gentle-shell-launcher.ts +154 -15
- package/lib/gentle-shell-startup-update.ts +61 -9
- package/lib/gentle-shell-uninstall.ts +51 -2
- package/lib/herdr-activity.ts +24 -0
- package/lib/image-attachments.ts +150 -0
- package/lib/inprocess-reviewer.ts +33 -2
- package/lib/nan-provider.ts +3 -0
- package/lib/native-review-cli.ts +20 -5
- package/lib/notification-audio-native.ts +149 -13
- package/lib/notification-audio.ts +4 -0
- package/lib/notification-pulse-worker.ts +82 -18
- package/lib/notification-service.ts +5 -1
- package/lib/orchestrator-discovery.ts +5 -1
- package/lib/orchestrator-presence.ts +87 -8
- package/lib/orchestrator-scope.ts +31 -3
- package/lib/orchestrator-state.ts +11 -3
- package/lib/permission-mode-policy.ts +2 -2
- package/lib/provider-contract-bundle.ts +99 -53
- package/lib/review-candidate-view-owner.ts +29 -19
- package/lib/review-candidate-view.ts +36 -2
- package/lib/review-host-relay.ts +11 -2
- package/lib/review-reminder-receipt.ts +24 -1
- package/lib/review-risk-assessment.ts +43 -38
- package/lib/review-session-standing-permission.ts +3 -1
- package/lib/session-profile-append-controller.ts +713 -0
- package/lib/session-profile-disk-reader.ts +182 -0
- package/lib/session-profile-freeze.ts +30 -1
- package/lib/session-profile-persistence.ts +15 -7
- package/lib/session-scratch.ts +523 -0
- package/lib/session-worktree-registry.ts +7 -0
- package/lib/shell-bar.ts +29 -28
- package/lib/shell-changes-view.ts +80 -13
- package/lib/shell-changes.ts +81 -5
- package/lib/shell-gauge.ts +49 -6
- package/lib/shell-prompt.ts +4 -4
- package/lib/shell-sidebar-layout.ts +26 -6
- package/lib/shell-todo.ts +24 -5
- package/lib/shell-usage-view.ts +5 -1
- package/lib/shell-usage.ts +7 -7
- package/lib/verifier-admission.ts +30 -0
- package/lib/vim-editor-adapter.ts +11 -8
- package/lib/voice-artifacts.ts +188 -0
- package/lib/voice-command.ts +576 -0
- package/lib/voice-install.ts +455 -0
- package/lib/voice-platform.ts +99 -0
- package/lib/voice-push-to-talk.ts +262 -0
- package/lib/voice-session.ts +460 -0
- package/lib/voice-settings.ts +50 -0
- package/lib/voice-ui.ts +158 -0
- package/lib/yolo-session-policy.ts +58 -22
- package/lib/yolo-temp-scratch.ts +69 -0
- package/package.json +1 -2
- package/runtime/gentle-ai-binary.mjs +28 -7
- package/runtime/gentle-shell-bundled.mjs +145 -35
- package/runtime/gentle-shell-import-pi.mjs +234 -0
- package/runtime/gentle-shell-launcher.mjs +152 -13
- package/runtime/gentle-shell-startup-update.mjs +61 -9
- package/runtime/gentle-shell-uninstall.mjs +51 -2
- package/runtime/native-review-cli.mjs +19 -4
- package/runtime/review-risk-assessment.mjs +43 -38
- package/scripts/build-runtime-modules.mjs +1 -0
- package/scripts/bundled-engram-migration.mjs +119 -0
- package/scripts/gentle-ai-installer.mjs +10 -10
- package/scripts/installer-downloads.mjs +7 -7
- package/scripts/installer-preflight.mjs +5 -3
- package/scripts/installer-server.mjs +14 -4
- package/scripts/installer-windows.mjs +37 -7
- package/scripts/main-channel.mjs +94 -19
- package/scripts/pi-update.mjs +34 -6
- package/scripts/run-test-suite.mjs +5 -0
- package/scripts/verify-package-files.mjs +43 -13
- package/scripts/voice-worker.cjs +476 -0
- package/skills/design/SKILL.md +110 -0
- package/skills/gentle-ai/SKILL.md +3 -3
- package/skills/work-unit-commits/SKILL.md +5 -3
- package/tests/agent-profiles.test.ts +24 -0
- package/tests/agent-risk-escalation.test.ts +37 -10
- package/tests/agents-config.test.ts +61 -0
- package/tests/agents-fake-child.ts +8 -1
- package/tests/agents-protocol.test.ts +32 -0
- package/tests/agents-runner.test.ts +485 -7
- package/tests/agents-session-transport.test.ts +53 -3
- package/tests/ask-user-choice.test.ts +30 -0
- package/tests/auto-mode.test.ts +17 -0
- package/tests/auto-permission-memory-diagnostics.test.ts +56 -0
- package/tests/auto-permission-runtime.test.ts +85 -1
- package/tests/autonomous-guard.test.ts +142 -0
- package/tests/bounded-writer-admission.test.ts +11 -3
- package/tests/bundled-install.test.ts +3 -3
- package/tests/card-content-policy.test.ts +19 -9
- package/tests/child-safety.test.ts +39 -0
- package/tests/clipboard-image.test.ts +114 -0
- package/tests/codegraph-tools.test.ts +123 -2
- package/tests/command-palette.test.ts +15 -5
- package/tests/commit-slice-signal.test.ts +162 -0
- package/tests/context-backstop.test.ts +225 -0
- package/tests/delegate-for-reason-contract.test.ts +92 -24
- package/tests/delivered-test-lint.test.ts +105 -0
- package/tests/design-canvas-assets.test.ts +710 -0
- package/tests/design-canvas-server.test.ts +1096 -0
- package/tests/design-comments.test.ts +201 -0
- package/tests/design-handoffs.test.ts +282 -0
- package/tests/design-open-browser.test.ts +70 -0
- package/tests/design-sources.test.ts +132 -0
- package/tests/destructive-command-guard.test.ts +85 -1
- package/tests/engram-safety.test.ts +132 -57
- package/tests/external-editor.test.ts +84 -0
- package/tests/fixtures/primary-session-prompt/gentleman.golden.md +143 -0
- package/tests/fixtures/provider-contract-bundle/v1.4.0/README.md +29 -0
- package/tests/fixtures/provider-contract-bundle/v1.4.0/manifest.json +83 -0
- package/tests/fixtures/provider-contract-bundle/v1.4.0/orchestration/pi.md +70 -0
- package/tests/fixtures/provider-contract-bundle/v1.4.0/routing/pi.md +92 -0
- package/tests/fixtures/provider-contract-bundle/v1.4.0/schemas/lens.schema.json +16 -0
- package/tests/fixtures/provider-contract-bundle/v1.4.0/schemas/refuter.schema.json +1 -0
- package/tests/fixtures/provider-contract-bundle/v1.4.0/schemas/targeted-validator.schema.json +1 -0
- package/tests/fixtures/provider-contract-bundle/v1.4.0/vectors/lens.json +1 -0
- package/tests/fixtures/provider-contract-bundle/v1.4.0/vectors/refuter.json +1 -0
- package/tests/fixtures/provider-contract-bundle/v1.4.0/vectors/targeted-validator.json +1 -0
- package/tests/gauge-theme.test.ts +120 -0
- package/tests/gentle-agents.test.ts +645 -21
- package/tests/gentle-ai-binary.test.ts +1 -1
- package/tests/gentle-ai-child-guards.test.ts +61 -0
- package/tests/gentle-ai-doctor-herdr.test.ts +94 -0
- package/tests/gentle-ai-installer.test.ts +51 -51
- package/tests/gentle-ai.test.ts +292 -13
- package/tests/gentle-design.test.ts +1313 -0
- package/tests/gentle-music.test.ts +335 -0
- package/tests/gentle-shell-bin.test.ts +163 -0
- package/tests/gentle-shell-bundled.test.ts +392 -176
- package/tests/gentle-shell-import-pi.test.ts +262 -0
- package/tests/gentle-shell-launcher.test.ts +107 -2
- package/tests/gentle-shell-startup-update.test.ts +77 -1
- package/tests/gentle-shell-uninstall.test.ts +89 -0
- package/tests/gentle-shell.test.ts +225 -17
- package/tests/gentle-todo.test.ts +190 -0
- package/tests/headless-delivery-question.test.ts +58 -0
- package/tests/herdr-activity.test.ts +34 -1
- package/tests/image-attachments.test.ts +119 -0
- package/tests/image-paste-chips.test.ts +236 -0
- package/tests/inprocess-reviewer.test.ts +55 -2
- package/tests/installer-bundles.test.ts +12 -6
- package/tests/installer-posix-bootstrap.test.ts +8 -8
- package/tests/installer-preflight.test.ts +16 -0
- package/tests/installer-server.test.ts +10 -2
- package/tests/installer-windows-bootstrap.test.ts +94 -7
- package/tests/installer-windows-diagnostics.test.ts +111 -0
- package/tests/main-channel-upgrade.test.ts +232 -2
- package/tests/main-channel.test.ts +140 -21
- package/tests/nan-provider.test.ts +36 -0
- package/tests/native-review-capability-contract.test.ts +17 -4
- package/tests/native-review-parity.test.ts +23 -2
- package/tests/no-self-pause-contract.test.ts +5 -2
- package/tests/notification-audio-native.test.ts +128 -9
- package/tests/notification-customize.test.ts +17 -5
- package/tests/odd-integration.test.ts +5 -1
- package/tests/odd-routing-canonical-ratchet.test.ts +128 -162
- package/tests/odd-routing-contract.test.ts +115 -30
- package/tests/orchestrator-budget.test.ts +22 -8
- package/tests/orchestrator-discovery.test.ts +29 -0
- package/tests/orchestrator-presence.test.ts +100 -2
- package/tests/orchestrator-rdd-ownership.test.ts +3 -1
- package/tests/orchestrator-scope.test.ts +80 -0
- package/tests/orchestrator-state.test.ts +29 -0
- package/tests/orchestrator-status-publication-contract.test.ts +12 -8
- package/tests/package-files.test.ts +34 -0
- package/tests/package-manifest.test.ts +24 -20
- package/tests/permission-mode-policy.test.ts +10 -1
- package/tests/pi-update-launcher.test.ts +21 -2
- package/tests/pi-update.test.ts +99 -4
- package/tests/primary-session-prompt.test.ts +101 -0
- package/tests/provider-contract-bundle.test.ts +118 -0
- package/tests/provider-contract-mirror.test.ts +24 -0
- package/tests/quiet-tool-rendering.test.ts +23 -0
- package/tests/rdd-aware-verification-contract.test.ts +29 -11
- package/tests/rdd-status-line.test.ts +79 -0
- package/tests/review-agent-end-preflight.test.ts +6 -0
- package/tests/review-candidate-view.test.ts +49 -0
- package/tests/review-contract-prompt.test.ts +5 -2
- package/tests/review-controller-native-routing.test.ts +14 -1
- package/tests/review-controller.test.ts +1 -1
- package/tests/review-host-relay-routing.test.ts +147 -3
- package/tests/review-host-relay.test.ts +9 -2
- package/tests/review-json-arguments.test.ts +20 -3
- package/tests/review-loadout-rdd-off-sdk.test.ts +258 -0
- package/tests/review-owner-identity.test.ts +132 -0
- package/tests/review-relay-transport-agent.test.ts +26 -0
- package/tests/review-reminder-receipt.test.ts +31 -1
- package/tests/review-risk-assessment.test.ts +163 -76
- package/tests/review-session-standing-permission-controller.test.ts +14 -1
- package/tests/review-session-standing-permission.test.ts +27 -1
- package/tests/run-test-suite.test.ts +15 -0
- package/tests/runtime-harness.mjs +16 -6
- package/tests/session-changes-shell.test.ts +1 -1
- package/tests/session-changes.test.ts +2 -1
- package/tests/session-profile-append-controller-fork.test.ts +458 -0
- package/tests/session-profile-append-controller-pi.test.ts +290 -0
- package/tests/session-profile-append-controller.test.ts +731 -0
- package/tests/session-profile-disk-reader-pi.test.ts +183 -0
- package/tests/session-profile-disk-reader.test.ts +311 -0
- package/tests/session-profile-freeze.test.ts +23 -0
- package/tests/session-scratch.test.ts +1011 -0
- package/tests/session-subject-routing-contract.test.ts +2 -1
- package/tests/session-worktree-registry.test.ts +13 -1
- package/tests/shell-bar.test.ts +21 -2
- package/tests/shell-changes-view.test.ts +137 -1
- package/tests/shell-changes.test.ts +148 -1
- package/tests/shell-gauge.test.ts +42 -0
- package/tests/shell-prompt.test.ts +3 -3
- package/tests/shell-render-cache.test.ts +134 -1
- package/tests/shell-sidebar-layout.test.ts +50 -0
- package/tests/short-check-routing-contract.test.ts +1 -1
- package/tests/skill-collision-prefixes.test.ts +3 -1
- package/tests/skill-registry-watcher.test.ts +17 -5
- package/tests/skill-registry.test.ts +47 -3
- package/tests/small-task-inline-contract.test.ts +37 -0
- package/tests/standalone-config-home.test.ts +324 -0
- package/tests/startup-banner.test.ts +5 -4
- package/tests/startup-home-repo.test.ts +34 -0
- package/tests/support/orchestrator-modules.ts +1 -0
- package/tests/task-size-routing-contract.test.ts +70 -10
- package/tests/test-scratch-contract.test.ts +102 -0
- package/tests/test-strength-contract.test.ts +36 -1
- package/tests/tool-batching-contract.test.ts +64 -0
- package/tests/verification-proportionality.test.ts +46 -0
- package/tests/verifier-admission.test.ts +154 -0
- package/tests/verify-agent-contract.test.ts +8 -8
- package/tests/voice-command.test.ts +773 -0
- package/tests/voice-install.test.ts +323 -0
- package/tests/voice-platform.test.ts +176 -0
- package/tests/voice-push-to-talk.test.ts +444 -0
- package/tests/voice-session.test.ts +434 -0
- package/tests/voice-settings.test.ts +49 -0
- package/tests/voice-ui.test.ts +97 -0
- package/tests/voice-worker.test.ts +445 -0
- package/tests/weak-model-economy-prompt.test.ts +32 -0
- package/tests/windows-hidden-processes.test.ts +30 -2
- package/tests/writer-edit-surface-scope.test.ts +3 -0
- package/tests/writer-feature-document-admission.test.ts +156 -0
- package/tests/yolo-customize.test.ts +5 -1
- package/tests/yolo-mode.test.ts +19 -1
- package/tests/yolo-session-policy-capture.test.ts +104 -0
- package/tests/yolo-temp-scratch.test.ts +71 -0
- package/scripts/mirror-odd-routing.mjs +0 -242
- /package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/orchestration/pi.md +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/schemas/lens.schema.json +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/schemas/refuter.schema.json +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/schemas/targeted-validator.schema.json +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/vectors/lens.json +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/vectors/refuter.json +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/vectors/targeted-validator.json +0 -0
|
@@ -4,7 +4,7 @@ import { chmodSync, existsSync, mkdirSync, mkdtempSync, readFileSync, realpathSy
|
|
|
4
4
|
import { createRequire } from "node:module";
|
|
5
5
|
import { tmpdir } from "node:os";
|
|
6
6
|
import { join } from "node:path";
|
|
7
|
-
import test, { after, afterEach } from "node:test";
|
|
7
|
+
import test, { after, afterEach, type TestContext } from "node:test";
|
|
8
8
|
import { initTheme, type ExtensionAPI, type ExtensionContext, type SlashCommandInfo, type SourceInfo } from "@earendil-works/pi-coding-agent";
|
|
9
9
|
import { CURSOR_MARKER, matchesKey, stripTerminalSequences as stripAnsi, TuiMainScreen, visibleWidth, type TUI, type TuiMouseEvent } from "@earendil-works/pi-tui";
|
|
10
10
|
import installGentleShell, { backgroundChangeRefreshesSettled, buildShellBarModel, createActiveProfileReader, changesShortcut, devBinaryCard, extractQueuedText, fetchCodexUsage, fetchNanUsage, loadFileDiff, shellGitRunner, openInExternalEditor, usageShortcut, GentlePromptEditor } from "../extensions/gentle-shell.ts";
|
|
@@ -88,6 +88,7 @@ interface FakeUi {
|
|
|
88
88
|
overlay: unknown;
|
|
89
89
|
overlayView: { render(width: number): string[]; handleInput(data: string): void } | undefined;
|
|
90
90
|
closeOverlay: (() => void) | undefined;
|
|
91
|
+
customOptions?: unknown[];
|
|
91
92
|
}
|
|
92
93
|
|
|
93
94
|
interface GitScript {
|
|
@@ -162,6 +163,9 @@ function fakePi(script: GitScript[] = [{ numstat: "", porcelain: "" }], commands
|
|
|
162
163
|
if (args.includes("worktree")) return { stdout: "worktree /repo\0branch refs/heads/main\0\0", stderr: "", code: 0, killed: false };
|
|
163
164
|
// Roots outside the worktree list read as a detached HEAD.
|
|
164
165
|
if (args.includes("symbolic-ref")) return { stdout: "", stderr: "", code: 1, killed: false };
|
|
166
|
+
// The unborn-HEAD ask is cheap metadata, not a scan round: the script
|
|
167
|
+
// models one numstat+porcelain pair per round, exactly like real Git.
|
|
168
|
+
if (args.includes("rev-parse")) return { stdout: "", stderr: "", code: 0, killed: false };
|
|
165
169
|
const isNumstat = args.includes("diff");
|
|
166
170
|
const step = script[Math.min(isNumstat ? round : round++, script.length - 1)];
|
|
167
171
|
return { stdout: isNumstat ? step.numstat : step.porcelain, stderr: "", code: 0, killed: false };
|
|
@@ -175,7 +179,7 @@ async function fire(handlers: Map<string, Array<(event: unknown, ctx: ExtensionC
|
|
|
175
179
|
}
|
|
176
180
|
|
|
177
181
|
function fakeContext(options: { theme?: typeof plainTheme & { getBgAnsi?(): string }; hasUI?: boolean; entries?: unknown[]; oauth?: boolean; pending?: boolean; idle?: boolean; editorFactory?: unknown; token?: string; select?: (title: string, options: string[]) => Promise<string | undefined> } = {}): { ctx: ExtensionContext; ui: FakeUi; overlayReady: Promise<void> } {
|
|
178
|
-
const ui: FakeUi = { footerFactory: undefined, editorFactory: options.editorFactory, widgets: new Map(), widgetSets: 0, workingVisible: undefined, notices: [], overlay: undefined, overlayView: undefined, closeOverlay: undefined };
|
|
182
|
+
const ui: FakeUi = { footerFactory: undefined, editorFactory: options.editorFactory, widgets: new Map(), widgetSets: 0, workingVisible: undefined, notices: [], overlay: undefined, overlayView: undefined, closeOverlay: undefined, customOptions: [] };
|
|
179
183
|
let resolveOverlay: () => void;
|
|
180
184
|
const overlayReady = new Promise<void>((resolve) => { resolveOverlay = resolve; });
|
|
181
185
|
const entries = options.entries ?? [];
|
|
@@ -226,8 +230,9 @@ function fakeContext(options: { theme?: typeof plainTheme & { getBgAnsi?(): stri
|
|
|
226
230
|
// whatever their `done` callback is given (see ExtensionUIContext.custom
|
|
227
231
|
// in pi-coding-agent), not always null. `closeOverlay` stays a
|
|
228
232
|
// null-resolving escape hatch for tests that only need to end the wait.
|
|
229
|
-
custom<T>(factory: (tui: unknown, theme: unknown, keybindings: unknown, done: (value: T) => void) => { render(width: number): string[]; handleInput(data: string): void }) {
|
|
233
|
+
custom<T>(factory: (tui: unknown, theme: unknown, keybindings: unknown, done: (value: T) => void) => { render(width: number): string[]; handleInput(data: string): void }, options?: unknown) {
|
|
230
234
|
ui.overlay = factory;
|
|
235
|
+
ui.customOptions?.push(options);
|
|
231
236
|
return new Promise<T | null>((resolve) => {
|
|
232
237
|
ui.closeOverlay = () => resolve(null);
|
|
233
238
|
// An identity bg mock hides focused rows; omit it so overlay navigation
|
|
@@ -2046,7 +2051,7 @@ test("gentleShell shows working while the agent runs and queued when messages wa
|
|
|
2046
2051
|
const editor = installedPrompt(ctx, ui, handlers);
|
|
2047
2052
|
|
|
2048
2053
|
for (const handler of handlers.get("agent_start") ?? []) handler({}, ctx);
|
|
2049
|
-
assert.match(stripAnsi(editor.render(60)[0]), /^╭─
|
|
2054
|
+
assert.match(stripAnsi(editor.render(60)[0]), /^╭─ [✿❀❁✾] working… ─+╮$/);
|
|
2050
2055
|
pending.value = true;
|
|
2051
2056
|
assert.match(stripAnsi(editor.render(60)[0]), /^╭─ [✿❀❁✾] queued ─+╮$/);
|
|
2052
2057
|
for (const handler of handlers.get("agent_end") ?? []) handler({}, ctx);
|
|
@@ -2819,7 +2824,7 @@ test("vim selector exposes enable, disable and status; status and cancellation d
|
|
|
2819
2824
|
} finally { editor.dispose(); }
|
|
2820
2825
|
});
|
|
2821
2826
|
|
|
2822
|
-
test("animations command reports without writing and switches the live
|
|
2827
|
+
test("animations command reports without writing and switches the live watchdog", async (t) => {
|
|
2823
2828
|
const configHome = scopedDoubleEscCancelConfigHome(t);
|
|
2824
2829
|
const delays: number[] = [];
|
|
2825
2830
|
let active = 0;
|
|
@@ -2837,9 +2842,9 @@ test("animations command reports without writing and switches the live pulse", a
|
|
|
2837
2842
|
assert.match(ui.notices.at(-1)!, /animations: quality/);
|
|
2838
2843
|
assert.equal(existsSync(join(configHome, "animations.json")), false);
|
|
2839
2844
|
for (const handler of handlers.get("agent_start") ?? []) handler({}, ctx);
|
|
2840
|
-
assert.deepEqual(delays, [2000,
|
|
2845
|
+
assert.deepEqual(delays, [2000, 1000]);
|
|
2841
2846
|
await command.handler("performance", ctx);
|
|
2842
|
-
assert.deepEqual(delays, [2000,
|
|
2847
|
+
assert.deepEqual(delays, [2000, 1000, 1000]);
|
|
2843
2848
|
assert.equal(active, 2);
|
|
2844
2849
|
await command.handler("potato", ctx);
|
|
2845
2850
|
assert.equal(active, 1);
|
|
@@ -2848,7 +2853,7 @@ test("animations command reports without writing and switches the live pulse", a
|
|
|
2848
2853
|
await command.handler("invalid", ctx);
|
|
2849
2854
|
assert.equal(JSON.parse(readFileSync(join(configHome, "animations.json"), "utf8")).policy, "potato");
|
|
2850
2855
|
await command.handler("quality", ctx);
|
|
2851
|
-
assert.deepEqual(delays, [2000,
|
|
2856
|
+
assert.deepEqual(delays, [2000, 1000, 1000, 1000]);
|
|
2852
2857
|
for (const handler of handlers.get("agent_settled") ?? []) handler({}, ctx);
|
|
2853
2858
|
assert.equal(active, 1);
|
|
2854
2859
|
editor.dispose();
|
|
@@ -2937,7 +2942,7 @@ test("animations with no argument opens a selectable menu and applies the chosen
|
|
|
2937
2942
|
assert.equal(existsSync(join(dismissHome, "animations.json")), false);
|
|
2938
2943
|
});
|
|
2939
2944
|
|
|
2940
|
-
test("prompt
|
|
2945
|
+
test("prompt installs the silence watchdog unref'd and releases it at settlement", async (t) => {
|
|
2941
2946
|
const configHome = scopedDoubleEscCancelConfigHome(t);
|
|
2942
2947
|
writeFileSync(join(configHome, "animations.json"), '{"schema":"gentle-pi.animations/v1","policy":"quality"}');
|
|
2943
2948
|
const delays: number[] = [];
|
|
@@ -2955,7 +2960,7 @@ test("prompt uses the compact banner cadence and releases its unref timer at set
|
|
|
2955
2960
|
const editor = installedPrompt(ctx, ui, handlers);
|
|
2956
2961
|
assert.equal(active, 1);
|
|
2957
2962
|
for (const handler of handlers.get("agent_start") ?? []) handler({}, ctx);
|
|
2958
|
-
assert.deepEqual(delays, [2000,
|
|
2963
|
+
assert.deepEqual(delays, [2000, 1000]);
|
|
2959
2964
|
assert.equal(unrefs, 2);
|
|
2960
2965
|
for (const handler of handlers.get("agent_settled") ?? []) handler({}, ctx);
|
|
2961
2966
|
assert.equal(active, 1);
|
|
@@ -2964,6 +2969,109 @@ test("prompt uses the compact banner cadence and releases its unref timer at set
|
|
|
2964
2969
|
assert.equal(active, 0);
|
|
2965
2970
|
});
|
|
2966
2971
|
|
|
2972
|
+
// gentle-shell#994: the working animation is render-driven. Frames the host
|
|
2973
|
+
// already paints while streaming advance the petal for free; a low-cadence
|
|
2974
|
+
// watchdog repaints only silent phases (long tool runs with no output).
|
|
2975
|
+
interface RecordedInterval { tick: () => void; delay: number; stopped: boolean }
|
|
2976
|
+
|
|
2977
|
+
function recordIntervals(t: TestContext): RecordedInterval[] {
|
|
2978
|
+
const intervals: RecordedInterval[] = [];
|
|
2979
|
+
t.mock.method(globalThis, "setInterval", (tick: () => void, delay: number) => {
|
|
2980
|
+
const timer: RecordedInterval = { tick, delay, stopped: false };
|
|
2981
|
+
intervals.push(timer);
|
|
2982
|
+
return { unref() {}, timer };
|
|
2983
|
+
});
|
|
2984
|
+
t.mock.method(globalThis, "clearInterval", (handle: { timer: RecordedInterval }) => { handle.timer.stopped = true; });
|
|
2985
|
+
return intervals;
|
|
2986
|
+
}
|
|
2987
|
+
|
|
2988
|
+
function petalGlyphOf(editor: GentlePromptEditor): string {
|
|
2989
|
+
return stripAnsi(editor.render(60)[0]).match(/[✿❀❁✾]/)?.[0] ?? "";
|
|
2990
|
+
}
|
|
2991
|
+
|
|
2992
|
+
test("streaming frames advance the working petal and the quality watchdog stays silent while renders flow", (t) => {
|
|
2993
|
+
const configHome = scopedDoubleEscCancelConfigHome(t);
|
|
2994
|
+
writeFileSync(join(configHome, "animations.json"), '{"schema":"gentle-pi.animations/v1","policy":"quality"}');
|
|
2995
|
+
const intervals = recordIntervals(t);
|
|
2996
|
+
const renders = t.mock.method(fakeTui, "requestRender", () => {});
|
|
2997
|
+
const { pi, handlers } = fakePi();
|
|
2998
|
+
gentleShell(pi, { GENTLE_PI_CONFIG_HOME: configHome }, { now: () => 1_000_000 });
|
|
2999
|
+
const { ctx, ui } = fakeContext();
|
|
3000
|
+
const editor = installedPrompt(ctx, ui, handlers);
|
|
3001
|
+
for (const handler of handlers.get("agent_start") ?? []) handler({}, ctx);
|
|
3002
|
+
const watchdog = intervals.find((entry) => entry.delay === 1000);
|
|
3003
|
+
assert.ok(watchdog, "working installs the 1000ms silence watchdog");
|
|
3004
|
+
const glyphs = [0, 1, 2, 3].map(() => petalGlyphOf(editor));
|
|
3005
|
+
assert.ok(new Set(glyphs).size > 1, `each streamed frame must advance the petal, got ${glyphs.join("")}`);
|
|
3006
|
+
const before = renders.mock.callCount();
|
|
3007
|
+
watchdog!.tick();
|
|
3008
|
+
assert.equal(renders.mock.callCount(), before, "renders are already flowing, so the watchdog must not force one");
|
|
3009
|
+
editor.dispose();
|
|
3010
|
+
assert.equal(watchdog!.stopped, true);
|
|
3011
|
+
});
|
|
3012
|
+
|
|
3013
|
+
test("silent working phases repaint only through the 1000ms watchdog window", (t) => {
|
|
3014
|
+
const configHome = scopedDoubleEscCancelConfigHome(t);
|
|
3015
|
+
writeFileSync(join(configHome, "animations.json"), '{"schema":"gentle-pi.animations/v1","policy":"quality"}');
|
|
3016
|
+
const intervals = recordIntervals(t);
|
|
3017
|
+
const renders = t.mock.method(fakeTui, "requestRender", () => {});
|
|
3018
|
+
let clock = 1_000_000;
|
|
3019
|
+
const { pi, handlers } = fakePi();
|
|
3020
|
+
gentleShell(pi, { GENTLE_PI_CONFIG_HOME: configHome }, { now: () => clock });
|
|
3021
|
+
const { ctx, ui } = fakeContext();
|
|
3022
|
+
const editor = installedPrompt(ctx, ui, handlers);
|
|
3023
|
+
for (const handler of handlers.get("agent_start") ?? []) handler({}, ctx);
|
|
3024
|
+
const watchdog = intervals.find((entry) => entry.delay === 1000);
|
|
3025
|
+
assert.ok(watchdog, "working installs the 1000ms silence watchdog");
|
|
3026
|
+
const baseline = petalGlyphOf(editor);
|
|
3027
|
+
const before = renders.mock.callCount();
|
|
3028
|
+
watchdog!.tick();
|
|
3029
|
+
assert.equal(renders.mock.callCount(), before, "no frame painted within the window, but the window has not elapsed yet");
|
|
3030
|
+
clock += 1000;
|
|
3031
|
+
watchdog!.tick();
|
|
3032
|
+
assert.equal(renders.mock.callCount(), before + 1, "silence past the window forces exactly one repaint");
|
|
3033
|
+
assert.notEqual(petalGlyphOf(editor), baseline, "the watchdog advanced the petal tick");
|
|
3034
|
+
const afterForced = renders.mock.callCount();
|
|
3035
|
+
watchdog!.tick();
|
|
3036
|
+
assert.equal(renders.mock.callCount(), afterForced, "the watchdog's own repaint counts as render traffic for the next window");
|
|
3037
|
+
editor.dispose();
|
|
3038
|
+
});
|
|
3039
|
+
|
|
3040
|
+
test("potato installs no working watchdog and the petal never advances", (t) => {
|
|
3041
|
+
const configHome = scopedDoubleEscCancelConfigHome(t);
|
|
3042
|
+
writeFileSync(join(configHome, "animations.json"), '{"schema":"gentle-pi.animations/v1","policy":"potato"}');
|
|
3043
|
+
const intervals = recordIntervals(t);
|
|
3044
|
+
const { pi, handlers } = fakePi();
|
|
3045
|
+
gentleShell(pi, { GENTLE_PI_CONFIG_HOME: configHome });
|
|
3046
|
+
const { ctx, ui } = fakeContext();
|
|
3047
|
+
const editor = installedPrompt(ctx, ui, handlers);
|
|
3048
|
+
for (const handler of handlers.get("agent_start") ?? []) handler({}, ctx);
|
|
3049
|
+
assert.ok(!intervals.some((entry) => entry.delay !== 2000), "potato must not animate: no watchdog interval at all");
|
|
3050
|
+
const glyphs = [0, 1, 2].map(() => petalGlyphOf(editor));
|
|
3051
|
+
assert.deepEqual(glyphs, ["✿", "✿", "✿"], "potato keeps the petal static");
|
|
3052
|
+
editor.dispose();
|
|
3053
|
+
});
|
|
3054
|
+
|
|
3055
|
+
test("the working watchdog is cleared on settlement and on dispose", (t) => {
|
|
3056
|
+
const configHome = scopedDoubleEscCancelConfigHome(t);
|
|
3057
|
+
writeFileSync(join(configHome, "animations.json"), '{"schema":"gentle-pi.animations/v1","policy":"quality"}');
|
|
3058
|
+
const intervals = recordIntervals(t);
|
|
3059
|
+
const { pi, handlers } = fakePi();
|
|
3060
|
+
gentleShell(pi, { GENTLE_PI_CONFIG_HOME: configHome });
|
|
3061
|
+
const { ctx, ui } = fakeContext();
|
|
3062
|
+
const editor = installedPrompt(ctx, ui, handlers);
|
|
3063
|
+
for (const handler of handlers.get("agent_start") ?? []) handler({}, ctx);
|
|
3064
|
+
const watchdog = intervals.filter((entry) => entry.delay === 1000).at(-1);
|
|
3065
|
+
assert.ok(watchdog && !watchdog.stopped, "working keeps the watchdog armed");
|
|
3066
|
+
for (const handler of handlers.get("agent_settled") ?? []) handler({}, ctx);
|
|
3067
|
+
assert.equal(watchdog!.stopped, true, "settlement must clear the watchdog");
|
|
3068
|
+
for (const handler of handlers.get("agent_start") ?? []) handler({}, ctx);
|
|
3069
|
+
const rearmWatchdog = intervals.filter((entry) => entry.delay === 1000).at(-1);
|
|
3070
|
+
assert.ok(rearmWatchdog && rearmWatchdog !== watchdog && !rearmWatchdog.stopped, "the next turn re-arms a fresh watchdog");
|
|
3071
|
+
editor.dispose();
|
|
3072
|
+
assert.equal(rearmWatchdog!.stopped, true, "dispose must clear the watchdog");
|
|
3073
|
+
});
|
|
3074
|
+
|
|
2967
3075
|
// ---------------------------------------------------------------------------
|
|
2968
3076
|
// double-esc-cancel (issue #1163): opt-in, off by default. While the prompt
|
|
2969
3077
|
// is working and autocomplete is hidden, the first Esc is swallowed and the
|
|
@@ -4775,11 +4883,43 @@ test("a registered worktree opens Changes as its own group", async () => {
|
|
|
4775
4883
|
} finally { ui.closeOverlay?.(); await opened; await fire(handlers,"session_shutdown",ctx); }
|
|
4776
4884
|
});
|
|
4777
4885
|
|
|
4886
|
+
// /gentle:changes opens like Gentle Agents: a full-terminal overlay whose
|
|
4887
|
+
// native fullscreen wrapper still routes mouse events to the changes view.
|
|
4888
|
+
test("Changes opens full screen and the wrapper keeps mouse hit-testing aligned", async () => {
|
|
4889
|
+
const {pi,handlers,commands}=fakePi([{numstat:"",porcelain:" M file.ts\0?? dirty.ts\0"}]);
|
|
4890
|
+
gentleShell(pi,{});
|
|
4891
|
+
const {ctx,ui,overlayReady}=fakeContext();
|
|
4892
|
+
await fire(handlers,"session_start",ctx);
|
|
4893
|
+
const opened=commands.get("gentle:changes")!.handler("",ctx);
|
|
4894
|
+
await overlayReady;
|
|
4895
|
+
const overlay=ui.overlayView as unknown as { render(width: number): string[]; handleInput(data: string): void; handleMouse(event: unknown): unknown };
|
|
4896
|
+
const at=(type: string, x: number, y: number) => ({ type, button: type === "move" ? "none" : "left", x, y, screenX: x, screenY: y, width: 140, height: 40, shift: false, alt: false, ctrl: false });
|
|
4897
|
+
try {
|
|
4898
|
+
assert.deepEqual(ui.customOptions?.at(-1), { overlay: true, overlayOptions: { width: "100%", maxHeight: "100%", margin: 0, anchor: "center" } });
|
|
4899
|
+
assert.equal(overlay.render(140).length, 40, "the overlay uses every terminal row");
|
|
4900
|
+
overlay.handleInput("\r");
|
|
4901
|
+
overlay.render(140);
|
|
4902
|
+
const { handled, render } = overlay.handleMouse(at("click", 3, 3)) as { handled?: boolean; render?: boolean };
|
|
4903
|
+
assert.deepEqual({ handled, render }, { handled: true, render: true });
|
|
4904
|
+
assert.match(overlay.render(140)[3]!, /▸ M file\.ts/, "a click through the wrapper selects the file");
|
|
4905
|
+
overlay.handleInput("F");
|
|
4906
|
+
assert.match(overlay.render(140)[1]!, /^│ M file\.ts +│$/, "F switches to diff-only inside the fullscreen overlay");
|
|
4907
|
+
} finally { ui.closeOverlay?.(); await opened; }
|
|
4908
|
+
const reopened=commands.get("gentle:changes")!.handler("",ctx);
|
|
4909
|
+
for (let attempt=0; attempt<100 && ui.overlayView===overlay; attempt++) await new Promise((resolve)=>setTimeout(resolve,5));
|
|
4910
|
+
try {
|
|
4911
|
+
assert.match(ui.overlayView!.render(140)[1]!, /main · repo/, "diff-only mode is not persisted across openings");
|
|
4912
|
+
} finally { ui.closeOverlay?.(); await reopened; await fire(handlers,"session_shutdown",ctx); }
|
|
4913
|
+
});
|
|
4914
|
+
|
|
4778
4915
|
test("new sessions do not inherit another session's worktrees", async () => {
|
|
4779
4916
|
const h=fakePi(); gentleShell(h.pi,{});
|
|
4780
4917
|
const first=fakeContext();
|
|
4781
4918
|
await fire(h.handlers,"session_start",first.ctx);
|
|
4782
4919
|
await h.tools.get("session_worktree_register")!.execute("r",{path:"/linked"},undefined,undefined,first.ctx);
|
|
4920
|
+
// The registration kicks off its own async scan; settle it so its Git calls
|
|
4921
|
+
// cannot land after the next session clears the shared call log.
|
|
4922
|
+
await changesSettled();
|
|
4783
4923
|
await fire(h.handlers,"session_shutdown",first.ctx);
|
|
4784
4924
|
const next=fakeContext({entries:[...first.ctx.sessionManager.getEntries()]});
|
|
4785
4925
|
(next.ctx.sessionManager as any).getSessionId=()=>"new-session";
|
|
@@ -4849,7 +4989,7 @@ test("registered canonical root governs real Git discovery, status and diff desp
|
|
|
4849
4989
|
assert.ok(await loadFileDiff(bounded, { path: "tracked.txt", added: 70_001, deleted: 1, status: CHANGE_STATUS.MODIFIED }) === DIFF_TOO_LARGE);
|
|
4850
4990
|
const opened = h.commands.get("gentle:changes")!.handler("", ctx);
|
|
4851
4991
|
await overlayReady;
|
|
4852
|
-
const trees = (ui.overlayView as unknown as { trees: Array<{ root: string; model: { files: Array<{ path: string }> } }> }).trees;
|
|
4992
|
+
const trees = (ui.overlayView as unknown as { children: Array<{ trees: Array<{ root: string; model: { files: Array<{ path: string }> } }> }> }).children[0]!.trees;
|
|
4853
4993
|
assert.deepEqual(trees.map((tree) => [tree.root, tree.model.files.map((file) => file.path)]), [[selected, ["selected-only.txt", "tracked.txt"]]], "inherited routing never lists the foreign repository");
|
|
4854
4994
|
ui.closeOverlay?.();
|
|
4855
4995
|
await opened;
|
|
@@ -6281,19 +6421,21 @@ test("an explicit session binding routes the Usage scope ahead of the pin and th
|
|
|
6281
6421
|
|
|
6282
6422
|
// S9: the config home each of these reads resolves, per case. The OS home is
|
|
6283
6423
|
// a temporary folder in every case, so no case can reach the real one.
|
|
6284
|
-
// "unset" and "empty"
|
|
6285
|
-
//
|
|
6286
|
-
// a bundled install (testing overlay) whose
|
|
6287
|
-
// user's own GENTLE_PI_CONFIG_HOME is then not
|
|
6424
|
+
// "unset" and "empty" both name ~/.pi/gentle-ai: an empty GENTLE_PI_CONFIG_HOME
|
|
6425
|
+
// counts as unset (#2201; it used to name the current folder). "bundled" stands
|
|
6426
|
+
// in for this package running from a bundled install (testing overlay) whose
|
|
6427
|
+
// prefix the launcher passed; the user's own GENTLE_PI_CONFIG_HOME is then not
|
|
6428
|
+
// this install's.
|
|
6288
6429
|
type ConfigHomeCase = "unset" | "empty" | "bundled";
|
|
6289
6430
|
function configHomeCase(t: { after: (fn: () => void) => void }, kind: ConfigHomeCase): { env: NodeJS.ProcessEnv; dir: string; userConfig: string } {
|
|
6290
6431
|
const base = realpathSync(mkdtempSync(join(tmpdir(), `config-home-${kind}-`)));
|
|
6291
|
-
const saved = { HOME: process.env.HOME, USERPROFILE: process.env.USERPROFILE, GENTLE_PI_CONFIG_HOME: process.env.GENTLE_PI_CONFIG_HOME, GENTLE_SHELL_PREFIX: process.env.GENTLE_SHELL_PREFIX };
|
|
6432
|
+
const saved = { HOME: process.env.HOME, USERPROFILE: process.env.USERPROFILE, GENTLE_PI_CONFIG_HOME: process.env.GENTLE_PI_CONFIG_HOME, GENTLE_SHELL_PREFIX: process.env.GENTLE_SHELL_PREFIX, GENTLE_SHELL_CONFIG_HOME: process.env.GENTLE_SHELL_CONFIG_HOME };
|
|
6292
6433
|
const cwd = process.cwd();
|
|
6293
6434
|
process.env.HOME = join(base, "home");
|
|
6294
6435
|
process.env.USERPROFILE = join(base, "home");
|
|
6295
6436
|
delete process.env.GENTLE_PI_CONFIG_HOME;
|
|
6296
6437
|
delete process.env.GENTLE_SHELL_PREFIX;
|
|
6438
|
+
delete process.env.GENTLE_SHELL_CONFIG_HOME;
|
|
6297
6439
|
t.after(() => {
|
|
6298
6440
|
process.chdir(cwd);
|
|
6299
6441
|
setBundledInstallForTesting(undefined);
|
|
@@ -6308,7 +6450,7 @@ function configHomeCase(t: { after: (fn: () => void) => void }, kind: ConfigHome
|
|
|
6308
6450
|
if (kind === "empty") {
|
|
6309
6451
|
mkdirSync(join(base, "cwd"));
|
|
6310
6452
|
process.chdir(join(base, "cwd"));
|
|
6311
|
-
return { env: { GENTLE_PI_CONFIG_HOME: "" }, dir: join(base, "
|
|
6453
|
+
return { env: { GENTLE_PI_CONFIG_HOME: "" }, dir: join(base, "home", ".pi", "gentle-ai"), userConfig };
|
|
6312
6454
|
}
|
|
6313
6455
|
const prefix = join(base, "prefix");
|
|
6314
6456
|
mkdirSync(prefix);
|
|
@@ -6343,3 +6485,69 @@ for (const kind of ["unset", "empty", "bundled"] as const) {
|
|
|
6343
6485
|
assert.equal(read(), "other (repo)", "the pinned profile exists only in that config home's store");
|
|
6344
6486
|
});
|
|
6345
6487
|
}
|
|
6488
|
+
|
|
6489
|
+
// gentle-shell#2464: a pasted image is an atomic chip in the draft and a real
|
|
6490
|
+
// image attachment on submit, never a temp-file path the model must `read`.
|
|
6491
|
+
const CHIP_PNG = new Uint8Array([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, 0x00, 0x0d, 0x49, 0x48, 0x44, 0x52, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x01]);
|
|
6492
|
+
const chipKeybindings = { matches: (data: string, action: string) => action === "app.clipboard.pasteImage" && data === "\x1bv" };
|
|
6493
|
+
|
|
6494
|
+
async function pastedChipPrompt(model?: ExtensionContext["model"]) {
|
|
6495
|
+
const { pi, handlers } = fakePi();
|
|
6496
|
+
gentleShell(pi, {}, { readClipboardImage: async () => ({ bytes: CHIP_PNG, mimeType: "image/png" }) });
|
|
6497
|
+
const { ctx, ui } = fakeContext();
|
|
6498
|
+
if (model) (ctx as { model: unknown }).model = model;
|
|
6499
|
+
const editor = installedPrompt(ctx, ui, handlers, chipKeybindings);
|
|
6500
|
+
editor.onPasteImage = () => assert.fail("Pi's path-inserting paste must not run when the clipboard holds an image");
|
|
6501
|
+
editor.setText("what is this?");
|
|
6502
|
+
editor.handleInput("\x1bv");
|
|
6503
|
+
await settle();
|
|
6504
|
+
const input = (text: string, source = "interactive") => handlers.get("input")?.[0]?.({ type: "input", text, source }, ctx);
|
|
6505
|
+
// Pi's real order: the editor empties its draft, then onSubmit hands the
|
|
6506
|
+
// text to session.prompt(), which runs the input handlers.
|
|
6507
|
+
const submit = () => {
|
|
6508
|
+
let sent = "";
|
|
6509
|
+
editor.onSubmit = (text) => { sent = text; };
|
|
6510
|
+
editor.handleInput("\r");
|
|
6511
|
+
return input(sent);
|
|
6512
|
+
};
|
|
6513
|
+
return { editor, ui, input, submit };
|
|
6514
|
+
}
|
|
6515
|
+
|
|
6516
|
+
test("pasted image chips are submitted as real image attachments (gentle-shell#2464)", async () => {
|
|
6517
|
+
const { editor, ui, input, submit } = await pastedChipPrompt();
|
|
6518
|
+
assert.equal(editor.getText(), "what is this? [Image#1]");
|
|
6519
|
+
const strip = (ui.widgets.get("gentle-shell-image-attachments") as (tui: unknown, theme: unknown) => { render(width: number): string[] })(fakeTui, plainTheme);
|
|
6520
|
+
assert.equal(stripAnsi(strip.render(80)[0] ?? ""), "[Image#1] png · 2×1 · 24 B");
|
|
6521
|
+
assert.deepEqual(submit(), {
|
|
6522
|
+
action: "transform",
|
|
6523
|
+
text: "what is this? [Image#1]",
|
|
6524
|
+
images: [{ type: "image", data: Buffer.from(CHIP_PNG).toString("base64"), mimeType: "image/png" }],
|
|
6525
|
+
});
|
|
6526
|
+
// Sent images are released: recalling the same text later is ordinary text.
|
|
6527
|
+
assert.equal(editor.getText(), "");
|
|
6528
|
+
assert.deepEqual(strip.render(80), []);
|
|
6529
|
+
assert.deepEqual(input("what is this? [Image#1]"), { action: "continue" });
|
|
6530
|
+
});
|
|
6531
|
+
|
|
6532
|
+
test("a pasted image chip deleted before submit is never sent (gentle-shell#2464)", async () => {
|
|
6533
|
+
const { editor, submit } = await pastedChipPrompt();
|
|
6534
|
+
editor.handleInput("\x7f");
|
|
6535
|
+
assert.equal(editor.getText(), "what is this? ");
|
|
6536
|
+
assert.deepEqual(submit(), { action: "continue" });
|
|
6537
|
+
});
|
|
6538
|
+
|
|
6539
|
+
test("pasted image chips leave non-interactive input and deleted chips untouched (gentle-shell#2464)", async () => {
|
|
6540
|
+
const { input } = await pastedChipPrompt();
|
|
6541
|
+
assert.deepEqual(input("[Image#1] from an extension", "extension"), { action: "continue" });
|
|
6542
|
+
assert.deepEqual(input("the chip was deleted"), { action: "continue" });
|
|
6543
|
+
});
|
|
6544
|
+
|
|
6545
|
+
test("pasted image chips warn instead of sending images a text-only model rejects (gentle-shell#2464)", async () => {
|
|
6546
|
+
const { ui, input } = await pastedChipPrompt({ id: "text-only", provider: "acme", input: ["text"] } as unknown as ExtensionContext["model"]);
|
|
6547
|
+
assert.deepEqual(input("what is this? [Image#1]"), {
|
|
6548
|
+
action: "transform",
|
|
6549
|
+
text: "what is this? [Image#1 omitted: the current model does not accept images]",
|
|
6550
|
+
images: [],
|
|
6551
|
+
});
|
|
6552
|
+
assert.ok(ui.notices.some((notice) => /1 pasted image was not sent: the current model does not accept images/.test(notice)));
|
|
6553
|
+
});
|
|
@@ -3,6 +3,7 @@ import test, { after, before } from "node:test";
|
|
|
3
3
|
import { convertToLlm, type ExtensionAPI, type ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
4
4
|
import type { Component, TUI } from "@earendil-works/pi-tui";
|
|
5
5
|
import gentleTodo, { todoCollapseKey, todoEnabled } from "../extensions/gentle-todo.ts";
|
|
6
|
+
import { TODO_PROGRESS_EVENT } from "../lib/shell-todo.ts";
|
|
6
7
|
import { CARD_STYLE, cardStyle, setCardStyle } from "../lib/shell-card.ts";
|
|
7
8
|
import { stripAnsi } from "../lib/terminal-theme.ts";
|
|
8
9
|
import { sidebarState } from "../lib/shell-sidebar.ts";
|
|
@@ -497,6 +498,177 @@ test("the system prompt carries only a byte-stable todo block; the current list
|
|
|
497
498
|
assert.equal(finishedEvent.systemPromptOptions.appendSystemPrompt, emptyEvent.systemPromptOptions.appendSystemPrompt, "finishing the list must not change the system prompt");
|
|
498
499
|
});
|
|
499
500
|
|
|
501
|
+
// weak-model-economy S1: small tasks skip the todo list (bench C-econ).
|
|
502
|
+
test("the todo block exempts small tasks before the tracking rules", async () => {
|
|
503
|
+
const { pi, fire } = fakePi();
|
|
504
|
+
gentleTodo(pi, {});
|
|
505
|
+
const { ctx } = fakeContext();
|
|
506
|
+
await fire("session_start", ctx);
|
|
507
|
+
const event = promptEvent();
|
|
508
|
+
await fire("before_agent_start", ctx, event);
|
|
509
|
+
assert.ok(event.systemPromptOptions.appendSystemPrompt.startsWith("## Todo list\nA small task (Task Size) uses no todo list: do it and report. For tracked or multi-step work, keep it current with the `todo` tool: mark a task in_progress before starting it,"));
|
|
510
|
+
});
|
|
511
|
+
|
|
512
|
+
/**
|
|
513
|
+
* Drives the extension the way Pi's agent loop does (#2195): every model request
|
|
514
|
+
* runs the context handlers over the persisted transcript, and a custom message
|
|
515
|
+
* sent with triggerTurn false while streaming is appended to the transcript at
|
|
516
|
+
* turn_end, after the tool results (agent-session `_flushPendingCustomMessages`).
|
|
517
|
+
*/
|
|
518
|
+
function liveSession() {
|
|
519
|
+
const base = fakePi();
|
|
520
|
+
const queued: ContextMessage[] = [];
|
|
521
|
+
const pi = {
|
|
522
|
+
...base.pi,
|
|
523
|
+
sendMessage(message: { customType: string; content: string; display: boolean }, options: { triggerTurn?: boolean }) {
|
|
524
|
+
assert.equal(options.triggerTurn, false, "todo state must never start a turn");
|
|
525
|
+
queued.push({ role: "custom", customType: message.customType, content: message.content, display: message.display, timestamp: 0 });
|
|
526
|
+
},
|
|
527
|
+
} as unknown as ExtensionAPI;
|
|
528
|
+
gentleTodo(pi, {});
|
|
529
|
+
const { ctx } = fakeContext();
|
|
530
|
+
const transcript: ContextMessage[] = [{ role: "user", content: "Work on it", timestamp: 1 }];
|
|
531
|
+
const requests: ContextMessage[][] = [];
|
|
532
|
+
let call = 0;
|
|
533
|
+
const request = async () => {
|
|
534
|
+
const result = await base.fire("context", ctx, { messages: [...transcript] }) as { messages: ContextMessage[] } | undefined;
|
|
535
|
+
const messages = result?.messages ?? [...transcript];
|
|
536
|
+
requests.push(messages);
|
|
537
|
+
return messages;
|
|
538
|
+
};
|
|
539
|
+
/** One model request whose assistant reply calls `todo` (when params are given) or `read`. */
|
|
540
|
+
const turn = async (todo?: Record<string, unknown>) => {
|
|
541
|
+
await request();
|
|
542
|
+
call += 1;
|
|
543
|
+
const id = `call-${call}`;
|
|
544
|
+
const name = todo ? "todo" : "read";
|
|
545
|
+
const assistant = { role: "assistant", content: [{ type: "toolCall", id, name, arguments: todo ?? {} }], timestamp: 0 };
|
|
546
|
+
transcript.push(assistant);
|
|
547
|
+
if (todo) await base.tools.get("todo")!.execute(id, todo, undefined, undefined, ctx);
|
|
548
|
+
transcript.push({ role: "toolResult", content: [{ type: "text", text: "ok" }], timestamp: 0 });
|
|
549
|
+
await base.fire("turn_end", ctx, { message: assistant, toolResults: [] });
|
|
550
|
+
transcript.push(...queued.splice(0));
|
|
551
|
+
};
|
|
552
|
+
const assertAppendOnly = (from: number) => {
|
|
553
|
+
for (let index = from + 1; index < requests.length; index++) {
|
|
554
|
+
const previous = requests[index - 1]!;
|
|
555
|
+
assert.deepEqual(requests[index]!.slice(0, previous.length), previous, `request ${index} must extend request ${index - 1} byte for byte`);
|
|
556
|
+
}
|
|
557
|
+
};
|
|
558
|
+
const visibleState = (messages: ContextMessage[]) => [...messages].reverse().find((message) => message.customType === "gentle-todo-state")?.content;
|
|
559
|
+
return { ctx, fire: base.fire, transcript, requests, request, turn, assertAppendOnly, visibleState };
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
test("a todo write in a running tool loop reaches the transcript before the next request, which stays append-only (#2195)", async () => {
|
|
563
|
+
const live = liveSession();
|
|
564
|
+
await live.fire("session_start", live.ctx);
|
|
565
|
+
await live.turn({ action: "write", tasks: [{ title: "Fix the bug" }, { title: "Ship it" }] });
|
|
566
|
+
await live.turn();
|
|
567
|
+
await live.turn();
|
|
568
|
+
await live.turn({ action: "update", id: 1, status: "done" });
|
|
569
|
+
await live.turn();
|
|
570
|
+
await live.request();
|
|
571
|
+
assert.match(live.visibleState(live.requests.at(-1)!) as string, /1\. \[done\] Fix the bug\n2\. \[pending\] Ship it/);
|
|
572
|
+
live.assertAppendOnly(0);
|
|
573
|
+
});
|
|
574
|
+
|
|
575
|
+
test("closing every task does not leave a stale open list in the transcript (#2195)", async () => {
|
|
576
|
+
for (const close of [{ action: "update", id: 1, status: "done" }, { action: "update", id: 1, status: "dropped" }, { action: "clear" }]) {
|
|
577
|
+
const live = liveSession();
|
|
578
|
+
await live.fire("session_start", live.ctx);
|
|
579
|
+
await live.turn({ action: "write", tasks: [{ title: "Fix the bug" }] });
|
|
580
|
+
await live.turn(close);
|
|
581
|
+
await live.turn();
|
|
582
|
+
const messages = await live.request();
|
|
583
|
+
assert.equal(live.visibleState(messages), "Current todo list: no open tasks.", `${JSON.stringify(close)}: the model must not see the closed task as still open`);
|
|
584
|
+
live.assertAppendOnly(0);
|
|
585
|
+
}
|
|
586
|
+
});
|
|
587
|
+
|
|
588
|
+
test("an open list missing from the transcript (compaction, branch switch) is persisted again once, then requests stay append-only (#2195)", async () => {
|
|
589
|
+
const live = liveSession();
|
|
590
|
+
await live.fire("session_start", live.ctx);
|
|
591
|
+
await live.turn({ action: "write", tasks: [{ title: "Fix the bug" }] });
|
|
592
|
+
// Compaction summarises the head of the conversation, including the persisted state.
|
|
593
|
+
live.transcript.splice(0, live.transcript.length, { role: "user", content: "Summary of earlier work", timestamp: 2 });
|
|
594
|
+
const healedFrom = live.requests.length;
|
|
595
|
+
await live.turn();
|
|
596
|
+
await live.turn();
|
|
597
|
+
await live.turn();
|
|
598
|
+
await live.request();
|
|
599
|
+
for (const messages of live.requests.slice(healedFrom)) assert.match(String(live.visibleState(messages)), /\[pending\] Fix the bug/);
|
|
600
|
+
assert.equal(live.transcript.filter((message) => message.customType === "gentle-todo-state").length, 1, "the missing state is persisted exactly once");
|
|
601
|
+
live.assertAppendOnly(healedFrom + 1);
|
|
602
|
+
});
|
|
603
|
+
|
|
604
|
+
test("consecutive context requests with an unchanged Todo list preserve prefix stability (#2195)", async () => {
|
|
605
|
+
const { pi, tools, fire } = fakePi();
|
|
606
|
+
gentleTodo(pi, {});
|
|
607
|
+
const { ctx } = fakeContext();
|
|
608
|
+
await fire("session_start", ctx);
|
|
609
|
+
await tools.get("todo")!.execute("c1", { action: "write", tasks: [{ title: "Fix the bug" }] }, undefined, undefined, ctx);
|
|
610
|
+
|
|
611
|
+
// Request 1: first model request
|
|
612
|
+
const userMsg = { role: "user", content: "Work on it", timestamp: 100 };
|
|
613
|
+
const req1 = await fire("context", ctx, { messages: [userMsg] }) as { messages: Array<{ role: string; customType?: string }> };
|
|
614
|
+
assert.ok(req1, "req1 must return context messages");
|
|
615
|
+
assert.ok(req1.messages.some((m) => (m as { customType?: string }).customType === "gentle-todo-state"), "req1 must include gentle-todo-state");
|
|
616
|
+
|
|
617
|
+
// Model executes a tool (read), producing assistant message and tool result
|
|
618
|
+
const assistantMsg = { role: "assistant", content: [{ type: "toolCall", id: "read-1", name: "read", arguments: {} }], timestamp: 200 };
|
|
619
|
+
const toolResultMsg = { role: "toolResult", toolCallId: "read-1", content: [{ type: "text", text: "file content" }], timestamp: 300 };
|
|
620
|
+
|
|
621
|
+
// Request 2: next model request with unchanged todo list.
|
|
622
|
+
// The input messages to context include the transcript with the already-delivered state message
|
|
623
|
+
const inputReq2 = [...req1.messages, assistantMsg, toolResultMsg];
|
|
624
|
+
const req2 = await fire("context", ctx, { messages: inputReq2 }) as { messages: Array<{ role: string; customType?: string }> } | undefined;
|
|
625
|
+
|
|
626
|
+
// req2 must not append a redundant trailing gentle-todo-state that would invalidate prefix caching
|
|
627
|
+
const effectiveReq2 = req2?.messages ?? inputReq2;
|
|
628
|
+
assert.deepEqual(
|
|
629
|
+
effectiveReq2.slice(0, req1.messages.length),
|
|
630
|
+
req1.messages,
|
|
631
|
+
"req1 must be an exact prefix of req2 to preserve prompt caching",
|
|
632
|
+
);
|
|
633
|
+
// No duplicate trailing state message at the end of req2
|
|
634
|
+
const stateMessages = effectiveReq2.filter((m) => (m as { customType?: string }).customType === "gentle-todo-state");
|
|
635
|
+
assert.equal(stateMessages.length, 1, "must not append duplicate gentle-todo-state when unchanged");
|
|
636
|
+
});
|
|
637
|
+
|
|
638
|
+
test("todo write and update persist gentle-todo-state custom message to transcript (#2195)", async () => {
|
|
639
|
+
const sent: Array<{ message: unknown; options: unknown }> = [];
|
|
640
|
+
const base = fakePi();
|
|
641
|
+
const pi = {
|
|
642
|
+
...base.pi,
|
|
643
|
+
sendMessage(message: unknown, options: unknown) {
|
|
644
|
+
sent.push({ message, options });
|
|
645
|
+
},
|
|
646
|
+
} as unknown as ExtensionAPI;
|
|
647
|
+
gentleTodo(pi, {});
|
|
648
|
+
const { ctx } = fakeContext();
|
|
649
|
+
await base.fire("session_start", ctx);
|
|
650
|
+
|
|
651
|
+
// 1. todo write persists state
|
|
652
|
+
await base.tools.get("todo")!.execute("c1", { action: "write", tasks: [{ title: "Task 1" }] }, undefined, undefined, ctx);
|
|
653
|
+
assert.equal(sent.length, 1);
|
|
654
|
+
assert.deepEqual(sent[0]!.options, { triggerTurn: false });
|
|
655
|
+
const firstMsg = sent[0]!.message as { role: string; customType: string; content: string; display: boolean };
|
|
656
|
+
assert.equal(firstMsg.customType, "gentle-todo-state");
|
|
657
|
+
assert.equal(firstMsg.display, false);
|
|
658
|
+
assert.match(firstMsg.content, /1\. \[pending\] Task 1/);
|
|
659
|
+
|
|
660
|
+
// 2. todo update persists new state
|
|
661
|
+
await base.tools.get("todo")!.execute("c2", { action: "update", id: 1, status: "in_progress", note: "doing" }, undefined, undefined, ctx);
|
|
662
|
+
assert.equal(sent.length, 2);
|
|
663
|
+
assert.deepEqual(sent[1]!.options, { triggerTurn: false });
|
|
664
|
+
const secondMsg = sent[1]!.message as { content: string };
|
|
665
|
+
assert.match(secondMsg.content, /1\. \[in_progress\] Task 1 — doing/);
|
|
666
|
+
|
|
667
|
+
// 3. todo list does NOT persist extra messages
|
|
668
|
+
await base.tools.get("todo")!.execute("c3", { action: "list" }, undefined, undefined, ctx);
|
|
669
|
+
assert.equal(sent.length, 2, "read actions must not emit state messages");
|
|
670
|
+
});
|
|
671
|
+
|
|
500
672
|
test("before_agent_start is idempotent: a todo block already present in appendSystemPrompt is not duplicated", async () => {
|
|
501
673
|
const { pi, tools, fire } = fakePi();
|
|
502
674
|
gentleTodo(pi, {});
|
|
@@ -612,3 +784,21 @@ test("a list left with only dropped tasks open finishes and clears; a blocked-on
|
|
|
612
784
|
}
|
|
613
785
|
assert.match(widget()![2], /⊘ Deploy · waiting for an admin/);
|
|
614
786
|
});
|
|
787
|
+
|
|
788
|
+
// Harness roadmap T6: the todo list is the session's progress, so the runtime
|
|
789
|
+
// publishes it for peers instead of asking the model to call orchestrator_session_id.
|
|
790
|
+
test("a todo write emits the session's progress; list and errors emit nothing", async () => {
|
|
791
|
+
const { pi, tools } = fakePi();
|
|
792
|
+
const emitted: Array<{ name: string; data: unknown }> = [];
|
|
793
|
+
Object.assign(pi, { events: { emit: (name: string, data: unknown) => { emitted.push({ name, data }); }, on: () => () => {} } });
|
|
794
|
+
gentleTodo(pi, {});
|
|
795
|
+
const { ctx } = fakeContext();
|
|
796
|
+
const todo = (params: unknown) => tools.get("todo")!.execute("call", params, undefined, undefined, ctx);
|
|
797
|
+
await todo({ action: "write", tasks: [{ title: "Write parser", status: "done" }, { title: "Wire CLI", status: "in_progress" }, { title: "Docs" }] });
|
|
798
|
+
assert.deepEqual(emitted, [{ name: TODO_PROGRESS_EVENT, data: { sessionId: "s1", done: 1, total: 3, current: "Wire CLI" } }]);
|
|
799
|
+
await todo({ action: "list" });
|
|
800
|
+
await todo({ action: "update", id: 99, status: "done" });
|
|
801
|
+
assert.equal(emitted.length, 1, "list and a failed update publish nothing");
|
|
802
|
+
await todo({ action: "update", id: 2, status: "done" });
|
|
803
|
+
assert.deepEqual(emitted.at(-1)?.data, { sessionId: "s1", done: 2, total: 3, current: null });
|
|
804
|
+
});
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import { __testing } from "../extensions/gentle-ai.ts";
|
|
4
|
+
import { INTERACTIVE_HOST_ENV } from "../lib/rpc-host.ts";
|
|
5
|
+
|
|
6
|
+
// Bench gate-p53-r1-BS40 x6 run 3 (headless RPC): the ask-on-risk delivery
|
|
7
|
+
// question ran before any code was written; ask_user_question answered
|
|
8
|
+
// "unavailable outside the interactive TUI", the orchestrator printed the menu
|
|
9
|
+
// and stopped, and the round ended with "No code changed yet". When no human
|
|
10
|
+
// can answer, the delivery question resolves to the recommended chain instead.
|
|
11
|
+
|
|
12
|
+
const HEADLESS_DELIVERY = "No human can answer questions in this session";
|
|
13
|
+
|
|
14
|
+
function promptFor(mode: string | undefined): string {
|
|
15
|
+
return __testing.buildGentlePrompt("neutral", process.cwd(), undefined, undefined, mode);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function withInteractiveHost<T>(value: string | undefined, run: () => T): T {
|
|
19
|
+
const previous = process.env[INTERACTIVE_HOST_ENV];
|
|
20
|
+
if (value === undefined) delete process.env[INTERACTIVE_HOST_ENV];
|
|
21
|
+
else process.env[INTERACTIVE_HOST_ENV] = value;
|
|
22
|
+
try {
|
|
23
|
+
return run();
|
|
24
|
+
} finally {
|
|
25
|
+
if (previous === undefined) delete process.env[INTERACTIVE_HOST_ENV];
|
|
26
|
+
else process.env[INTERACTIVE_HOST_ENV] = previous;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
test("headless host modes resolve the ask-on-risk delivery question to the feature-branch chain without asking", () => {
|
|
31
|
+
withInteractiveHost(undefined, () => {
|
|
32
|
+
for (const mode of ["rpc", "print", "json"]) {
|
|
33
|
+
const prompt = promptFor(mode);
|
|
34
|
+
assert.ok(prompt.includes(HEADLESS_DELIVERY), `${mode} must say no human can answer`);
|
|
35
|
+
assert.match(prompt, /ask-on-risk/, `${mode} must name the ask-on-risk delivery question`);
|
|
36
|
+
assert.match(prompt, /`feature-branch-chain`/, `${mode} must name the chain it resolves to`);
|
|
37
|
+
assert.match(prompt, /feature document/, `${mode} must record the choice in the feature document`);
|
|
38
|
+
assert.match(prompt, /consent/, `${mode} must keep consent prompts out of the default`);
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test("interactive hosts keep asking the delivery question", () => {
|
|
44
|
+
withInteractiveHost(undefined, () => {
|
|
45
|
+
for (const mode of ["tui", undefined]) assert.ok(!promptFor(mode).includes(HEADLESS_DELIVERY), `${String(mode)} keeps the question`);
|
|
46
|
+
});
|
|
47
|
+
withInteractiveHost("1", () => {
|
|
48
|
+
assert.ok(!promptFor("rpc").includes(HEADLESS_DELIVERY), "an interactive RPC host keeps the question");
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test("the reference documents the headless delivery default", async () => {
|
|
53
|
+
const { readFileSync } = await import("node:fs");
|
|
54
|
+
const reference = readFileSync(new URL("../docs/readme-reference.md", import.meta.url), "utf8");
|
|
55
|
+
for (const term of ["no human can answer questions", "`GENTLE_SHELL_INTERACTIVE_HOST=1`", "`ask-on-risk` does not ask", "`feature-branch-chain`"]) {
|
|
56
|
+
assert.ok(reference.includes(term), `docs/readme-reference.md is missing ${term}`);
|
|
57
|
+
}
|
|
58
|
+
});
|