gentle-pi 3.7.0 → 4.1.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 +155 -10
- package/assets/agents/gentle-ai-explore.md +13 -6
- package/assets/agents/gentle-ai-verify.md +27 -6
- package/assets/agents/gentle-ai-worker.md +13 -15
- package/assets/install-wizard/index.html +47 -0
- package/assets/install-wizard/wizard.css +1279 -0
- package/assets/install-wizard/wizard.js +951 -0
- package/assets/orchestrator-delegation.md +75 -169
- package/assets/orchestrator-memory.md +12 -26
- package/assets/orchestrator-prompts.md +48 -0
- package/assets/orchestrator-skills.md +1 -1
- package/assets/orchestrator-tracking.md +53 -0
- package/assets/orchestrator-verification.md +28 -0
- package/assets/orchestrator-writer.md +38 -0
- package/assets/orchestrator.md +30 -38
- package/assets/sounds/LICENSE.md +52 -0
- package/assets/sounds/attention.wav +0 -0
- package/assets/sounds/error.wav +0 -0
- package/assets/sounds/success.wav +0 -0
- package/assets/support/strict-tdd-verify.md +4 -266
- package/assets/support/strict-tdd.md +8 -360
- package/bin/engram-guard +9 -0
- package/bin/gentle-shell-install.mjs +302 -0
- package/bin/gentle-shell.mjs +956 -50
- package/contracts/review-provider-contract-mirror/provider-contract.lock.json +7 -7
- package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/manifest.json +2 -2
- package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/orchestration/pi.md +16 -4
- package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/generated/provider-capabilities.baseline.json +2 -2
- package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/generated/provider-roles.baseline.json +1 -1
- package/docs/bundled-install.md +397 -0
- package/docs/delegated-verification.md +27 -4
- package/docs/gentle-agents-activity.md +537 -0
- package/docs/gentle-shell-uninstall.md +136 -0
- package/docs/gentle-shell.md +165 -51
- package/docs/install-wizard.md +2040 -0
- package/docs/native-authority-architecture.md +3 -3
- package/docs/permission-modes.md +82 -0
- package/docs/prompt-history.md +280 -0
- package/docs/readme-reference.md +364 -231
- package/docs/render-cache-performance.md +28 -0
- package/docs/sound-notifications-proposal.md +191 -0
- package/docs/sound-notifications.md +114 -0
- package/docs/telemetry.md +1 -1
- package/docs/yolo-mode.md +86 -0
- package/extensions/child-context.ts +35 -0
- package/extensions/child-safety.ts +25 -0
- package/extensions/codegraph-tools.ts +18 -5
- package/extensions/gentle-agents.ts +1093 -326
- package/extensions/gentle-ai.ts +1636 -890
- package/extensions/gentle-herdr-activity.ts +81 -0
- package/extensions/gentle-notifications.ts +104 -0
- package/extensions/gentle-shell.ts +1619 -136
- package/extensions/gentle-stats.ts +101 -0
- package/extensions/gentle-todo.ts +126 -20
- package/extensions/history/atomic-write.ts +38 -0
- package/extensions/history/hide-prompts.ts +183 -0
- package/extensions/history/index.ts +1424 -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/pi-pretty.ts +39 -3
- package/extensions/quiet-tools.ts +254 -92
- package/extensions/resume-hint.ts +60 -0
- package/extensions/skill-registry.ts +191 -58
- package/extensions/startup-banner.ts +71 -33
- package/lib/agent-assets.ts +604 -0
- package/lib/agent-home.ts +10 -1
- package/lib/agent-model-resolution.ts +80 -0
- package/lib/agent-profile-pin.ts +12 -0
- package/lib/agent-profiles.ts +13 -3
- package/lib/agents-completion-delivery.ts +11 -3
- package/lib/agents-message-delivery.ts +181 -0
- package/lib/agents-messaging.ts +1 -1
- package/lib/agents-protocol.ts +99 -0
- package/lib/agents-runner.ts +180 -107
- package/lib/agents-view.ts +26 -3
- package/lib/agents-widget.ts +16 -9
- package/lib/append-system-prompt.ts +21 -0
- package/lib/auto-permission-classifier.ts +280 -0
- package/lib/auto-permission-runtime.ts +268 -0
- package/lib/background-jobs-tools.ts +202 -0
- package/lib/background-jobs.ts +253 -0
- package/lib/background-monitor.ts +154 -0
- package/lib/bounded-writer-admission.ts +220 -0
- package/lib/builtin-codemode-optout.ts +166 -0
- package/lib/card-content-policy.ts +85 -0
- package/lib/card-style-policy.ts +60 -0
- package/lib/child-context-files.ts +166 -0
- package/lib/child-package-injection.ts +62 -0
- package/lib/codemode-renderer.ts +195 -0
- package/lib/command-palette-catalog.ts +5 -10
- package/lib/command-palette.ts +25 -14
- package/lib/context-threshold.ts +26 -0
- package/lib/destructive-command-guard.ts +144 -0
- package/lib/gentle-ai-binary.ts +109 -4
- package/lib/gentle-ai-elapsed-store.ts +87 -0
- package/lib/gentle-ai-renderer.ts +196 -39
- package/lib/gentle-shell-bundled.ts +517 -0
- package/lib/gentle-shell-launcher.ts +89 -27
- package/lib/gentle-shell-resume-hint.ts +176 -0
- package/lib/gentle-shell-startup-flow.ts +54 -0
- package/lib/gentle-shell-startup-update.ts +92 -0
- package/lib/gentle-shell-uninstall.ts +840 -0
- package/lib/gentle-shell-update-panel.ts +231 -0
- package/lib/herdr-activity.ts +183 -0
- package/lib/history-capture-policy.ts +95 -0
- package/lib/inprocess-reviewer.ts +3 -0
- package/lib/jobs-sidebar-state.ts +20 -0
- package/lib/jobs-view.ts +161 -0
- package/lib/model-price-ratio.ts +83 -0
- package/lib/model-routing-authority.ts +5 -1
- package/lib/nan-provider.ts +249 -0
- package/lib/native-review-cli.ts +242 -180
- package/lib/notification-audio-native.ts +162 -0
- package/lib/notification-audio-windows.ts +399 -0
- package/lib/notification-audio.ts +332 -0
- package/lib/notification-customize.ts +241 -0
- package/lib/notification-events.ts +113 -0
- package/lib/notification-policy.ts +148 -0
- package/lib/notification-pulse-client.ts +367 -0
- package/lib/notification-pulse-protocol.ts +305 -0
- package/lib/notification-pulse-stream.ts +213 -0
- package/lib/notification-pulse-worker.ts +66 -0
- package/lib/notification-scheduler.ts +173 -0
- package/lib/notification-service.ts +202 -0
- package/lib/notification-ui.ts +87 -0
- package/lib/odd-phase-inference.ts +231 -0
- package/lib/odd-phase.ts +141 -0
- package/lib/orchestrator-alias.ts +20 -0
- package/lib/orchestrator-catalog.ts +79 -0
- package/lib/orchestrator-consultation.ts +61 -0
- package/lib/orchestrator-discovery.ts +49 -0
- package/lib/orchestrator-helper-consent.ts +122 -0
- package/lib/orchestrator-helper.ts +167 -0
- package/lib/orchestrator-message-card.ts +105 -0
- package/lib/orchestrator-presence.ts +115 -14
- package/lib/orchestrator-scope.ts +80 -0
- package/lib/orchestrator-state.ts +107 -0
- package/lib/orchestrator-tool-card.ts +280 -0
- package/lib/orchestrator-work-search.ts +218 -0
- package/lib/orchestrator-work.ts +79 -0
- package/lib/overlay-repaint.ts +26 -0
- package/lib/permission-mode-policy.ts +59 -0
- package/lib/pi-tui-keys.ts +53 -0
- package/lib/review-candidate-view-owner.ts +73 -21
- package/lib/review-candidate-view.ts +115 -27
- package/lib/review-compact-contract.ts +2 -2
- package/lib/review-host-relay.ts +7 -2
- package/lib/review-integration-v2.ts +100 -22
- package/lib/review-publication-gate.ts +1 -1
- package/lib/review-reminder-receipt.ts +48 -8
- package/lib/review-risk-assessment.ts +227 -56
- package/lib/review-sidebar-state.ts +224 -0
- package/lib/review-transaction.ts +72 -3
- package/lib/runtime-metrics-children.ts +1 -1
- package/lib/runtime-metrics-native.ts +3 -2
- package/lib/selection-engine.ts +527 -0
- package/lib/session-change-capture.ts +5 -4
- package/lib/session-changes.ts +27 -22
- package/lib/session-messaging-grants.ts +135 -0
- package/lib/session-profile-binding.ts +83 -0
- package/lib/session-profile-freeze.ts +266 -0
- package/lib/session-worktree-registry.ts +87 -9
- package/lib/shell-bar.ts +215 -30
- package/lib/shell-card.ts +287 -18
- package/lib/shell-changes-view.ts +42 -14
- package/lib/shell-changes.ts +86 -71
- package/lib/shell-prompt.ts +102 -6
- package/lib/shell-sidebar-layout.ts +83 -15
- package/lib/shell-sidebar.ts +15 -1
- package/lib/shell-todo.ts +84 -31
- package/lib/shell-usage-view.ts +9 -4
- package/lib/shell-usage.ts +70 -13
- package/lib/stats-collector.ts +381 -0
- package/lib/stats-view.ts +431 -0
- package/lib/status-timing-diagnostics.ts +201 -0
- package/lib/telemetry-trigger.ts +3 -1
- package/lib/theme-customization.ts +52 -0
- package/lib/vim-editor-adapter.ts +383 -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 +119 -0
- package/lib/visual-customize-view.ts +481 -0
- package/lib/visual-profiles.ts +228 -0
- package/lib/writer-surfaces.ts +162 -0
- package/lib/yolo-session-policy.ts +317 -0
- package/package.json +20 -8
- package/runtime/child-package-injection.mjs +63 -0
- package/runtime/gentle-ai-binary.mjs +109 -4
- package/runtime/gentle-shell-bundled.mjs +518 -0
- package/runtime/gentle-shell-launcher.mjs +87 -25
- package/runtime/gentle-shell-resume-hint.mjs +177 -0
- package/runtime/gentle-shell-startup-flow.mjs +55 -0
- package/runtime/gentle-shell-startup-update.mjs +93 -0
- package/runtime/gentle-shell-uninstall.mjs +841 -0
- package/runtime/gentle-shell-update-panel.mjs +232 -0
- package/runtime/native-review-cli.mjs +239 -177
- package/runtime/review-integration-v2.mjs +100 -22
- package/runtime/review-risk-assessment.mjs +225 -54
- package/runtime/status-timing-diagnostics.mjs +202 -0
- package/runtime/telemetry-trigger.mjs +3 -1
- package/scripts/bootstrap.cmd +255 -0
- package/scripts/bootstrap.sh +168 -0
- package/scripts/build-distribution.mjs +157 -0
- package/scripts/build-installer-bundles.mjs +136 -0
- package/scripts/build-runtime-modules.mjs +8 -0
- package/scripts/bundled-install.mjs +1308 -0
- package/scripts/bundled-wizard.mjs +399 -0
- package/scripts/check-types.mjs +4 -6
- package/scripts/gentle-ai-installer.mjs +83 -35
- package/scripts/install-gentle-ai.mjs +48 -1
- package/scripts/install-tui-mode-setting.mjs +10 -1
- package/scripts/install-wizard-preview.mjs +154 -0
- package/scripts/installer-downloads.mjs +648 -0
- package/scripts/installer-preflight.mjs +387 -0
- package/scripts/installer-probes.mjs +564 -0
- package/scripts/installer-runner.mjs +1102 -0
- package/scripts/installer-server.mjs +881 -0
- package/scripts/installer-windows-artifacts.json +15 -0
- package/scripts/installer-windows.mjs +752 -0
- package/scripts/main-channel.mjs +452 -0
- package/scripts/mirror-odd-routing.mjs +2 -2
- package/scripts/npm-pack-result.mjs +31 -0
- package/scripts/render-cache-performance-worker.mjs +205 -0
- package/scripts/render-cache-performance.mjs +115 -0
- package/scripts/run-test-suite.mjs +90 -0
- package/scripts/test-installer-acceptance.sh +161 -0
- package/scripts/test-packed-runner.mjs +54 -19
- package/scripts/types-baseline.json +2 -92
- package/scripts/verify-package-files.mjs +91 -38
- 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 +16 -21
- 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-home.test.ts +56 -3
- package/tests/agent-model-resolution.test.ts +113 -0
- package/tests/agent-profiles.test.ts +30 -0
- package/tests/agent-risk-escalation.test.ts +123 -0
- package/tests/agents-completion-delivery.test.ts +17 -0
- package/tests/agents-fake-child.ts +2 -2
- package/tests/agents-message-delivery.test.ts +106 -0
- package/tests/agents-protocol.test.ts +97 -0
- package/tests/agents-queries.test.ts +9 -9
- package/tests/agents-runner-process.test.ts +31 -0
- package/tests/agents-runner.test.ts +467 -93
- 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/auto-mode-invariants.test.ts +258 -0
- package/tests/auto-mode-sdk.test.ts +57 -0
- package/tests/auto-mode.test.ts +49 -0
- package/tests/auto-permission-classifier.test.ts +283 -0
- package/tests/auto-permission-runtime.test.ts +196 -0
- package/tests/autonomous-guard.test.ts +69 -1
- package/tests/background-cache-warming.test.ts +2 -1
- package/tests/background-jobs.test.ts +227 -0
- package/tests/background-monitor.test.ts +195 -0
- package/tests/background-subagents-default-mode.test.ts +28 -5
- package/tests/bounded-stop-contract.test.ts +85 -0
- package/tests/bounded-writer-admission.test.ts +146 -0
- package/tests/branch-pr-skill.test.ts +43 -0
- package/tests/bridge-wake-lifecycle.test.ts +129 -0
- package/tests/bridge-wake-rendering.test.ts +56 -0
- package/tests/build-distribution.test.ts +269 -0
- package/tests/builtin-codemode-optout.test.ts +242 -0
- package/tests/bundled-install.test.ts +1319 -0
- package/tests/bundled-wizard.test.ts +505 -0
- package/tests/card-content-policy.test.ts +68 -0
- package/tests/card-style-policy.test.ts +87 -0
- package/tests/chained-pr-skill.test.ts +124 -0
- package/tests/child-codegraph-bootstrap.test.ts +74 -0
- package/tests/child-context-files.test.ts +255 -0
- package/tests/child-package-entrypoints.test.ts +65 -0
- package/tests/child-package-injection.test.ts +74 -0
- package/tests/child-safety.test.ts +94 -0
- package/tests/child-web-package.test.ts +224 -0
- package/tests/codegraph-tools.test.ts +35 -0
- package/tests/codemode-rendering.test.ts +596 -0
- package/tests/command-palette.test.ts +43 -7
- package/tests/context-threshold.test.ts +31 -0
- package/tests/delegate-for-reason-contract.test.ts +327 -0
- package/tests/delegated-key-learnings-contract.test.ts +3 -78
- package/tests/destructive-command-guard.test.ts +84 -0
- package/tests/devbinary/native-review-parity.devtest.ts +171 -3
- package/tests/devbinary/non-git-subagent-bootstrap.devtest.ts +193 -0
- package/tests/devbinary/pi-host-relay.devtest.ts +173 -3
- package/tests/engram-safety.test.ts +288 -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 +69 -0
- package/tests/gentle-agents.test.ts +2804 -346
- package/tests/gentle-ai-binary-isolation.test.ts +135 -0
- package/tests/gentle-ai-binary.test.ts +21 -4
- package/tests/gentle-ai-child-guards.test.ts +202 -0
- package/tests/gentle-ai-dev-binary-surfacing.test.ts +125 -15
- package/tests/gentle-ai-elapsed-store.test.ts +68 -0
- package/tests/gentle-ai-installer.test.ts +566 -62
- package/tests/gentle-ai-prompt-model-routing.test.ts +123 -0
- package/tests/gentle-ai-renderer.test.ts +493 -14
- package/tests/gentle-ai.test.ts +1427 -236
- package/tests/gentle-card-text.ts +2 -1
- package/tests/gentle-herdr-activity.test.ts +100 -0
- package/tests/gentle-notifications.test.ts +427 -0
- package/tests/gentle-shell-bin.test.ts +896 -115
- package/tests/gentle-shell-bundled.test.ts +1002 -0
- package/tests/gentle-shell-launcher.test.ts +333 -65
- package/tests/gentle-shell-resume-hint.test.ts +270 -0
- package/tests/gentle-shell-startup-flow.test.ts +75 -0
- package/tests/gentle-shell-startup-update.test.ts +85 -0
- package/tests/gentle-shell-uninstall.test.ts +978 -0
- package/tests/gentle-shell-update-panel.test.ts +158 -0
- package/tests/gentle-shell.test.ts +4216 -119
- package/tests/gentle-stats.test.ts +152 -0
- package/tests/gentle-theme.test.ts +4 -1
- package/tests/gentle-todo.test.ts +333 -10
- package/tests/herdr-activity.test.ts +243 -0
- package/tests/history-agent-home.test.ts +81 -0
- package/tests/history-atomic-write.test.ts +57 -0
- package/tests/history-capture-policy.test.ts +102 -0
- package/tests/history-child-guard.test.ts +70 -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 +49 -19
- package/tests/install-gentle-ai-failure.test.ts +69 -0
- package/tests/install-tui-mode-setting.test.ts +15 -0
- package/tests/install-wizard.test.ts +1223 -0
- package/tests/installer-bundles.test.ts +122 -0
- package/tests/installer-posix-bootstrap.test.ts +1201 -0
- package/tests/installer-preflight.test.ts +750 -0
- package/tests/installer-probes.test.ts +1106 -0
- package/tests/installer-runner.test.ts +2158 -0
- package/tests/installer-server.test.ts +1567 -0
- package/tests/installer-windows-bootstrap.test.ts +2410 -0
- package/tests/issue-creation-skill.test.ts +61 -0
- package/tests/jobs-view.test.ts +160 -0
- package/tests/main-channel-upgrade.test.ts +389 -0
- package/tests/main-channel.test.ts +344 -0
- package/tests/maintainer/provider-relay.maintest.ts +13 -12
- package/tests/model-price-ratio.test.ts +116 -0
- package/tests/model-profiles-preservation.test.ts +153 -0
- package/tests/model-routing-authority.test.ts +16 -0
- package/tests/model-routing-package.test.ts +10 -0
- package/tests/nan-provider-runtime.test.ts +156 -0
- package/tests/nan-provider.test.ts +481 -0
- package/tests/native-binary-gate.test.ts +2 -0
- package/tests/native-review-capability-contract.test.ts +13 -1
- package/tests/native-review-cli.test.ts +239 -118
- package/tests/native-review-consent.test.ts +2 -2
- package/tests/native-review-parity-runtime.test.ts +101 -4
- package/tests/native-review-parity.test.ts +12 -48
- package/tests/native-risk-evidence-parity.test.ts +21 -0
- package/tests/no-self-pause-contract.test.ts +87 -0
- package/tests/notification-audio-native.test.ts +281 -0
- package/tests/notification-audio-windows.test.ts +581 -0
- package/tests/notification-audio.test.ts +548 -0
- package/tests/notification-customize.test.ts +655 -0
- package/tests/notification-events.test.ts +115 -0
- package/tests/notification-policy.test.ts +201 -0
- package/tests/notification-pulse-client.test.ts +356 -0
- package/tests/notification-pulse-protocol.test.ts +213 -0
- package/tests/notification-pulse-stream.test.ts +320 -0
- package/tests/notification-scheduler.test.ts +258 -0
- package/tests/notification-ui.test.ts +157 -0
- package/tests/npm-pack-result.test.ts +41 -0
- package/tests/odd-integration.test.ts +76 -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 +60 -14
- package/tests/odd-routing-contract.test.ts +158 -50
- package/tests/orchestrator-budget.test.ts +43 -45
- package/tests/orchestrator-catalog.test.ts +146 -0
- package/tests/orchestrator-consultation-sdk.test.ts +614 -0
- package/tests/orchestrator-consultation.test.ts +111 -0
- package/tests/orchestrator-discovery.test.ts +241 -0
- package/tests/orchestrator-helper-consent.test.ts +171 -0
- package/tests/orchestrator-helper.test.ts +297 -0
- package/tests/orchestrator-message-card.test.ts +190 -0
- package/tests/orchestrator-presence.test.ts +23 -0
- package/tests/orchestrator-rdd-ownership.test.ts +8 -7
- package/tests/orchestrator-scope.test.ts +107 -0
- package/tests/orchestrator-state.test.ts +114 -0
- package/tests/orchestrator-status-publication-contract.test.ts +34 -0
- package/tests/orchestrator-tool-card.test.ts +398 -0
- package/tests/orchestrator-work-search.test.ts +318 -0
- package/tests/orchestrator-work.test.ts +147 -0
- package/tests/overlay-repaint.test.ts +74 -0
- package/tests/package-manifest.test.ts +344 -123
- package/tests/packed-runner-owned-path.test.ts +80 -0
- package/tests/parallel-reason-contract.test.ts +21 -0
- package/tests/parallel-writer-admission.test.ts +210 -0
- package/tests/permission-mode-policy.test.ts +194 -0
- package/tests/persona-single-channel.test.ts +10 -18
- package/tests/pi-pretty.test.ts +56 -0
- package/tests/provider-defect-handoff.test.ts +7 -14
- package/tests/quiet-bash-runtime.test.ts +76 -0
- package/tests/quiet-tool-rendering.test.ts +569 -187
- package/tests/rdd-aware-verification-contract.test.ts +103 -40
- package/tests/rdd-status-line.test.ts +9 -4
- package/tests/render-cache-performance.test.ts +48 -0
- package/tests/resume-hint-extension.test.ts +122 -0
- package/tests/review-agent-end-preflight.test.ts +237 -21
- package/tests/review-candidate-owner-retry.test.ts +22 -1
- package/tests/review-candidate-view.test.ts +399 -12
- package/tests/review-contract-prompt.test.ts +138 -44
- package/tests/review-controller-lock-status.test.ts +0 -1
- package/tests/review-controller-native-recovery.test.ts +327 -17
- package/tests/review-controller-native-routing.test.ts +866 -6
- package/tests/review-controller-workspace-root.test.ts +206 -6
- package/tests/review-controller.test.ts +2 -2
- package/tests/review-correction-lifecycle.test.ts +2 -1
- package/tests/review-gate.test.ts +7 -4
- package/tests/review-graph-schema.test.ts +2 -2
- package/tests/review-host-relay-routing.test.ts +342 -4
- package/tests/review-host-relay.test.ts +49 -2
- package/tests/review-integration-v2-forward.test.ts +221 -1
- package/tests/review-json-arguments.test.ts +167 -0
- package/tests/review-last-event-closure.test.ts +157 -8
- package/tests/review-ledger-contract.test.ts +14 -37
- package/tests/review-policy-judgment-day.test.ts +1 -1
- package/tests/review-policy-ordinary.test.ts +2 -2
- package/tests/review-relay-transport-agent.test.ts +66 -1
- package/tests/review-reminder-receipt.test.ts +47 -1
- package/tests/review-risk-assessment.test.ts +594 -26
- package/tests/review-session-standing-permission-controller.test.ts +16 -17
- package/tests/review-session-standing-permission-ipc.test.ts +6 -5
- package/tests/review-sidebar-state.test.ts +402 -0
- package/tests/review-test-fixtures.ts +7 -1
- package/tests/review-transaction.test.ts +18 -1
- package/tests/run-test-suite.test.ts +202 -0
- package/tests/runtime-harness.mjs +186 -800
- package/tests/runtime-metrics-children.test.ts +16 -23
- package/tests/runtime-metrics-extension.test.ts +1 -1
- package/tests/runtime-metrics-native.test.ts +31 -1
- package/tests/selection-engine.test.ts +451 -0
- package/tests/session-change-capture.test.ts +10 -10
- package/tests/session-changes-shell.test.ts +242 -48
- package/tests/session-changes.test.ts +17 -24
- package/tests/session-messaging-grants.test.ts +255 -0
- package/tests/session-profile-binding.test.ts +163 -0
- package/tests/session-profile-freeze.test.ts +381 -0
- package/tests/session-subject-routing-contract.test.ts +34 -0
- package/tests/session-worktree-registry.test.ts +134 -2
- package/tests/shell-bar.test.ts +422 -1
- package/tests/shell-card.test.ts +353 -1
- package/tests/shell-changes-view.test.ts +135 -32
- package/tests/shell-changes.test.ts +61 -62
- package/tests/shell-prompt.test.ts +94 -2
- package/tests/shell-render-cache-runtime.test.ts +69 -0
- package/tests/shell-render-cache.test.ts +129 -0
- package/tests/shell-sidebar-layout.test.ts +351 -22
- package/tests/shell-sidebar-scroll-benchmark.test.ts +255 -0
- package/tests/shell-todo.test.ts +207 -3
- package/tests/shell-usage-view.test.ts +28 -1
- package/tests/shell-usage.test.ts +108 -0
- package/tests/short-check-routing-contract.test.ts +49 -0
- package/tests/skill-registry-watcher.test.ts +119 -0
- package/tests/skill-registry.test.ts +357 -2
- package/tests/startup-banner.test.ts +246 -4
- package/tests/startup-home-repo.test.ts +170 -0
- package/tests/stats-collector.test.ts +195 -0
- package/tests/stats-view.test.ts +202 -0
- package/tests/status-timing-diagnostics.test.ts +283 -0
- package/tests/support/orchestrator-modules.ts +19 -0
- package/tests/task-size-routing-contract.test.ts +241 -0
- package/tests/telemetry-trigger.test.ts +107 -20
- package/tests/test-strength-contract.test.ts +124 -0
- package/tests/theme-customization.test.ts +72 -0
- package/tests/verify-agent-contract.test.ts +90 -0
- package/tests/verify-first-launch-contract.test.ts +139 -0
- package/tests/verify-package-files.test.ts +56 -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 +139 -0
- package/tests/visual-customize-view.test.ts +696 -0
- package/tests/visual-profiles.test.ts +87 -0
- package/tests/writer-edit-surface-scope.test.ts +59 -2
- package/tests/writer-surfaces.test.ts +199 -0
- package/tests/yolo-customize.test.ts +271 -0
- package/tests/yolo-mode-runtime.test.ts +188 -0
- package/tests/yolo-mode.test.ts +289 -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
- /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/README.md +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/schemas/lens.schema.json +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/schemas/refuter.schema.json +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/schemas/targeted-validator.schema.json +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/vectors/lens.json +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/vectors/refuter.json +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/vectors/targeted-validator.json +0 -0
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
import { readFileSync } from "node:fs";
|
|
4
|
+
import { resolve } from "node:path";
|
|
5
|
+
import { performance } from "node:perf_hooks";
|
|
6
|
+
import { pathToFileURL } from "node:url";
|
|
7
|
+
|
|
8
|
+
const config = JSON.parse(process.argv[2]);
|
|
9
|
+
const piVersion = JSON.parse(readFileSync(resolve(config.piRoot, "package.json"), "utf8")).version;
|
|
10
|
+
const tuiVersion = JSON.parse(readFileSync(resolve(config.piRoot, "node_modules/@earendil-works/pi-tui/package.json"), "utf8")).version;
|
|
11
|
+
assert.equal(piVersion, "1.1.0", "private frame API audited only on Pi 1.1.0");
|
|
12
|
+
assert.equal(tuiVersion, piVersion, "host TUI package must match");
|
|
13
|
+
const hostImport = (path) => import(pathToFileURL(resolve(config.piRoot, "dist", path)).href);
|
|
14
|
+
const { createJiti } = await hostImport("core/extensions/jiti-loader.js");
|
|
15
|
+
const { VIRTUAL_MODULES } = await hostImport("core/extensions/virtual-modules.js");
|
|
16
|
+
const pi = VIRTUAL_MODULES["@earendil-works/pi-coding-agent"];
|
|
17
|
+
const { Container, TuiAltScreen } = VIRTUAL_MODULES["@earendil-works/pi-tui"];
|
|
18
|
+
const nativeTheme = await hostImport("modes/interactive/theme/theme.js");
|
|
19
|
+
const { KeybindingsManager } = await hostImport("core/keybindings.js");
|
|
20
|
+
const { ToolExecutionComponent } = await hostImport("modes/interactive/components/tool-execution.js");
|
|
21
|
+
const { UserMessageComponent } = await hostImport("modes/interactive/components/user-message.js");
|
|
22
|
+
const { AssistantMessageComponent } = await hostImport("modes/interactive/components/assistant-message.js");
|
|
23
|
+
const { createChatViewport } = await hostImport("modes/interactive/chat-viewport.js");
|
|
24
|
+
const loader = createJiti(import.meta.url, { moduleCache: false, fsCache: false, virtualModules: VIRTUAL_MODULES, tryNative: false });
|
|
25
|
+
const source = (path) => loader.import(resolve(config.root, path));
|
|
26
|
+
const shell = await source("extensions/gentle-shell.ts");
|
|
27
|
+
const quiet = await source("extensions/quiet-tools.ts");
|
|
28
|
+
const bar = await source("lib/shell-bar.ts");
|
|
29
|
+
const sidebar = await source("lib/shell-sidebar.ts");
|
|
30
|
+
const layout = await source("lib/shell-sidebar-layout.ts");
|
|
31
|
+
const cards = await source("lib/shell-card.ts");
|
|
32
|
+
assert.equal(Object.getPrototypeOf(shell.GentlePromptEditor.prototype), pi.CustomEditor.prototype, "editor must extend this exact host class");
|
|
33
|
+
const memory = () => Object.fromEntries(Object.entries(process.memoryUsage()).map(([key, bytes]) => [key, bytes / 1024 / 1024]));
|
|
34
|
+
const digest = (lines) => createHash("sha256").update(lines.join("\n")).digest("hex");
|
|
35
|
+
const usage = { input: 100, output: 20, cacheRead: 0, cacheWrite: 0, totalTokens: 120, cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0.001 } };
|
|
36
|
+
|
|
37
|
+
function fixture(spec, live = false) {
|
|
38
|
+
pi.initTheme("dark");
|
|
39
|
+
cards.setCardStyle(spec.style);
|
|
40
|
+
const counts = { paints: 0, projections: 0, entries: 0, requests: 0, pulses: 0, bytes: 0 };
|
|
41
|
+
const theme = { fg: (...args) => nativeTheme.theme.fg(...args), bg: (...args) => nativeTheme.theme.bg(...args), bold: (text) => nativeTheme.theme.bold(text) };
|
|
42
|
+
const toolTheme = { ...theme, fg: (...args) => { counts.paints++; return theme.fg(...args); }, bg: (...args) => { counts.paints++; return theme.bg(...args); } };
|
|
43
|
+
const geometry = { columns: spec.width, rows: 32 };
|
|
44
|
+
let inputHandler;
|
|
45
|
+
const terminal = {
|
|
46
|
+
start(onInput) { inputHandler = onInput; }, stop() {}, async drainInput() {}, write(text) { counts.bytes += Buffer.byteLength(text); },
|
|
47
|
+
get columns() { return geometry.columns; }, get rows() { return geometry.rows; },
|
|
48
|
+
moveBy() {}, hideCursor() {}, showCursor() {}, clearLine() {}, clearFromCursor() {}, clearScreen() {},
|
|
49
|
+
setTitle() {}, setProgress() {}, kittyProtocolActive: false, getBufferedInput() { return ""; }, hasPendingInput() { return false; },
|
|
50
|
+
};
|
|
51
|
+
const tui = new TuiAltScreen(terminal, false);
|
|
52
|
+
const manager = pi.SessionManager.inMemory("/performance", { id: "performance-session" });
|
|
53
|
+
const getEntries = manager.getEntries.bind(manager);
|
|
54
|
+
manager.getEntries = () => { counts.entries++; return getEntries(); };
|
|
55
|
+
const model = { id: "synthetic-model", provider: "synthetic", reasoning: true, contextWindow: 200000 };
|
|
56
|
+
const ctx = { model, sessionManager: manager, modelRegistry: { isUsingOAuth: () => false },
|
|
57
|
+
getContextUsage() { counts.projections++; return pi.AgentSession.prototype.getContextUsage.call({ sessionManager: manager, _limitsModel: () => model }); } };
|
|
58
|
+
const api = { getThinkingLevel: () => "high" };
|
|
59
|
+
const footerData = { getGitBranch: () => "performance", getExtensionStatuses: () => new Map(), onBranchChange: () => () => {} };
|
|
60
|
+
let clock = 0;
|
|
61
|
+
const editor = new shell.GentlePromptEditor(tui, nativeTheme.getEditorTheme(), new KeybindingsManager(), {
|
|
62
|
+
...theme, pending: () => false, now: () => live ? Date.now() : clock, doubleEscCancelEnabled: () => false,
|
|
63
|
+
dispatchQueuedText() {}, requestRender() { counts.requests++; if (live) tui.requestRender(); },
|
|
64
|
+
});
|
|
65
|
+
let pulse = () => {};
|
|
66
|
+
if (live) editor.setWorking(true);
|
|
67
|
+
else {
|
|
68
|
+
// Capture the actual quality interval callback, not a reimplementation.
|
|
69
|
+
const original = globalThis.setInterval;
|
|
70
|
+
try { globalThis.setInterval = (fn, ms) => { assert.equal(ms, 80); pulse = () => { counts.pulses++; fn(); }; return { unref() {} }; }; editor.setWorking(true); }
|
|
71
|
+
finally { globalThis.setInterval = original; }
|
|
72
|
+
}
|
|
73
|
+
const document = new Container();
|
|
74
|
+
const renderer = quiet.createQuietToolRenderer("read");
|
|
75
|
+
const definition = { ...renderer, renderCall: (args, _theme, context) => renderer.renderCall(args, toolTheme, context),
|
|
76
|
+
renderResult: (result, options, _theme, context) => renderer.renderResult(result, options, toolTheme, context) };
|
|
77
|
+
let toolCount = 0;
|
|
78
|
+
let latest;
|
|
79
|
+
const append = () => {
|
|
80
|
+
const id = `tool-${toolCount++}`;
|
|
81
|
+
const args = { path: `src/file-${id}.ts` };
|
|
82
|
+
const text = `Inspect **${id}** with Unicode 広い and ANSI-safe content.`;
|
|
83
|
+
manager.appendMessage({ role: "user", content: text, timestamp: 1 });
|
|
84
|
+
const assistant = { role: "assistant", content: [{ type: "text", text: "Checking `source` and a **settled** paragraph." }, { type: "toolCall", id, name: "read", arguments: args }],
|
|
85
|
+
api: "openai-responses", provider: "synthetic", model: model.id, stopReason: "toolUse", timestamp: 1, usage };
|
|
86
|
+
manager.appendMessage(assistant);
|
|
87
|
+
const result = { content: [{ type: "text", text: `Result ${id}\nconst answer = 42;\n${"long wrapped output ".repeat(12)}` }], details: {}, isError: false };
|
|
88
|
+
manager.appendMessage({ ...result, role: "toolResult", toolCallId: id, toolName: "read", timestamp: 1 });
|
|
89
|
+
document.addChild(new UserMessageComponent(text));
|
|
90
|
+
document.addChild(new AssistantMessageComponent(assistant));
|
|
91
|
+
latest = new ToolExecutionComponent("read", id, args, {}, definition, tui, "/performance");
|
|
92
|
+
latest.setArgsComplete(); latest.updateResult(result); document.addChild(latest);
|
|
93
|
+
};
|
|
94
|
+
for (let index = 0; index < spec.tools; index++) append();
|
|
95
|
+
const footer = new Container();
|
|
96
|
+
const modelForFrame = () => shell.buildShellBarModel(api, ctx, footerData);
|
|
97
|
+
const bottom = shell.createShellBarComponent(api, ctx, tui, theme, footerData);
|
|
98
|
+
footer.addChild(sidebar.sidebarPart(tui, "footer", bottom, {
|
|
99
|
+
digest: () => JSON.stringify(modelForFrame()), render: (width) => bar.renderShellSidebarBar(modelForFrame(), theme, width), invalidate() {},
|
|
100
|
+
}));
|
|
101
|
+
const disposeHeader = sidebar.sidebarHeader(tui, { digest: () => JSON.stringify(modelForFrame()),
|
|
102
|
+
render: (width) => bar.renderShellHeaderChrome(bar.buildShellHeaderModel(modelForFrame()), theme, width).rows, invalidate() {} });
|
|
103
|
+
const viewport = createChatViewport({ document, pendingMessages: new Container(), status: new Container(), widgetsAbove: new Container(),
|
|
104
|
+
editor, widgetsBelow: new Container(), footer, scrollbar: "hidden" });
|
|
105
|
+
tui.setLayoutRoot(viewport.root); tui.setFocus(editor);
|
|
106
|
+
let uninstall = layout.installSidebar(tui, theme);
|
|
107
|
+
tui.start();
|
|
108
|
+
const render = () => {
|
|
109
|
+
tui.doRender();
|
|
110
|
+
assert.equal(sidebar.sidebarState(tui).active, geometry.columns >= layout.SIDEBAR_BREAKPOINT, "sidebar must really own wide frames");
|
|
111
|
+
assert.equal(cards.cardStyle(), spec.style);
|
|
112
|
+
return [...tui.previousScreen];
|
|
113
|
+
};
|
|
114
|
+
return { tui, editor, manager, counts, render, input: (data) => inputHandler(data),
|
|
115
|
+
advance() { clock += 80; pulse(); },
|
|
116
|
+
apply(scenario, index) {
|
|
117
|
+
if (scenario === "scroll") { if (index % 2) viewport.transcript.scrollToEnd(); else viewport.transcript.scrollTo(0, { disableFollow: true }); }
|
|
118
|
+
if (scenario === "typing") { const text = editor.getText(); editor.handleInput("x"); assert.equal(editor.getText(), text + "x"); }
|
|
119
|
+
if (scenario === "stream") latest.updateResult({ content: [{ type: "text", text: `stream ${index} ${"partial ".repeat(index + 1)}` }], details: {} }, true);
|
|
120
|
+
if (scenario === "append") append();
|
|
121
|
+
if (scenario === "resize") { const previous = geometry.columns; geometry.columns = index % 2 ? spec.width : Math.max(40, spec.width - 60); assert.notEqual(geometry.columns, previous); }
|
|
122
|
+
if (scenario === "theme") {
|
|
123
|
+
const previous = theme.bg("toolSuccessBg", "");
|
|
124
|
+
pi.initTheme(index % 2 ? "dark" : "light");
|
|
125
|
+
assert.notEqual(theme.bg("toolSuccessBg", ""), previous);
|
|
126
|
+
document.invalidate(); editor.invalidate();
|
|
127
|
+
uninstall(); uninstall = layout.installSidebar(tui, { ...theme });
|
|
128
|
+
}
|
|
129
|
+
},
|
|
130
|
+
close() { editor.dispose(); disposeHeader(); uninstall(); bottom.dispose(); tui.stop(); },
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function controlled(spec) {
|
|
135
|
+
globalThis.gc();
|
|
136
|
+
const idleMemory = memory();
|
|
137
|
+
const buildStart = performance.now();
|
|
138
|
+
const scene = fixture(spec);
|
|
139
|
+
const buildMs = performance.now() - buildStart;
|
|
140
|
+
assert.equal(scene.counts.projections, 0, "first frame must not already have rendered");
|
|
141
|
+
const coldStart = performance.now();
|
|
142
|
+
const coldLines = scene.render();
|
|
143
|
+
const coldMs = performance.now() - coldStart;
|
|
144
|
+
const coldScreen = digest(coldLines);
|
|
145
|
+
for (let index = 0; index < config.warmup; index++) { scene.advance(); scene.render(); }
|
|
146
|
+
globalThis.gc();
|
|
147
|
+
const warmMemory = memory();
|
|
148
|
+
const initial = { ...scene.counts };
|
|
149
|
+
const result = { spec, buildMs, coldMs, coldScreen, idleMemory, warmMemory, wallMs: [], cpuMs: [], screens: [] };
|
|
150
|
+
const samples = ["resize", "theme"].includes(spec.scenario) ? Math.min(8, config.samples) : config.samples;
|
|
151
|
+
try {
|
|
152
|
+
for (let index = 0; index < samples; index++) {
|
|
153
|
+
const cpu = process.cpuUsage();
|
|
154
|
+
const start = performance.now();
|
|
155
|
+
scene.advance(); scene.apply(spec.scenario, index);
|
|
156
|
+
const screen = scene.render();
|
|
157
|
+
result.wallMs.push(performance.now() - start);
|
|
158
|
+
const used = process.cpuUsage(cpu);
|
|
159
|
+
result.cpuMs.push((used.user + used.system) / 1000);
|
|
160
|
+
result.screens.push(digest(screen));
|
|
161
|
+
}
|
|
162
|
+
result.counters = Object.fromEntries(Object.entries(scene.counts).map(([key, value]) => [key, value - initial[key]]));
|
|
163
|
+
assert.equal(result.counters.pulses, samples, "every controlled frame advances the real quality pulse");
|
|
164
|
+
} finally { scene.close(); }
|
|
165
|
+
return result;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
async function live(spec) {
|
|
169
|
+
const scene = fixture(spec, true);
|
|
170
|
+
for (let index = 0; index < config.warmup; index++) scene.render();
|
|
171
|
+
scene.editor.setWorking(false); scene.editor.setWorking(true); // Align real pulse onset after warmup.
|
|
172
|
+
const original = scene.tui.doRender.bind(scene.tui);
|
|
173
|
+
const result = { spec, durationMs: config.liveMs, frameMs: [], inputToFrameMs: [], inputTimerDriftMs: [] };
|
|
174
|
+
const started = performance.now();
|
|
175
|
+
const initial = { ...scene.counts };
|
|
176
|
+
let pendingInput;
|
|
177
|
+
let expected = started + 100;
|
|
178
|
+
scene.tui.doRender = () => {
|
|
179
|
+
const start = performance.now(); original();
|
|
180
|
+
result.frameMs.push(performance.now() - start);
|
|
181
|
+
if (pendingInput !== undefined) { result.inputToFrameMs.push(performance.now() - pendingInput); pendingInput = undefined; }
|
|
182
|
+
};
|
|
183
|
+
const input = setInterval(() => {
|
|
184
|
+
result.inputTimerDriftMs.push(performance.now() - expected); expected += 100;
|
|
185
|
+
pendingInput ??= performance.now(); scene.input("x");
|
|
186
|
+
}, 100);
|
|
187
|
+
try { await new Promise((resolve) => setTimeout(resolve, config.liveMs)); }
|
|
188
|
+
finally { clearInterval(input); scene.close(); }
|
|
189
|
+
result.actualDurationMs = performance.now() - started;
|
|
190
|
+
result.counters = Object.fromEntries(Object.entries(scene.counts).map(([key, value]) => [key, value - initial[key]]));
|
|
191
|
+
return result;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const cases = [];
|
|
195
|
+
for (const tools of config.counts) for (const width of config.widths) for (const style of config.styles) {
|
|
196
|
+
for (const scenario of ["pulse", "scroll", "typing", "stream", "append", "resize", "theme"]) {
|
|
197
|
+
const result = controlled({ tools, width, style, scenario });
|
|
198
|
+
// WeakRef keep-alive lasts until the job boundary; collect only after it.
|
|
199
|
+
await new Promise((resolve) => setImmediate(resolve)); globalThis.gc(); result.closedMemory = memory(); cases.push(result);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
const liveRuns = config.liveMs ? [await live({ tools: Math.max(...config.counts), width: Math.max(...config.widths), style: config.styles[0], scenario: "real-quality-timers-and-typing" })] : [];
|
|
203
|
+
const metadata = { piVersion, tuiVersion,
|
|
204
|
+
editorIdentityVerified: true, animation: "quality/80ms", terminal: "in-memory ANSI sink; no emulator", scheduler: "controlled quality callback; separate real-timer run" };
|
|
205
|
+
console.log(JSON.stringify({ metadata, cases, live: liveRuns }));
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
// Diagnostic comparison, not a speed gate. Run each source in a fresh process.
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { execFileSync } from "node:child_process";
|
|
4
|
+
import { createHash } from "node:crypto";
|
|
5
|
+
import { closeSync, ftruncateSync, openSync, readFileSync, readdirSync, realpathSync, writeSync } from "node:fs";
|
|
6
|
+
import { cpus, loadavg } from "node:os";
|
|
7
|
+
import { basename, dirname, isAbsolute, relative, resolve, sep } from "node:path";
|
|
8
|
+
import { fileURLToPath } from "node:url";
|
|
9
|
+
|
|
10
|
+
export function percentile(values, fraction) {
|
|
11
|
+
if (!values.length) return null;
|
|
12
|
+
const sorted = [...values].sort((a, b) => a - b);
|
|
13
|
+
const position = (sorted.length - 1) * fraction;
|
|
14
|
+
const low = Math.floor(position);
|
|
15
|
+
return sorted[low] + (sorted[Math.ceil(position)] - sorted[low]) * (position - low);
|
|
16
|
+
}
|
|
17
|
+
export function summarize(values) {
|
|
18
|
+
return { count: values.length, median: percentile(values, 0.5), p95: percentile(values, 0.95), max: values.length ? Math.max(...values) : null };
|
|
19
|
+
}
|
|
20
|
+
export function compare(before, after) {
|
|
21
|
+
assert.deepEqual(after.metadata, before.metadata, "host and scheduler must match");
|
|
22
|
+
assert.equal(before.cases.length, after.cases.length);
|
|
23
|
+
return before.cases.map((original, index) => {
|
|
24
|
+
const candidate = after.cases[index];
|
|
25
|
+
assert.deepEqual(candidate.spec, original.spec);
|
|
26
|
+
assert.equal(candidate.coldScreen, original.coldScreen, "cold visible output differs");
|
|
27
|
+
assert.deepEqual(candidate.screens, original.screens, `visible output differs: ${JSON.stringify(original.spec)}`);
|
|
28
|
+
return { spec: original.spec, before: summarize(original.wallMs), after: summarize(candidate.wallMs),
|
|
29
|
+
speedup: percentile(original.wallMs, 0.5) / percentile(candidate.wallMs, 0.5) };
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
function hashFiles(root, paths) {
|
|
33
|
+
const hash = createHash("sha256");
|
|
34
|
+
for (const path of paths.sort()) hash.update(path + "\0").update(readFileSync(resolve(root, path))).update("\0");
|
|
35
|
+
return hash.digest("hex");
|
|
36
|
+
}
|
|
37
|
+
function fingerprint(root) {
|
|
38
|
+
const paths = execFileSync("git", ["-C", root, "ls-files", "-z"], { encoding: "utf8" }).split("\0").filter(Boolean);
|
|
39
|
+
return { root, head: execFileSync("git", ["-C", root, "rev-parse", "HEAD"], { encoding: "utf8" }).trim(), sourceSha256: hashFiles(root, paths) };
|
|
40
|
+
}
|
|
41
|
+
function hostFingerprint(root) {
|
|
42
|
+
const packages = [root, ...["pi-tui", "pi-ai", "pi-agent-core", "jiti"].map((name) => resolve(root, "node_modules", name === "jiti" ? name : `@earendil-works/${name}`))];
|
|
43
|
+
return packages.map((path) => {
|
|
44
|
+
const packageRoot = realpathSync(path);
|
|
45
|
+
const files = readdirSync(packageRoot, { recursive: true, withFileTypes: true }).filter((entry) => entry.isFile()).map((entry) => relative(packageRoot, resolve(entry.parentPath, entry.name)));
|
|
46
|
+
return { root: packageRoot, package: JSON.parse(readFileSync(resolve(path, "package.json"), "utf8")).version, sha256: hashFiles(packageRoot, files) };
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
export function outputDescriptor(output, protectedRoots) {
|
|
50
|
+
const canonical = resolve(realpathSync(dirname(resolve(output))), basename(output));
|
|
51
|
+
for (const root of protectedRoots) {
|
|
52
|
+
const path = relative(realpathSync(root), canonical);
|
|
53
|
+
assert.ok(path && (isAbsolute(path) || path === ".." || path.startsWith(".." + sep)), "output must be outside source, harness and host roots");
|
|
54
|
+
}
|
|
55
|
+
return openSync(canonical, "wx", 0o600);
|
|
56
|
+
}
|
|
57
|
+
async function main() {
|
|
58
|
+
const options = Object.fromEntries(process.argv.slice(2).map((arg) => {
|
|
59
|
+
assert.match(arg, /^--[a-z-]+=.+$/, "use --name=value arguments");
|
|
60
|
+
const split = arg.indexOf("="); return [arg.slice(2, split), arg.slice(split + 1)];
|
|
61
|
+
}));
|
|
62
|
+
for (const key of ["baseline-root", "candidate-root", "pi-root", "output"]) assert.ok(options[key], `--${key} is required`);
|
|
63
|
+
const roots = { before: resolve(options["baseline-root"]), after: resolve(options["candidate-root"]) };
|
|
64
|
+
const initial = Object.fromEntries(Object.entries(roots).map(([key, root]) => [key, fingerprint(root)]));
|
|
65
|
+
assert.equal(initial.before.head, initial.after.head, "compare worktrees based on the same commit");
|
|
66
|
+
const numeric = (name, fallback, min) => { const value = Number(options[name] ?? fallback); assert.ok(Number.isInteger(value) && value >= min, `invalid --${name}`); return value; };
|
|
67
|
+
const list = (name, fallback) => (options[name] ?? fallback).split(",").map(Number);
|
|
68
|
+
const config = { piRoot: resolve(options["pi-root"]), counts: list("counts", "100,1000,5000"), widths: list("widths", "100,160"),
|
|
69
|
+
styles: (options.styles ?? "float,neon").split(","), samples: numeric("samples", 50, 2), warmup: 5, liveMs: numeric("live-ms", 6000, 0) };
|
|
70
|
+
assert.ok([...config.counts, ...config.widths].every((value) => Number.isInteger(value) && value > 0));
|
|
71
|
+
assert.ok(config.styles.length <= 2 && config.styles.every((style) => ["float", "neon"].includes(style)));
|
|
72
|
+
assert.ok(config.widths.every((width) => width > 40), "resize scenarios need widths above 40");
|
|
73
|
+
const repetitions = numeric("repetitions", 4, 1);
|
|
74
|
+
assert.ok(repetitions <= 20 && config.samples <= 2000 && config.liveMs <= 60000);
|
|
75
|
+
assert.ok(config.counts.length <= 4 && config.widths.length <= 4 && Math.max(...config.counts) <= 20000 && Math.max(...config.widths) <= 300);
|
|
76
|
+
const report = { schema: "gentle-shell.render-performance/v1", started: new Date().toISOString(), node: process.version,
|
|
77
|
+
cpu: cpus()[0]?.model, loadAtStart: loadavg(), sources: initial, config, runs: [], comparisons: [] };
|
|
78
|
+
const worker = fileURLToPath(new URL("render-cache-performance-worker.mjs", import.meta.url));
|
|
79
|
+
const instrumentationHash = () => createHash("sha256").update(readFileSync(fileURLToPath(import.meta.url))).update(readFileSync(worker)).digest("hex");
|
|
80
|
+
report.instrumentationSha256 = instrumentationHash();
|
|
81
|
+
report.host = hostFingerprint(config.piRoot);
|
|
82
|
+
const fd = outputDescriptor(options.output, [...Object.values(roots), config.piRoot, ...report.host.map((entry) => entry.root), resolve(dirname(fileURLToPath(import.meta.url)), "..")]);
|
|
83
|
+
const persist = () => {
|
|
84
|
+
const data = Buffer.from(JSON.stringify(report, null, 2) + "\n");
|
|
85
|
+
ftruncateSync(fd, 0);
|
|
86
|
+
let offset = 0;
|
|
87
|
+
while (offset < data.length) {
|
|
88
|
+
const written = writeSync(fd, data, offset, data.length - offset, offset);
|
|
89
|
+
assert.ok(written > 0); offset += written;
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
try {
|
|
93
|
+
for (let repetition = 0; repetition < repetitions; repetition++) {
|
|
94
|
+
const pair = {};
|
|
95
|
+
for (const variant of repetition % 2 ? ["after", "before"] : ["before", "after"]) {
|
|
96
|
+
console.log(`replica ${repetition + 1}/${repetitions}: ${variant}`);
|
|
97
|
+
const output = execFileSync(process.execPath, ["--expose-gc", "--experimental-strip-types", worker,
|
|
98
|
+
JSON.stringify({ ...config, root: roots[variant], repetition })], { encoding: "utf8", maxBuffer: 64 * 1024 * 1024, timeout: 1_200_000 });
|
|
99
|
+
pair[variant] = JSON.parse(output);
|
|
100
|
+
report.runs.push({ repetition, variant, ...pair[variant] });
|
|
101
|
+
persist();
|
|
102
|
+
}
|
|
103
|
+
report.comparisons.push({ repetition, cases: compare(pair.before, pair.after) });
|
|
104
|
+
persist();
|
|
105
|
+
}
|
|
106
|
+
for (const [key, root] of Object.entries(roots)) assert.deepEqual(fingerprint(root), initial[key], "source changed during measurement");
|
|
107
|
+
assert.equal(instrumentationHash(), report.instrumentationSha256, "instrumentation changed during measurement");
|
|
108
|
+
assert.deepEqual(hostFingerprint(config.piRoot), report.host, "host runtime changed during measurement");
|
|
109
|
+
report.finished = new Date().toISOString();
|
|
110
|
+
report.loadAtEnd = loadavg();
|
|
111
|
+
persist();
|
|
112
|
+
} finally { closeSync(fd); }
|
|
113
|
+
console.log(`PASS: ${report.comparisons[0].cases.length} equivalent cases × ${repetitions} paired replicas; raw report: ${resolve(options.output)}`);
|
|
114
|
+
}
|
|
115
|
+
if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) await main();
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Sequential test-suite runner for `pnpm test` (gentle-pi#1285).
|
|
3
|
+
//
|
|
4
|
+
// `pnpm test` used to chain its three stages with `&&`, so any stage-1
|
|
5
|
+
// failure — including a known-flaky unit test — silently suppressed the
|
|
6
|
+
// provider-contract check and the runtime harness while reporting a plain
|
|
7
|
+
// "test failed". This runner executes every stage unconditionally, prints a
|
|
8
|
+
// per-stage header and a final summary, and exits non-zero when any stage
|
|
9
|
+
// fails. Stages are plain shell commands, so behavior stays identical on
|
|
10
|
+
// POSIX and Windows CI.
|
|
11
|
+
|
|
12
|
+
import { spawn } from "node:child_process";
|
|
13
|
+
import { mkdtempSync, readFileSync, realpathSync, rmSync } from "node:fs";
|
|
14
|
+
import { tmpdir } from "node:os";
|
|
15
|
+
import { join } from "node:path";
|
|
16
|
+
import { fileURLToPath } from "node:url";
|
|
17
|
+
|
|
18
|
+
export const DEFAULT_STAGES = Object.freeze([
|
|
19
|
+
{ name: "unit-tests", command: "node --experimental-strip-types --test tests/*.test.ts" },
|
|
20
|
+
{ name: "provider-contract", command: "pnpm run check:provider-contract" },
|
|
21
|
+
{ name: "runtime-harness", command: "pnpm run test:harness" },
|
|
22
|
+
]);
|
|
23
|
+
|
|
24
|
+
export async function runStage(stage) {
|
|
25
|
+
// Unit fixtures must not inherit the terminal's Git overrides, child-session
|
|
26
|
+
// identity, or personal guardrails. Other stages retain their existing env.
|
|
27
|
+
const configHome = stage.name === "unit-tests" ? mkdtempSync(join(tmpdir(), "gentle-pi-unit-config-")) : undefined;
|
|
28
|
+
const env = configHome ? Object.fromEntries(Object.entries(process.env).filter(([key]) => {
|
|
29
|
+
const normalizedKey = key.toUpperCase();
|
|
30
|
+
return !normalizedKey.startsWith("GIT_") && !normalizedKey.startsWith("GENTLE_PI_AGENTS_") && normalizedKey !== "GENTLE_PI_CONFIG_HOME";
|
|
31
|
+
})) : process.env;
|
|
32
|
+
if (configHome) env.GENTLE_PI_CONFIG_HOME = configHome;
|
|
33
|
+
try {
|
|
34
|
+
return await new Promise((resolve) => {
|
|
35
|
+
const child = spawn(stage.command, { shell: true, stdio: "inherit", env });
|
|
36
|
+
child.on("close", (code) => resolve({ name: stage.name, code: code ?? 1 }));
|
|
37
|
+
child.on("error", (error) => {
|
|
38
|
+
console.error(`\n[${stage.name}] spawn failed: ${error.message}`);
|
|
39
|
+
resolve({ name: stage.name, code: 1 });
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
} finally {
|
|
43
|
+
if (configHome) rmSync(configHome, { recursive: true, force: true });
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export async function runTestSuite(stages = DEFAULT_STAGES, { runStageImpl = runStage, write = (line) => console.log(line) } = {}) {
|
|
48
|
+
const results = [];
|
|
49
|
+
for (const stage of stages) {
|
|
50
|
+
write(`\n=== [${stage.name}] ${stage.command} ===`);
|
|
51
|
+
results.push(await runStageImpl(stage));
|
|
52
|
+
}
|
|
53
|
+
write("\n=== test suite summary ===");
|
|
54
|
+
for (const result of results) write(`${result.code === 0 ? "PASS" : "FAIL"} ${result.name}`);
|
|
55
|
+
const failures = results.filter((result) => result.code !== 0);
|
|
56
|
+
write(failures.length === 0 ? "all stages passed" : `${failures.length} stage(s) failed: ${failures.map((f) => f.name).join(", ")}`);
|
|
57
|
+
return results;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function readStagesFromJsonPath(jsonPath) {
|
|
61
|
+
const stages = JSON.parse(readFileSync(jsonPath, "utf8"));
|
|
62
|
+
if (!Array.isArray(stages) || stages.some((stage) => typeof stage?.name !== "string" || typeof stage?.command !== "string")) {
|
|
63
|
+
throw new Error(`stages file must be an array of { name, command } objects: ${jsonPath}`);
|
|
64
|
+
}
|
|
65
|
+
return stages;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Direct-invocation detection must survive symlinks: Node real-paths
|
|
69
|
+
// `import.meta.url` but keeps `process.argv[1]` logical, so a plain equality
|
|
70
|
+
// check silently no-ops the whole runner (zero stages run, exit 0) when the
|
|
71
|
+
// script is reached through a file symlink.
|
|
72
|
+
function isDirectRun(argv1) {
|
|
73
|
+
if (!argv1) return false;
|
|
74
|
+
try {
|
|
75
|
+
return realpathSync(argv1) === realpathSync(fileURLToPath(import.meta.url));
|
|
76
|
+
} catch {
|
|
77
|
+
return false;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (isDirectRun(process.argv[1])) {
|
|
82
|
+
try {
|
|
83
|
+
const stages = process.argv[2] ? readStagesFromJsonPath(process.argv[2]) : DEFAULT_STAGES;
|
|
84
|
+
const results = await runTestSuite(stages);
|
|
85
|
+
process.exitCode = results.some((result) => result.code !== 0) ? 1 : 0;
|
|
86
|
+
} catch (error) {
|
|
87
|
+
console.error(error instanceof Error ? error.message : error);
|
|
88
|
+
process.exitCode = 1;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
# Opt-in Linux clean-machine acceptance for the browser installation wizard.
|
|
3
|
+
# Runs the real bootstrap and wizard inside a disposable debian:bookworm-slim
|
|
4
|
+
# container as a non-root user. Network access happens only inside that
|
|
5
|
+
# container; nothing reads or writes the host HOME. Not part of CI.
|
|
6
|
+
#
|
|
7
|
+
# Usage: sh scripts/test-installer-acceptance.sh [--worktree]
|
|
8
|
+
# default bundle = `git archive HEAD`
|
|
9
|
+
# --worktree bundle = tracked files as they are in the working tree,
|
|
10
|
+
# including uncommitted modifications (untracked files excluded)
|
|
11
|
+
# GENTLE_ACCEPTANCE_KEEP=1 keeps a failed run's container for inspection.
|
|
12
|
+
set -eu
|
|
13
|
+
fail() { printf 'acceptance: %s\n' "$1" >&2; exit 1; }
|
|
14
|
+
note() { printf 'acceptance: %s\n' "$1"; }
|
|
15
|
+
source_mode='head'
|
|
16
|
+
case $# in
|
|
17
|
+
0) ;;
|
|
18
|
+
1) [ "$1" = --worktree ] || fail "unknown argument: $1"; source_mode=worktree;;
|
|
19
|
+
*) fail 'usage: sh scripts/test-installer-acceptance.sh [--worktree]';;
|
|
20
|
+
esac
|
|
21
|
+
image=debian:bookworm-slim
|
|
22
|
+
# Upper bound for the whole installation inside the container.
|
|
23
|
+
install_limit=${GENTLE_ACCEPTANCE_INSTALL_SECONDS:-1500}
|
|
24
|
+
case $install_limit in ''|*[!0-9]*) fail 'GENTLE_ACCEPTANCE_INSTALL_SECONDS must be a whole number';; esac
|
|
25
|
+
command -v docker >/dev/null 2>&1 || fail 'Docker is required; refusing to run anything on the host'
|
|
26
|
+
docker info >/dev/null 2>&1 || fail 'Docker daemon is unavailable; refusing to run anything on the host'
|
|
27
|
+
command -v git >/dev/null 2>&1 || fail 'git is required to package the bundle'
|
|
28
|
+
repo=$(git rev-parse --show-toplevel) || fail 'run from inside the repository'
|
|
29
|
+
started=$(date +%s)
|
|
30
|
+
elapsed() { printf '%ss' "$(( $(date +%s) - started ))"; }
|
|
31
|
+
name=gentle-install-acceptance-$$-$started
|
|
32
|
+
container=
|
|
33
|
+
passed=0
|
|
34
|
+
cleanup() {
|
|
35
|
+
if [ -n "$container" ] && [ "$passed" = 0 ] && [ "${GENTLE_ACCEPTANCE_KEEP:-}" = 1 ]; then
|
|
36
|
+
printf 'acceptance: kept %s; inspect with: docker exec -it -u tester %s bash; remove with: docker rm -f %s\n' "$container" "$container" "$container" >&2
|
|
37
|
+
elif [ -n "$container" ]; then
|
|
38
|
+
docker rm -f "$container" >/dev/null 2>&1 || printf 'acceptance: could not remove container %s\n' "$container" >&2
|
|
39
|
+
fi
|
|
40
|
+
}
|
|
41
|
+
trap cleanup EXIT
|
|
42
|
+
trap 'exit 1' HUP INT TERM
|
|
43
|
+
|
|
44
|
+
note "image $image, bundle from $source_mode"
|
|
45
|
+
docker run -d --name "$name" --pull missing "$image" sleep infinity >/dev/null || fail 'cannot start the container'
|
|
46
|
+
container=$name
|
|
47
|
+
# Clean machine: no Node, npm, pnpm, Pi or Gentle Shell. curl and CA roots are
|
|
48
|
+
# bootstrap prerequisites that a stock Debian desktop already has.
|
|
49
|
+
docker exec "$container" sh -c 'export DEBIAN_FRONTEND=noninteractive
|
|
50
|
+
apt-get update -qq >/dev/null && apt-get install -y -qq --no-install-recommends ca-certificates curl >/dev/null
|
|
51
|
+
useradd --create-home --shell /bin/bash tester
|
|
52
|
+
mkdir /opt/bundle
|
|
53
|
+
for tool in node npm pnpm pi gentle-shell; do if command -v "$tool" >/dev/null; then echo "unexpected $tool" >&2; exit 1; fi; done' \
|
|
54
|
+
|| fail 'container preparation failed'
|
|
55
|
+
# gentle-shell setup makes 2 anonymous GitHub API requests (upstream Gentle AI's
|
|
56
|
+
# latest-engram lookup), limited to 60 per hour per public IP. /rate_limit does
|
|
57
|
+
# not consume quota. Stop early instead of failing deep inside setup.
|
|
58
|
+
quota=$(docker exec "$container" sh -c 'curl -fsS --max-time 20 -H "Accept: application/vnd.github+json" https://api.github.com/rate_limit' | tr -d ' \n') \
|
|
59
|
+
|| fail 'cannot query the GitHub API rate limit from the container'
|
|
60
|
+
rate=$(printf '%s' "$quota" | sed -n 's/.*"rate":{\([^}]*\)}.*/\1/p')
|
|
61
|
+
remaining=$(printf '%s' "$rate" | sed -n 's/.*"remaining":\([0-9]*\).*/\1/p')
|
|
62
|
+
reset=$(printf '%s' "$rate" | sed -n 's/.*"reset":\([0-9]*\).*/\1/p')
|
|
63
|
+
[ -n "$remaining" ] && [ -n "$reset" ] || fail 'unexpected GitHub rate limit response'
|
|
64
|
+
reset_at=$(date -u -d "@$reset" '+%Y-%m-%d %H:%M:%S UTC' 2>/dev/null || date -u -r "$reset" '+%Y-%m-%d %H:%M:%S UTC' 2>/dev/null || printf 'epoch %s' "$reset")
|
|
65
|
+
if [ "$remaining" -lt 5 ]; then
|
|
66
|
+
fail "only $remaining anonymous GitHub API requests remain on this network (setup needs 2); the quota resets at $reset_at. Not starting."
|
|
67
|
+
fi
|
|
68
|
+
note "GitHub API quota: $remaining anonymous requests left (resets at $reset_at)"
|
|
69
|
+
if [ "$source_mode" = head ]; then
|
|
70
|
+
git -C "$repo" archive --format=tar HEAD
|
|
71
|
+
else
|
|
72
|
+
git -C "$repo" ls-files -z | tar -C "$repo" --null -T - -cf -
|
|
73
|
+
fi | docker exec -i "$container" tar --no-same-owner -xf - -C /opt/bundle || fail 'cannot copy the bundle'
|
|
74
|
+
note "prepared in $(elapsed)"
|
|
75
|
+
|
|
76
|
+
bootstrap_started=$(date +%s)
|
|
77
|
+
# A terminal session exports SHELL (the user's login shell); docker exec does
|
|
78
|
+
# not. pnpm setup needs it to pick the profile to edit, and fails with
|
|
79
|
+
# ERR_PNPM_UNKNOWN_SHELL without it.
|
|
80
|
+
docker exec -d -u tester -w /home/tester -e SHELL=/bin/bash "$container" sh -c \
|
|
81
|
+
'sh /opt/bundle/scripts/bootstrap.sh > "$HOME/bootstrap.log" 2>&1; echo $? > "$HOME/bootstrap.status"' \
|
|
82
|
+
|| fail 'cannot start the bootstrap'
|
|
83
|
+
|
|
84
|
+
# Headless browser stand-in: the same requests the wizard page makes.
|
|
85
|
+
driver=$(cat <<'DRIVER'
|
|
86
|
+
set -eu
|
|
87
|
+
log=$HOME/bootstrap.log
|
|
88
|
+
say() { printf 'driver: %s\n' "$1"; }
|
|
89
|
+
stop() { say "$1"; cat "$log" 2>/dev/null || :; exit 1; }
|
|
90
|
+
waited=0
|
|
91
|
+
while :; do
|
|
92
|
+
url=$(grep -o 'http://127\.0\.0\.1:[0-9]*/session?code=[A-Za-z0-9_-]*' "$log" 2>/dev/null | head -n 1) || url=
|
|
93
|
+
[ -z "$url" ] || break
|
|
94
|
+
[ ! -f "$HOME/bootstrap.status" ] || stop 'bootstrap exited before printing the session URL'
|
|
95
|
+
waited=$((waited + 1)); [ "$waited" -lt 600 ] || stop 'no session URL after 600 s'
|
|
96
|
+
sleep 1
|
|
97
|
+
done
|
|
98
|
+
say "session URL after ${waited}s"
|
|
99
|
+
origin=${url%%/session*}
|
|
100
|
+
jar=$HOME/acceptance-cookies
|
|
101
|
+
curl -fsS -c "$jar" -b "$jar" -o /dev/null "$url" || stop 'session redemption failed'
|
|
102
|
+
api() { curl -fsS -b "$jar" -H 'X-Gentle-Install: 1' "$@"; }
|
|
103
|
+
post() { api -X POST -H "Origin: $origin" -H 'Content-Type: application/json' --data "$1" "$origin$2"; }
|
|
104
|
+
plan=$(api "$origin/api/plan") || stop 'plan request failed'
|
|
105
|
+
case $plan in *'"blockers":[]'*) ;; *) stop "plan has blockers: $plan";; esac
|
|
106
|
+
plan_id=$(printf '%s' "$plan" | sed -n 's/.*"planId":"\([^"]*\)".*/\1/p')
|
|
107
|
+
[ -n "$plan_id" ] || stop "plan has no planId: $plan"
|
|
108
|
+
say "plan: $(printf '%s' "$plan" | grep -o '"actions":\[[^]]*\]' | grep -o '"id":"[^"]*"' | cut -d'"' -f4 | tr '\n' ' ')"
|
|
109
|
+
post "{\"planId\":\"$plan_id\",\"consent\":true}" /api/install >/dev/null || stop 'install request rejected'
|
|
110
|
+
seq=0
|
|
111
|
+
polls=0
|
|
112
|
+
while :; do
|
|
113
|
+
progress=$(api "$origin/api/progress?after=$seq") || stop 'progress request failed'
|
|
114
|
+
printf '%s' "$progress" | grep -o '"step":"[^"]*","status":"[^"]*"' | sed 's/"step":"\([^"]*\)","status":"\([^"]*\)"/ step \1: \2/' || :
|
|
115
|
+
last=$(printf '%s' "$progress" | grep -o '"seq":[0-9]*' | tail -n 1 | cut -d: -f2) || last=
|
|
116
|
+
[ -z "$last" ] || seq=$last
|
|
117
|
+
case $progress in *'"running":false'*'"outcome":{'*) break;; esac
|
|
118
|
+
polls=$((polls + 1)); [ "$polls" -lt "$INSTALL_LIMIT" ] || stop "installation still running after $INSTALL_LIMIT s"
|
|
119
|
+
sleep 1
|
|
120
|
+
done
|
|
121
|
+
outcome=$(printf '%s' "$progress" | sed -n 's/.*"outcome":{"outcome":"\([a-z-]*\)".*/\1/p')
|
|
122
|
+
say "outcome: $outcome"
|
|
123
|
+
post '{}' /api/shutdown >/dev/null || stop 'shutdown request rejected'
|
|
124
|
+
waited=0
|
|
125
|
+
until [ -f "$HOME/bootstrap.status" ]; do
|
|
126
|
+
waited=$((waited + 1)); [ "$waited" -lt 60 ] || stop 'bootstrap still running 60 s after shutdown'
|
|
127
|
+
sleep 1
|
|
128
|
+
done
|
|
129
|
+
status=$(cat "$HOME/bootstrap.status")
|
|
130
|
+
say "bootstrap exit status: $status"
|
|
131
|
+
cat "$log"
|
|
132
|
+
case $outcome in ready|terminal-action-required) [ "$status" = 0 ] || exit 1;; *) exit 1;; esac
|
|
133
|
+
DRIVER
|
|
134
|
+
)
|
|
135
|
+
printf '%s\n' "$driver" | docker exec -i -u tester -w /home/tester -e INSTALL_LIMIT="$install_limit" "$container" sh -s \
|
|
136
|
+
|| fail "wizard flow failed after $(elapsed)"
|
|
137
|
+
note "bootstrap and wizard finished in $(( $(date +%s) - bootstrap_started ))s"
|
|
138
|
+
|
|
139
|
+
# A fresh interactive shell must find the persisted stack; the bootstrap tools
|
|
140
|
+
# must be gone. bash -i without a terminal warns about job control; harmless.
|
|
141
|
+
checks=$(cat <<'CHECKS'
|
|
142
|
+
set -eu
|
|
143
|
+
for tool in node npm pnpm gentle-shell; do printf '%s -> %s\n' "$tool" "$(command -v "$tool")"; done
|
|
144
|
+
printf 'node %s, npm %s, pnpm %s\n' "$(node --version)" "$(npm --version)" "$(pnpm --version)"
|
|
145
|
+
gentle-shell --version
|
|
146
|
+
for dir in "$HOME"/.gentle-shell-bootstrap-tools.*; do
|
|
147
|
+
if [ -e "$dir" ] || [ -L "$dir" ]; then echo "bootstrap tools left behind: $dir" >&2; exit 1; fi
|
|
148
|
+
done
|
|
149
|
+
echo 'bootstrap tools removed'
|
|
150
|
+
CHECKS
|
|
151
|
+
)
|
|
152
|
+
docker exec -u tester -w /home/tester "$container" bash -ic "$checks" || fail "fresh-terminal checks failed after $(elapsed)"
|
|
153
|
+
# Informational: pnpm setup edits only ~/.bashrc, which non-interactive login
|
|
154
|
+
# shells skip on Debian (see docs/install-wizard.md).
|
|
155
|
+
if docker exec -u tester -w /home/tester "$container" bash -lc 'command -v gentle-shell' >/dev/null 2>&1; then
|
|
156
|
+
note 'bash -lc resolves gentle-shell'
|
|
157
|
+
else
|
|
158
|
+
note 'bash -lc does not resolve gentle-shell (expected: ~/.profile is not edited)'
|
|
159
|
+
fi
|
|
160
|
+
passed=1
|
|
161
|
+
note "PASS in $(elapsed)"
|