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
|
@@ -9,9 +9,9 @@ import { dirname, join, resolve, sep } from "node:path";
|
|
|
9
9
|
import test from "node:test";
|
|
10
10
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
11
11
|
import { __testing, createGentleAiExtension, PendingReviewConsentRegistry } from "../extensions/gentle-ai.ts";
|
|
12
|
-
import { CandidateViewRegistry } from "../lib/review-candidate-view.ts";
|
|
12
|
+
import { CandidateViewError, CandidateViewRegistry } from "../lib/review-candidate-view.ts";
|
|
13
13
|
import { NATIVE_REVIEW_ERROR_CODE, NativeReviewCliError, NativeReviewConsentRequiredError, type NativeReviewCli } from "../lib/native-review-cli.ts";
|
|
14
|
-
import { decodeReviewConsentV3, decodeReviewStatusV3, type ReviewCollectInputV3, type ReviewStatusV3 } from "../lib/review-integration-v2.ts";
|
|
14
|
+
import { decodeReviewLastEventClosureV1, decodeReviewConsentV3, decodeReviewStatusV3, type ReviewCollectInputV3, type ReviewStatusV3 } from "../lib/review-integration-v2.ts";
|
|
15
15
|
|
|
16
16
|
const SHA = `sha256:${"a".repeat(64)}`;
|
|
17
17
|
const TREE = "b".repeat(40);
|
|
@@ -129,6 +129,149 @@ test("STATUS renders the managed_assets_outdated continuation command as the act
|
|
|
129
129
|
assert.equal(degraded.hint, undefined);
|
|
130
130
|
});
|
|
131
131
|
|
|
132
|
+
test("caller-relative STATUS approved guidance preserves selected root against a different session", async () => {
|
|
133
|
+
const lineageId = "caller-relative-status";
|
|
134
|
+
const root = realpathSync(process.cwd());
|
|
135
|
+
const native = { targetStatus: async () => approvedAcknowledgementStatus(lineageId, root) } as unknown as NativeReviewCli;
|
|
136
|
+
const result = await __testing.executeReviewControllerOperation({ operation: "status", lineageId, workspaceRoot: root }, dirname(root), native);
|
|
137
|
+
assert.equal(result.next_action, `gentle_review {"operation":"acknowledge-approved","lineageId":"${lineageId}","workspaceRoot":${JSON.stringify(root)}}`);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
test("caller-relative approved closure guidance preserves selected root and canonical omission", async () => {
|
|
141
|
+
const lineageId = "caller-relative-closure";
|
|
142
|
+
const root = realpathSync(process.cwd());
|
|
143
|
+
const { submission: _submission, artifactSubject: _artifactSubject, ...base } = collectInput(lineageId);
|
|
144
|
+
const input = { ...base, name: "provider_refuter", schema: "https://gentle-ai.dev/schema/review/refuter/v1", captureOperation: "review.capture-refuter", arguments: [
|
|
145
|
+
{ name: "lineage", value: lineageId, token: `--lineage=${lineageId}` },
|
|
146
|
+
{ name: "target", value: SHA, token: `--target=${SHA}` },
|
|
147
|
+
{ name: "agent", value: "pi", token: "--agent=pi" },
|
|
148
|
+
{ name: "execute", value: "true", token: "--execute=true" },
|
|
149
|
+
] } as unknown as ReviewCollectInputV3;
|
|
150
|
+
const transition = approvedAcknowledgementStatus(lineageId, root).nextTransition;
|
|
151
|
+
assert.equal(transition?.kind, "execute");
|
|
152
|
+
if (transition?.kind !== "execute") throw new Error("expected acknowledgement transition");
|
|
153
|
+
const acknowledgement = transition.execute;
|
|
154
|
+
const native = {
|
|
155
|
+
targetStatus: async () => ({ ...status(lineageId), nextTransition: { kind: "collect", reasonCode: "provider_role_required", collect: { inputs: [input] } } }),
|
|
156
|
+
captureProviderRole: async () => ({ schema: "gentle-ai.review-last-event-closure/v1", operation: "review.capture-refuter", lineageId, state: "approved", storeRevision: SHA, acknowledgement }),
|
|
157
|
+
} as unknown as NativeReviewCli;
|
|
158
|
+
const result = await __testing.executeReviewCaptureOperation({ lineageId, workspaceRoot: root, collectBinding: JSON.stringify(input) }, dirname(root), native);
|
|
159
|
+
assert.equal(result.next_action, `gentle_review {"operation":"acknowledge-approved","lineageId":"${lineageId}","workspaceRoot":${JSON.stringify(root)}}`);
|
|
160
|
+
const canonical = await __testing.executeReviewCaptureOperation({ lineageId, collectBinding: JSON.stringify(input) }, root, native);
|
|
161
|
+
assert.equal(canonical.next_action, `gentle_review {"operation":"acknowledge-approved","lineageId":"${lineageId}"}`);
|
|
162
|
+
assert.deepEqual((result.closure as Record<string, unknown>).acknowledgement, (canonical.closure as Record<string, unknown>).acknowledgement);
|
|
163
|
+
// This root-only registry double models a remembered lineage without a
|
|
164
|
+
// frozen projection, and enforces the same root identity at admission.
|
|
165
|
+
const boundViews = {
|
|
166
|
+
resolveWorkspaceRoot: (requestedLineage: string) => {
|
|
167
|
+
assert.equal(requestedLineage, lineageId);
|
|
168
|
+
return root;
|
|
169
|
+
},
|
|
170
|
+
assertWorkspaceRoot: (requestedLineage: string, requestedRoot: string) => {
|
|
171
|
+
assert.equal(requestedLineage, lineageId);
|
|
172
|
+
assert.equal(realpathSync(requestedRoot), root);
|
|
173
|
+
},
|
|
174
|
+
hasProjection: () => false,
|
|
175
|
+
} as unknown as CandidateViewRegistry;
|
|
176
|
+
const bound = await __testing.executeReviewCaptureOperation({ lineageId, collectBinding: JSON.stringify(input) }, dirname(root), native, undefined, boundViews);
|
|
177
|
+
assert.equal(bound.next_action, canonical.next_action);
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
test("caller-relative grouped capture closure preserves selected workspace root", async (t) => {
|
|
181
|
+
t.after(() => __testing.setReviewHostRelayGroupRunnersForTesting());
|
|
182
|
+
const lineageId = "caller-relative-grouped";
|
|
183
|
+
const root = realpathSync(process.cwd());
|
|
184
|
+
const inputs = (["review-risk", "review-resilience", "review-readability", "review-reliability"] as const).map((lens, order) => {
|
|
185
|
+
const input = collectInput(lineageId);
|
|
186
|
+
input.arguments = input.arguments.map((argument) => {
|
|
187
|
+
const value = argument.name === "lens" ? lens : argument.name === "order" ? String(order) : argument.name === "subject-hash" ? `sha256:${String(order).repeat(64)}` : argument.value;
|
|
188
|
+
return { ...argument, value, token: `--${argument.name}=${value}` };
|
|
189
|
+
});
|
|
190
|
+
input.submission!.argumentTokens = [...input.arguments.map((argument) => argument.token!), "--input={{value}}"];
|
|
191
|
+
input.arguments = [...input.arguments, { name: "agent", value: "pi", token: "--agent=pi" }, { name: "materialize", value: "true", token: "--materialize=true" }];
|
|
192
|
+
input.artifactSubject = { ...input.artifactSubject!, lens, selectedOrder: order, subjectHash: `sha256:${String(order).repeat(64)}` };
|
|
193
|
+
return input;
|
|
194
|
+
});
|
|
195
|
+
const transition = approvedAcknowledgementStatus(lineageId, root).nextTransition;
|
|
196
|
+
if (transition?.kind !== "execute") throw new Error("expected acknowledgement transition");
|
|
197
|
+
const acknowledgement = transition.execute;
|
|
198
|
+
const native = { targetStatus: async (request: { cwd: string }) => {
|
|
199
|
+
assert.equal(request.cwd, root);
|
|
200
|
+
return { ...status(lineageId, inputs), repositoryContext: { capability: "review.opaque_repository_context", handle: `rctx1_${"c".repeat(64)}`, revision: SHA, targetIdentity: SHA } };
|
|
201
|
+
} } as unknown as NativeReviewCli;
|
|
202
|
+
const wireAcknowledgement = { ...acknowledgement, binding: { lineage_id: lineageId, revision: SHA, target_identity: SHA } };
|
|
203
|
+
const wireClosure = { schema: "gentle-ai.review-last-event-closure/v1", operation: "review/capture-result", lineage_id: lineageId, state: "approved", store_revision: SHA, action: "approved by reviewers", acknowledgement: wireAcknowledgement };
|
|
204
|
+
const decodedClosure = decodeReviewLastEventClosureV1(wireClosure);
|
|
205
|
+
assert.equal(decodedClosure.lineageId, lineageId);
|
|
206
|
+
assert.deepEqual(decodedClosure.acknowledgement?.raw, wireAcknowledgement);
|
|
207
|
+
let submissions = 0;
|
|
208
|
+
__testing.setReviewHostRelayGroupRunnersForTesting(async (requests) => requests.map((request) => ({ request, promptByteLength: 64, resultByteLength: 32 })), async () => {
|
|
209
|
+
submissions += 1;
|
|
210
|
+
return { promptByteLength: 64, resultByteLength: 32, submission: JSON.stringify(wireClosure) };
|
|
211
|
+
});
|
|
212
|
+
const result = await __testing.executeReviewCaptureGroupOperation({ lineageId, workspaceRoot: root, collectBindings: inputs.map((input) => JSON.stringify(input)), reviewerRunAcknowledged: true }, dirname(root), native);
|
|
213
|
+
assert.equal(submissions, 1, JSON.stringify(result));
|
|
214
|
+
assert.equal(result.outcome, "native-last-event-closure", JSON.stringify(result));
|
|
215
|
+
assert.equal(result.next_action, `gentle_review {"operation":"acknowledge-approved","lineageId":"${lineageId}","workspaceRoot":${JSON.stringify(root)}}`);
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
// Issue #1371: approved status mappings and closure envelopes provide additive next_action
|
|
219
|
+
test("STATUS on approved target renders additive next_action naming exact gentle_review acknowledge-approved invocation", async () => {
|
|
220
|
+
const lineageId = "status-approved-ack";
|
|
221
|
+
const native = { targetStatus: async () => approvedAcknowledgementStatus(lineageId) } as unknown as NativeReviewCli;
|
|
222
|
+
const result = await __testing.executeReviewControllerOperation({ operation: "status", lineageId }, process.cwd(), native);
|
|
223
|
+
assert.equal(result.status, "blocked");
|
|
224
|
+
assert.equal(result.next_action, `gentle_review {"operation":"acknowledge-approved","lineageId":"${lineageId}"}`);
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
test("STATUS on approved target preserves workspaceRoot in next_action when distinct from process cwd", async (t) => {
|
|
228
|
+
const lineageId = "status-approved-ack-worktree";
|
|
229
|
+
const worktreeRoot = repository(t);
|
|
230
|
+
const native = { targetStatus: async () => approvedAcknowledgementStatus(lineageId, worktreeRoot) } as unknown as NativeReviewCli;
|
|
231
|
+
const result = await __testing.executeReviewControllerOperation({ operation: "status", lineageId, workspaceRoot: worktreeRoot }, worktreeRoot, native);
|
|
232
|
+
assert.equal(result.status, "blocked");
|
|
233
|
+
assert.equal(result.next_action, `gentle_review {"operation":"acknowledge-approved","lineageId":"${lineageId}","workspaceRoot":${JSON.stringify(worktreeRoot)}}`);
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
test("invalid acknowledgement input returns self-healing next_action for canonical lineage and fallback slug otherwise", async (t) => {
|
|
237
|
+
const lineageId = "canonical-lineage-ack";
|
|
238
|
+
const native = { targetStatus: async () => approvedAcknowledgementStatus(lineageId) } as unknown as NativeReviewCli;
|
|
239
|
+
|
|
240
|
+
// 1. Controller-only input with canonical lineage (no workspaceRoot)
|
|
241
|
+
const rejectedWithInput = await __testing.executeReviewControllerOperation(
|
|
242
|
+
{ operation: "acknowledge-approved", lineageId, input: "{}" },
|
|
243
|
+
process.cwd(),
|
|
244
|
+
native,
|
|
245
|
+
);
|
|
246
|
+
assert.equal(rejectedWithInput.outcome, "native-approved-acknowledgement-input-invalid");
|
|
247
|
+
assert.equal(rejectedWithInput.reason, "controller-only-input");
|
|
248
|
+
assert.equal(rejectedWithInput.field, "input");
|
|
249
|
+
assert.equal(rejectedWithInput.next_action, `gentle_review {"operation":"acknowledge-approved","lineageId":"${lineageId}"}`);
|
|
250
|
+
|
|
251
|
+
// 2. Controller-only input with canonical lineage and distinct workspaceRoot
|
|
252
|
+
const worktreeRoot = repository(t);
|
|
253
|
+
const rejectedWithWorktree = await __testing.executeReviewControllerOperation(
|
|
254
|
+
{ operation: "acknowledge-approved", lineageId, input: "{}", workspaceRoot: worktreeRoot },
|
|
255
|
+
process.cwd(),
|
|
256
|
+
native,
|
|
257
|
+
);
|
|
258
|
+
assert.equal(rejectedWithWorktree.outcome, "native-approved-acknowledgement-input-invalid");
|
|
259
|
+
assert.equal(
|
|
260
|
+
rejectedWithWorktree.next_action,
|
|
261
|
+
`gentle_review {"operation":"acknowledge-approved","lineageId":"${lineageId}","workspaceRoot":${JSON.stringify(worktreeRoot)}}`,
|
|
262
|
+
);
|
|
263
|
+
|
|
264
|
+
// 3. Non-canonical / invalid lineage returns fallback slug
|
|
265
|
+
const rejectedNonCanonical = await __testing.executeReviewControllerOperation(
|
|
266
|
+
{ operation: "acknowledge-approved", lineageId: "invalid lineage with newline\n", input: "{}" },
|
|
267
|
+
process.cwd(),
|
|
268
|
+
native,
|
|
269
|
+
);
|
|
270
|
+
assert.equal(rejectedNonCanonical.outcome, "native-approved-acknowledgement-input-invalid");
|
|
271
|
+
assert.equal(rejectedNonCanonical.reason, "controller-only-input");
|
|
272
|
+
assert.equal(rejectedNonCanonical.next_action, "resubmit-the-exact-lineage-without-controller-only-input");
|
|
273
|
+
});
|
|
274
|
+
|
|
132
275
|
test("public acknowledgement relays one current provider vector and never replays after authority burn", async () => {
|
|
133
276
|
const lineageId = "acknowledge-approved";
|
|
134
277
|
const requests: Array<Record<string, unknown>> = [];
|
|
@@ -463,7 +606,10 @@ test("interleaved sessions sharing a CLI retain only their own capture routes",
|
|
|
463
606
|
const index = requests.length - 1;
|
|
464
607
|
if (index < 2) { [readyA, readyB][index]!(); await waits[index]!; }
|
|
465
608
|
const lineageId = String(request.lineageId);
|
|
466
|
-
|
|
609
|
+
// Cache absence alone is not foreign-session evidence. The provider
|
|
610
|
+
// rejects selectorless requests for these committed-range candidates.
|
|
611
|
+
return request.baseRef === (lineageId === a ? "base-a" : "base-b")
|
|
612
|
+
? status(lineageId, [inputs.get(lineageId)!]) : status(lineageId, []);
|
|
467
613
|
},
|
|
468
614
|
captureCorrectionPlan: async ({ argumentTokens }: { argumentTokens: readonly string[] }) => {
|
|
469
615
|
captures += 1;
|
|
@@ -487,7 +633,25 @@ test("interleaved sessions sharing a CLI retain only their own capture routes",
|
|
|
487
633
|
outcomes: [ownA, ownB, foreign, cleaned].map(({ details }) => (details as { outcome?: string }).outcome),
|
|
488
634
|
revalidationCalls: requests.length - 2,
|
|
489
635
|
captures,
|
|
490
|
-
}, { outcomes: ["native-last-event-closure", "native-last-event-closure", "capture-binding-rejected", "capture-binding-rejected"], revalidationCalls:
|
|
636
|
+
}, { outcomes: ["native-last-event-closure", "native-last-event-closure", "capture-binding-rejected", "capture-binding-rejected"], revalidationCalls: 4, captures: 2 });
|
|
637
|
+
});
|
|
638
|
+
|
|
639
|
+
test("capture route recovery uses a trusted committed projection through unknown-outcome reconciliation", async (t) => {
|
|
640
|
+
const candidateViews = new CandidateViewRegistry();
|
|
641
|
+
t.after(() => candidateViews.cleanupAll());
|
|
642
|
+
const cwd = repository(t), lineageId = "recovered-committed", input = correctionPlanInput(lineageId);
|
|
643
|
+
const view = candidateViews.create({ contributorRoot: cwd, baseRef: execFileSync("git", ["rev-parse", "HEAD"], { cwd, encoding: "utf8" }).trim(), committedOnly: true });
|
|
644
|
+
candidateViews.retain(view.token, lineageId);
|
|
645
|
+
const target = candidateViews.resolveProjection(lineageId, cwd), requests: Array<Record<string, unknown>> = [];
|
|
646
|
+
const native = {
|
|
647
|
+
targetStatus: async (request: Record<string, unknown>) => { requests.push(request); return status(lineageId, [input]); },
|
|
648
|
+
captureCorrectionPlan: async () => { throw Object.assign(new Error("lost response"), { mutationOutcome: "unknown", nextAction: "review.status" }); },
|
|
649
|
+
} as unknown as NativeReviewCli;
|
|
650
|
+
const result = await __testing.executeReviewCaptureOperation({ lineageId, collectBinding: JSON.stringify(input), correctionLines: 1 }, cwd, native, undefined, candidateViews, new Map(), true);
|
|
651
|
+
assert.equal(result.outcome, "native-capture-outcome-unknown");
|
|
652
|
+
assert.equal(requests.length, 2, "one admission STATUS and one reconciliation, never capture replay");
|
|
653
|
+
assert.ok(requests.every((request) => request.cwd === cwd && request.lineageId === lineageId && request.baseRef === target.baseCommit && request.committedOnly === true));
|
|
654
|
+
assert.equal(requests[0]!.agent, "pi");
|
|
491
655
|
});
|
|
492
656
|
|
|
493
657
|
test("REPAIR retains frozen committed collect selectors and leaves workspace routes unselected", async (t) => {
|
|
@@ -649,7 +813,8 @@ test("STATUS preserves retained intended-untracked selection through selectorles
|
|
|
649
813
|
selections,
|
|
650
814
|
true,
|
|
651
815
|
);
|
|
652
|
-
assert.deepEqual({ outcome: stale.outcome, requests: requests.length, captures }, { outcome: "capture-binding-rejected", requests:
|
|
816
|
+
assert.deepEqual({ outcome: stale.outcome, requests: requests.length, captures }, { outcome: "capture-binding-rejected", requests: 4, captures: 0 });
|
|
817
|
+
assert.deepEqual(requests[3], { cwd, lineageId, agent: "pi", ...selectedUntracked });
|
|
653
818
|
|
|
654
819
|
const captured = await __testing.executeReviewCaptureOperation(
|
|
655
820
|
{ lineageId, collectBinding: bindingB, correctionLines: 1 },
|
|
@@ -666,6 +831,7 @@ test("STATUS preserves retained intended-untracked selection through selectorles
|
|
|
666
831
|
{ cwd, lineageId, agent: "pi", baseRef: "main", committedOnly: true },
|
|
667
832
|
{ cwd, lineageId, agent: "pi", ...selectedUntracked },
|
|
668
833
|
{ cwd, lineageId, agent: "pi", ...selectedUntracked },
|
|
834
|
+
{ cwd, lineageId, agent: "pi", ...selectedUntracked },
|
|
669
835
|
]);
|
|
670
836
|
assert.equal(captures, 1);
|
|
671
837
|
|
|
@@ -676,10 +842,14 @@ test("STATUS preserves retained intended-untracked selection through selectorles
|
|
|
676
842
|
});
|
|
677
843
|
|
|
678
844
|
test("route retention caps, rejects collisions and invalid selectors, and clears every terminal state", async () => {
|
|
679
|
-
const
|
|
845
|
+
const requests: Array<Record<string, unknown>> = [];
|
|
846
|
+
const native = { targetStatus: async (request: Record<string, unknown>) => { requests.push(request); return status(String(request.lineageId), request.baseRef === undefined ? [] : [collectInput(String(request.lineageId))]); } } as unknown as NativeReviewCli;
|
|
680
847
|
const selections = new Map();
|
|
681
848
|
for (let index = 0; index <= 64; index += 1) await __testing.executeReviewControllerOperation({ operation: "status", lineageId: `bounded-${index}`, input: JSON.stringify({ baseRef: `base-${index}`, committedOnly: true }) }, process.cwd(), native, undefined, undefined, undefined, selections);
|
|
849
|
+
assert.equal(selections.size, 64, "route retention remains bounded before native refresh");
|
|
682
850
|
const evicted = await __testing.executeReviewCaptureOperation({ lineageId: "bounded-0", collectBinding: JSON.stringify(collectInput("bounded-0")) }, process.cwd(), native, undefined, undefined, selections, true);
|
|
851
|
+
assert.equal(requests.length, 66, "eviction must negotiate fresh native STATUS before rejection");
|
|
852
|
+
assert.deepEqual(requests.at(-1), { cwd: process.cwd(), lineageId: "bounded-0", agent: "pi" });
|
|
683
853
|
const collision = new Map(), lineageId = "route-collision", input = correctionPlanInput(lineageId);
|
|
684
854
|
await __testing.executeReviewControllerOperation({ operation: "status", lineageId, input: JSON.stringify({ baseRef: "base-a", committedOnly: true }) }, process.cwd(), { targetStatus: async () => status(lineageId, [input]) } as unknown as NativeReviewCli, undefined, undefined, undefined, collision);
|
|
685
855
|
const rejected = await __testing.executeReviewControllerOperation({ operation: "status", lineageId, input: JSON.stringify({ baseRef: "base-b", committedOnly: true }) }, process.cwd(), { targetStatus: async () => status(lineageId, [input]) } as unknown as NativeReviewCli, undefined, undefined, undefined, collision);
|
|
@@ -936,6 +1106,72 @@ function startStatus(cwd: string, baseRef?: string, intendedUntracked: readonly
|
|
|
936
1106
|
}
|
|
937
1107
|
}
|
|
938
1108
|
|
|
1109
|
+
test("START reports POSIX candidate parent privacy refusal without native START or permission repair", { skip: process.platform === "win32" }, async (t) => {
|
|
1110
|
+
const cwd = repository(t);
|
|
1111
|
+
const target = startStatus(cwd);
|
|
1112
|
+
const parent = join(cwd, ".git", "gentle-ai", "candidate-views");
|
|
1113
|
+
chmodSync(parent, 0o777);
|
|
1114
|
+
let starts = 0;
|
|
1115
|
+
const native = {
|
|
1116
|
+
targetStatus: async () => target,
|
|
1117
|
+
start: async () => { starts++; throw new Error("native START must not run"); },
|
|
1118
|
+
} as unknown as NativeReviewCli;
|
|
1119
|
+
let result: Record<string, unknown>;
|
|
1120
|
+
try {
|
|
1121
|
+
result = await __testing.executeReviewControllerOperation({ operation: "start", input: JSON.stringify({ mode: "ordinary" }) }, cwd, native);
|
|
1122
|
+
} finally {
|
|
1123
|
+
assert.equal(fs.lstatSync(parent).mode & 0o777, 0o777);
|
|
1124
|
+
chmodSync(parent, 0o700);
|
|
1125
|
+
}
|
|
1126
|
+
assert.equal(result.status, "blocked");
|
|
1127
|
+
assert.equal(result.outcome, "native-operation-failed");
|
|
1128
|
+
assert.equal(result.lineage_created, false);
|
|
1129
|
+
assert.equal(result.mutation_outcome, "none");
|
|
1130
|
+
assert.deepEqual(result.diagnostics, {
|
|
1131
|
+
code: "candidate-owner-parent-privacy",
|
|
1132
|
+
message: "candidate-views parent must be owned by the current user and inaccessible to group and others; inspect its ownership and permissions, then correct them out of band before retrying START",
|
|
1133
|
+
});
|
|
1134
|
+
assert.equal(starts, 0);
|
|
1135
|
+
});
|
|
1136
|
+
|
|
1137
|
+
test("START reports ineffective POSIX private-mode mounts without native START or permission repair", { skip: process.platform === "win32" }, async (t) => {
|
|
1138
|
+
const cwd = repository(t);
|
|
1139
|
+
const target = startStatus(cwd);
|
|
1140
|
+
const parent = join(cwd, ".git", "gentle-ai", "candidate-views");
|
|
1141
|
+
mkdirSync(parent, { recursive: true, mode: 0o700 });
|
|
1142
|
+
chmodSync(parent, 0o777);
|
|
1143
|
+
const originalLstat = fs.lstatSync;
|
|
1144
|
+
t.mock.method(fs, "lstatSync", (path: Parameters<typeof fs.lstatSync>[0], ...args: unknown[]) => {
|
|
1145
|
+
const stat = (originalLstat as (...arguments_: unknown[]) => ReturnType<typeof fs.lstatSync>)(path, ...args);
|
|
1146
|
+
return typeof path === "string" && path.includes(".gentle-ai-chmod-probe-")
|
|
1147
|
+
? Object.assign(Object.create(Object.getPrototypeOf(stat)), stat, { mode: (Number(stat.mode) & ~0o777) | 0o777, uid: process.getuid?.() })
|
|
1148
|
+
: stat;
|
|
1149
|
+
});
|
|
1150
|
+
syncBuiltinESMExports();
|
|
1151
|
+
t.after(() => { t.mock.restoreAll(); syncBuiltinESMExports(); });
|
|
1152
|
+
let starts = 0;
|
|
1153
|
+
const native = {
|
|
1154
|
+
targetStatus: async () => target,
|
|
1155
|
+
start: async () => { starts++; throw new Error("native START must not run"); },
|
|
1156
|
+
} as unknown as NativeReviewCli;
|
|
1157
|
+
let result: Record<string, unknown>;
|
|
1158
|
+
try {
|
|
1159
|
+
result = await __testing.executeReviewControllerOperation({ operation: "start", input: JSON.stringify({ mode: "ordinary" }) }, cwd, native);
|
|
1160
|
+
} finally {
|
|
1161
|
+
assert.equal(fs.lstatSync(parent).mode & 0o777, 0o777);
|
|
1162
|
+
chmodSync(parent, 0o700);
|
|
1163
|
+
}
|
|
1164
|
+
assert.equal(result.status, "blocked");
|
|
1165
|
+
assert.equal(result.outcome, "native-operation-failed");
|
|
1166
|
+
assert.equal(result.lineage_created, false);
|
|
1167
|
+
assert.equal(result.mutation_outcome, "none");
|
|
1168
|
+
assert.deepEqual(result.diagnostics, {
|
|
1169
|
+
code: "candidate-owner-parent-chmod-ineffective",
|
|
1170
|
+
message: "candidate-views parent filesystem does not honor POSIX permission changes; move this repository's Git common directory to a POSIX-metadata filesystem or enable metadata support before retrying START",
|
|
1171
|
+
});
|
|
1172
|
+
assert.equal(starts, 0);
|
|
1173
|
+
});
|
|
1174
|
+
|
|
939
1175
|
test("ordinary START binds the native workspace candidate and returns the native result", async (t) => {
|
|
940
1176
|
const cwd = repository(t);
|
|
941
1177
|
const target = startStatus(cwd);
|
|
@@ -1045,6 +1281,171 @@ test("ordinary START adopts the offered committed-range base while an untracked
|
|
|
1045
1281
|
assert.equal(startCalls, 1, "native START is reached exactly once without drift");
|
|
1046
1282
|
});
|
|
1047
1283
|
|
|
1284
|
+
test("ordinary START adopts a provider-offered base tree without treating it as caller input", async (t) => {
|
|
1285
|
+
const cwd = repository(t);
|
|
1286
|
+
execFileSync("git", ["add", "tracked.txt"], { cwd });
|
|
1287
|
+
execFileSync("git", ["-c", "user.name=Routing Test", "-c", "user.email=routing@example.invalid", "commit", "-m", "candidate"], { cwd });
|
|
1288
|
+
const baseCommit = execFileSync("git", ["rev-parse", "HEAD~1"], { cwd, encoding: "utf8" }).trim();
|
|
1289
|
+
const baseTree = execFileSync("git", ["rev-parse", `${baseCommit}^{tree}`], { cwd, encoding: "utf8" }).trim();
|
|
1290
|
+
const offered = startStatus(cwd);
|
|
1291
|
+
offered.nextTransition = {
|
|
1292
|
+
kind: "execute", reasonCode: "start_required",
|
|
1293
|
+
execute: { operation: "review.start", arguments: [
|
|
1294
|
+
{ name: "target", value: offered.targetIdentity, token: `--target=${offered.targetIdentity}` },
|
|
1295
|
+
{ name: "projection", value: "workspace", token: "--projection=workspace" },
|
|
1296
|
+
{ name: "base-ref", value: baseTree, token: `--base-ref=${baseTree}` },
|
|
1297
|
+
{ name: "committed-only", value: "true", token: "--committed-only=true" },
|
|
1298
|
+
{ name: "agent", value: "pi", token: "--agent=pi" },
|
|
1299
|
+
], preconditions: [], binding: {} },
|
|
1300
|
+
} as unknown as ReviewStatusV3["nextTransition"];
|
|
1301
|
+
const adopted = startStatus(cwd, baseCommit);
|
|
1302
|
+
const requests: Array<Record<string, unknown>> = [];
|
|
1303
|
+
const starts: Array<Record<string, unknown>> = [];
|
|
1304
|
+
const native = {
|
|
1305
|
+
targetStatus: async (request: Record<string, unknown>) => {
|
|
1306
|
+
requests.push(request);
|
|
1307
|
+
return requests.length === 1 ? offered : adopted;
|
|
1308
|
+
},
|
|
1309
|
+
start: async (request: Record<string, unknown>) => {
|
|
1310
|
+
starts.push(request);
|
|
1311
|
+
return { lineageId: "tree-offer-started", state: "reviewing", riskLevel: "low", selectedLenses: [], changedFiles: 1, changedLines: 1, correctionBudget: 1, action: "created", lensesRequired: false, riskReasons: [], raw: {} };
|
|
1312
|
+
},
|
|
1313
|
+
} as unknown as NativeReviewCli;
|
|
1314
|
+
const views = new CandidateViewRegistry();
|
|
1315
|
+
t.after(() => views.cleanupAll());
|
|
1316
|
+
const result = await __testing.executeReviewControllerOperation({ operation: "start", input: JSON.stringify({ mode: "ordinary" }) }, cwd, native, undefined, views);
|
|
1317
|
+
assert.equal(result.operation, "start");
|
|
1318
|
+
assert.equal(requests.length, 2, "provider-offered tree requires a second bound STATUS");
|
|
1319
|
+
assert.equal(requests[1]!.baseRef, baseTree, "transport preserves the offered tree selector");
|
|
1320
|
+
assert.equal(starts.length, 1);
|
|
1321
|
+
assert.equal(starts[0]!.baseRef, baseTree, "native START receives the provider's original tree selector");
|
|
1322
|
+
assert.equal(starts[0]!.targetIdentity, adopted.targetIdentity);
|
|
1323
|
+
for (const input of [undefined, { baseRef: baseCommit, committedOnly: true }, undefined]) {
|
|
1324
|
+
const before = requests.length;
|
|
1325
|
+
const next = await __testing.executeReviewControllerOperation({ operation: "status", lineageId: "tree-offer-started", ...(input === undefined ? {} : { input: JSON.stringify(input) }) }, cwd, native, undefined, views);
|
|
1326
|
+
assert.equal(next.operation, "status");
|
|
1327
|
+
assert.equal(requests.length, before + 1);
|
|
1328
|
+
assert.equal(requests.at(-1)!.baseRef, input?.baseRef ?? baseTree, "selectorless STATUS must replay the provider tree; explicit caller commit wins only for its call");
|
|
1329
|
+
assert.equal(requests.at(-1)!.lineageId, "tree-offer-started");
|
|
1330
|
+
}
|
|
1331
|
+
});
|
|
1332
|
+
|
|
1333
|
+
test("facade STATUS hydrates an ordinary staged-index reviewer candidate without treating its base tree as an offered range", async (t) => {
|
|
1334
|
+
const cwd = repository(t);
|
|
1335
|
+
const baseTree = execFileSync("git", ["rev-parse", "HEAD^{tree}"], { cwd, encoding: "utf8" }).trim();
|
|
1336
|
+
writeFileSync(join(cwd, "tracked.txt"), "staged index candidate\n");
|
|
1337
|
+
execFileSync("git", ["add", "tracked.txt"], { cwd });
|
|
1338
|
+
const stagedTree = execFileSync("git", ["write-tree"], { cwd, encoding: "utf8" }).trim();
|
|
1339
|
+
const lineageId = "staged-index-hydration";
|
|
1340
|
+
const target = status(lineageId);
|
|
1341
|
+
target.projection = { ...target.projection, projection: "staged", baseTree, initialReviewTree: stagedTree, currentCandidateTree: stagedTree, paths: ["tracked.txt"] };
|
|
1342
|
+
const views = new CandidateViewRegistry();
|
|
1343
|
+
t.after(() => views.cleanupAll());
|
|
1344
|
+
const native = { targetStatus: async () => target } as unknown as NativeReviewCli;
|
|
1345
|
+
await __testing.executeReviewControllerOperation({ operation: "status", lineageId }, cwd, native, undefined, views);
|
|
1346
|
+
const restored = views.resolveCurrentForLens("review-risk", cwd);
|
|
1347
|
+
assert.equal(restored.baseTree, baseTree);
|
|
1348
|
+
assert.equal(restored.candidateTree, stagedTree);
|
|
1349
|
+
assert.equal(restored.committedOnly, false);
|
|
1350
|
+
assert.equal(views.resolveProjection(lineageId, cwd).providerBaseTree, undefined);
|
|
1351
|
+
});
|
|
1352
|
+
|
|
1353
|
+
test("fresh facade STATUS hydrates a historical commit-backed range without granting provider-tree provenance", async (t) => {
|
|
1354
|
+
const cwd = repository(t);
|
|
1355
|
+
const baseCommit = execFileSync("git", ["rev-parse", "HEAD"], { cwd, encoding: "utf8" }).trim();
|
|
1356
|
+
const baseTree = execFileSync("git", ["rev-parse", "HEAD^{tree}"], { cwd, encoding: "utf8" }).trim();
|
|
1357
|
+
writeFileSync(join(cwd, "tracked.txt"), "committed candidate\n");
|
|
1358
|
+
execFileSync("git", ["add", "tracked.txt"], { cwd });
|
|
1359
|
+
execFileSync("git", ["-c", "user.name=Routing Test", "-c", "user.email=routing@example.invalid", "commit", "-m", "candidate"], { cwd });
|
|
1360
|
+
const currentTree = execFileSync("git", ["rev-parse", "HEAD^{tree}"], { cwd, encoding: "utf8" }).trim();
|
|
1361
|
+
const historical = status("historical-commit-range");
|
|
1362
|
+
historical.projection = { ...historical.projection, projection: "staged", baseTree, initialReviewTree: currentTree, currentCandidateTree: currentTree, paths: ["tracked.txt"] };
|
|
1363
|
+
const views = new CandidateViewRegistry();
|
|
1364
|
+
t.after(() => views.cleanupAll());
|
|
1365
|
+
await __testing.executeReviewControllerOperation({ operation: "status", lineageId: "historical-commit-range" }, cwd, { targetStatus: async () => historical } as unknown as NativeReviewCli, undefined, views);
|
|
1366
|
+
const restored = views.resolveCurrentForLens("review-risk", cwd);
|
|
1367
|
+
assert.equal(restored.baseCommit, baseCommit);
|
|
1368
|
+
assert.equal(restored.baseTree, baseTree);
|
|
1369
|
+
assert.equal(restored.candidateTree, currentTree);
|
|
1370
|
+
assert.equal(restored.committedOnly, true);
|
|
1371
|
+
assert.equal(views.resolveProjection("historical-commit-range", cwd).providerBaseTree, undefined);
|
|
1372
|
+
});
|
|
1373
|
+
|
|
1374
|
+
test("facade STATUS refuses an unreachable base tree and a false staged-index kind", async (t) => {
|
|
1375
|
+
const cwd = repository(t);
|
|
1376
|
+
const currentTree = execFileSync("git", ["rev-parse", "HEAD^{tree}"], { cwd, encoding: "utf8" }).trim();
|
|
1377
|
+
const unreachableTree = execFileSync("git", ["mktree"], { cwd, input: "", encoding: "utf8" }).trim();
|
|
1378
|
+
assert.notEqual(unreachableTree, currentTree);
|
|
1379
|
+
const unknown = status("unprovable-tree");
|
|
1380
|
+
unknown.projection = { ...unknown.projection, projection: "staged", baseTree: unreachableTree, initialReviewTree: currentTree, currentCandidateTree: currentTree, paths: ["tracked.txt"] };
|
|
1381
|
+
const views = new CandidateViewRegistry();
|
|
1382
|
+
t.after(() => views.cleanupAll());
|
|
1383
|
+
await __testing.executeReviewControllerOperation({ operation: "status", lineageId: "unprovable-tree" }, cwd, { targetStatus: async () => unknown } as unknown as NativeReviewCli, undefined, views);
|
|
1384
|
+
assert.equal(views.hasProjection("unprovable-tree", cwd), false);
|
|
1385
|
+
assert.throws(() => views.resolveCurrentForLens("review-risk", cwd), (error: unknown) => error instanceof CandidateViewError && error.reason === "current-binding-hydration-failed");
|
|
1386
|
+
|
|
1387
|
+
writeFileSync(join(cwd, "tracked.txt"), "staged index candidate\n");
|
|
1388
|
+
execFileSync("git", ["add", "tracked.txt"], { cwd });
|
|
1389
|
+
const stagedTree = execFileSync("git", ["write-tree"], { cwd, encoding: "utf8" }).trim();
|
|
1390
|
+
writeFileSync(join(cwd, "tracked.txt"), "uncommitted workspace content\n");
|
|
1391
|
+
const falseKind = status("false-staged-index");
|
|
1392
|
+
falseKind.projection = { ...falseKind.projection, projection: "staged", baseTree: currentTree, initialReviewTree: stagedTree, currentCandidateTree: stagedTree, paths: ["tracked.txt"] };
|
|
1393
|
+
const otherViews = new CandidateViewRegistry();
|
|
1394
|
+
t.after(() => otherViews.cleanupAll());
|
|
1395
|
+
await __testing.executeReviewControllerOperation({ operation: "status", lineageId: "false-staged-index" }, cwd, { targetStatus: async () => falseKind } as unknown as NativeReviewCli, undefined, otherViews);
|
|
1396
|
+
assert.equal(otherViews.hasProjection("false-staged-index", cwd), false);
|
|
1397
|
+
assert.throws(() => otherViews.resolveCurrentForLens("review-risk", cwd), (error: unknown) => error instanceof CandidateViewError && error.reason === "current-binding-hydration-failed" && error.message.includes("candidate-view-invalid"));
|
|
1398
|
+
});
|
|
1399
|
+
|
|
1400
|
+
test("provider tree adoption refuses drifted targets, malformed offers and explicit caller trees", async (t) => {
|
|
1401
|
+
const cwd = repository(t);
|
|
1402
|
+
execFileSync("git", ["add", "tracked.txt"], { cwd });
|
|
1403
|
+
execFileSync("git", ["-c", "user.name=Routing Test", "-c", "user.email=routing@example.invalid", "commit", "-m", "candidate"], { cwd });
|
|
1404
|
+
const baseCommit = execFileSync("git", ["rev-parse", "HEAD~1"], { cwd, encoding: "utf8" }).trim();
|
|
1405
|
+
const baseTree = execFileSync("git", ["rev-parse", `${baseCommit}^{tree}`], { cwd, encoding: "utf8" }).trim();
|
|
1406
|
+
const adopted = startStatus(cwd, baseCommit);
|
|
1407
|
+
const offered = startStatus(cwd);
|
|
1408
|
+
offered.nextTransition = { kind: "execute", reasonCode: "start_required", execute: { operation: "review.start", arguments: [{ name: "base-ref", value: baseTree }, { name: "committed-only", value: "true" }], preconditions: [], binding: {} } } as unknown as ReviewStatusV3["nextTransition"];
|
|
1409
|
+
const drifted = startStatus(cwd);
|
|
1410
|
+
const malformed = startStatus(cwd);
|
|
1411
|
+
malformed.nextTransition = { kind: "execute", reasonCode: "start_required", execute: { operation: "review.start", arguments: [{ name: "base-ref", value: baseTree }, { name: "base-ref", value: baseTree }, { name: "committed-only", value: "true" }], preconditions: [], binding: {} } } as unknown as ReviewStatusV3["nextTransition"];
|
|
1412
|
+
const malformedExtra = startStatus(cwd);
|
|
1413
|
+
malformedExtra.nextTransition = { kind: "execute", reasonCode: "start_required", execute: { operation: "review.start", arguments: [
|
|
1414
|
+
{ name: "agent", value: "pi" }, { name: "base-ref", value: baseTree, token: `--base-ref=${baseTree}` },
|
|
1415
|
+
{ name: "committed-only", value: "true", token: "--committed-only=true" }, { name: "committed-only", value: "false" },
|
|
1416
|
+
], preconditions: [], binding: {} } } as unknown as ReviewStatusV3["nextTransition"];
|
|
1417
|
+
const malformedToken = startStatus(cwd);
|
|
1418
|
+
malformedToken.nextTransition = { kind: "execute", reasonCode: "start_required", execute: { operation: "review.start", arguments: [
|
|
1419
|
+
{ name: "agent", value: "pi" }, { name: "base-ref", value: baseTree, token: "--base-ref=HEAD" },
|
|
1420
|
+
{ name: "committed-only", value: "true", token: "--committed-only=true" },
|
|
1421
|
+
], preconditions: [], binding: {} } } as unknown as ReviewStatusV3["nextTransition"];
|
|
1422
|
+
const blob = execFileSync("git", ["rev-parse", "HEAD:tracked.txt"], { cwd, encoding: "utf8" }).trim();
|
|
1423
|
+
const wrongType = startStatus(cwd);
|
|
1424
|
+
wrongType.nextTransition = { kind: "execute", reasonCode: "start_required", execute: { operation: "review.start", arguments: [{ name: "base-ref", value: blob }, { name: "committed-only", value: "true" }], preconditions: [], binding: {} } } as unknown as ReviewStatusV3["nextTransition"];
|
|
1425
|
+
for (const scenario of [
|
|
1426
|
+
{ name: "changed tree target", first: offered, second: drifted, expectedReads: 2 },
|
|
1427
|
+
{ name: "malformed duplicate offer", first: malformed, second: adopted, expectedReads: 1 },
|
|
1428
|
+
{ name: "conflicting selector among unrelated arguments", first: malformedExtra, second: adopted, expectedReads: 1 },
|
|
1429
|
+
{ name: "selector token disagrees with value", first: malformedToken, second: adopted, expectedReads: 1 },
|
|
1430
|
+
{ name: "wrong object type", first: wrongType, second: adopted, expectedReads: 2 },
|
|
1431
|
+
]) {
|
|
1432
|
+
let reads = 0, starts = 0;
|
|
1433
|
+
const views = new CandidateViewRegistry();
|
|
1434
|
+
t.after(() => views.cleanupAll());
|
|
1435
|
+
const native = { targetStatus: async () => (++reads === 1 ? scenario.first : scenario.second), start: async () => { starts++; throw new Error("native START must not run"); } } as unknown as NativeReviewCli;
|
|
1436
|
+
const result = await __testing.executeReviewControllerOperation({ operation: "start", input: JSON.stringify({ mode: "ordinary" }) }, cwd, native, undefined, views);
|
|
1437
|
+
assert.equal(reads, scenario.expectedReads, scenario.name);
|
|
1438
|
+
assert.equal(starts, 0, scenario.name);
|
|
1439
|
+
assert.equal(result.status, "blocked", scenario.name);
|
|
1440
|
+
}
|
|
1441
|
+
let reads = 0, starts = 0;
|
|
1442
|
+
const native = { targetStatus: async () => { reads++; return offered; }, start: async () => { starts++; throw new Error("native START must not run"); } } as unknown as NativeReviewCli;
|
|
1443
|
+
const rejected = await __testing.executeReviewControllerOperation({ operation: "start", input: JSON.stringify({ mode: "ordinary", baseRef: baseTree, committedOnly: true }) }, cwd, native);
|
|
1444
|
+
assert.equal(rejected.outcome, "native-start-base-ref-unresolvable");
|
|
1445
|
+
assert.equal(reads, 0);
|
|
1446
|
+
assert.equal(starts, 0);
|
|
1447
|
+
});
|
|
1448
|
+
|
|
1048
1449
|
test("ordinary START preserves sanitized foreign diagnostics through one ambiguous reconciliation", async (t) => {
|
|
1049
1450
|
const cwd = repository(t);
|
|
1050
1451
|
const target = startStatus(cwd);
|
|
@@ -1160,6 +1561,60 @@ for (const statusSchema of ["gentle-ai.review-integration.status/v6", "gentle-ai
|
|
|
1160
1561
|
assert.equal(requests.every((request) => !("lineageId" in request)), true);
|
|
1161
1562
|
});
|
|
1162
1563
|
|
|
1564
|
+
test("committed-range inspect stop carries its binding selector into selection STATUS and START, rejecting a mismatched binding", async (t) => {
|
|
1565
|
+
const { cwd, eligible, selection } = untrackedStopFixture(t);
|
|
1566
|
+
const baseRef = execFileSync("git", ["rev-parse", "HEAD"], { cwd, encoding: "utf8" }).trim();
|
|
1567
|
+
const initialTarget = startStatus(cwd, baseRef), target = startStatus(cwd, baseRef, [eligible]);
|
|
1568
|
+
const boundSelection = { ...selection, arguments: selection.arguments.map((arg) => arg.name === "base_tree" ? { ...arg, value: initialTarget.projection.baseTree } : arg.name === "candidate_tree" ? { ...arg, value: initialTarget.projection.currentCandidateTree } : arg) };
|
|
1569
|
+
const initial = { ...initialTarget, nextTransition: { kind: "collect", reasonCode: "intended_untracked_selection_required", collect: { inputs: [boundSelection] } }, raw: { schema: "gentle-ai.review-integration.status/v7" } } as ReviewStatusV3;
|
|
1570
|
+
const requests: Array<Record<string, unknown>> = [], starts: Array<Record<string, unknown>> = [], retained = new Map();
|
|
1571
|
+
const native = {
|
|
1572
|
+
targetStatus: async (request: Record<string, unknown>) => { requests.push(request); return "intendedUntrackedSelection" in request ? target : initial; },
|
|
1573
|
+
start: async (request: Record<string, unknown>) => { starts.push(request); return { lineageId: "review-selected", state: "reviewing", riskLevel: "low", selectedLenses: [], changedFiles: 1, changedLines: 1, correctionBudget: 1, action: "created", lensesRequired: false, riskReasons: [], raw: {} }; },
|
|
1574
|
+
} as unknown as NativeReviewCli;
|
|
1575
|
+
const inspected = await __testing.executeReviewControllerOperation({ operation: "inspect", input: JSON.stringify({ baseRef: "HEAD", committedOnly: true }) }, cwd, native, undefined, undefined, undefined, retained);
|
|
1576
|
+
assert.equal(typeof inspected.selectionBinding, "string");
|
|
1577
|
+
assert.equal(retained.has(`${cwd}\u0000`), true, "plain inspect stop retains its selector");
|
|
1578
|
+
const mismatch = await __testing.executeReviewControllerOperation({ operation: "select-intended-untracked", selectionBinding: (inspected.selectionBinding as string).replace(eligible, "other.md"), intendedUntracked: [eligible] }, cwd, native, undefined, undefined, undefined, retained);
|
|
1579
|
+
assert.equal(mismatch.outcome, "intended-untracked-selection-binding-rejected");
|
|
1580
|
+
assert.equal(requests.length, 1, "mismatched binding must not request another STATUS");
|
|
1581
|
+
assert.equal(starts.length, 0);
|
|
1582
|
+
const selected = await __testing.executeReviewControllerOperation({ operation: "select-intended-untracked", selectionBinding: inspected.selectionBinding, intendedUntracked: [eligible] }, cwd, native, undefined, undefined, undefined, retained);
|
|
1583
|
+
assert.equal(selected.operation, "select-intended-untracked");
|
|
1584
|
+
assert.equal(starts.length, 1, JSON.stringify(selected));
|
|
1585
|
+
assert.equal(requests.length, 3);
|
|
1586
|
+
for (const request of requests) { assert.equal(request.baseRef, baseRef); assert.equal(request.committedOnly, true); }
|
|
1587
|
+
assert.equal(starts[0]!.baseRef, baseRef);
|
|
1588
|
+
assert.equal(starts[0]!.committedOnly, true);
|
|
1589
|
+
});
|
|
1590
|
+
|
|
1591
|
+
test("committed-range selection rejects target-identity drift before START", async (t) => {
|
|
1592
|
+
const { cwd, eligible, selection } = untrackedStopFixture(t);
|
|
1593
|
+
const baseRef = execFileSync("git", ["rev-parse", "HEAD"], { cwd, encoding: "utf8" }).trim();
|
|
1594
|
+
const initialTarget = startStatus(cwd, baseRef);
|
|
1595
|
+
const boundSelection = { ...selection, arguments: selection.arguments.map((arg) => arg.name === "base_tree" ? { ...arg, value: initialTarget.projection.baseTree } : arg.name === "candidate_tree" ? { ...arg, value: initialTarget.projection.currentCandidateTree } : arg) };
|
|
1596
|
+
const initial = { ...initialTarget, nextTransition: { kind: "collect", reasonCode: "intended_untracked_selection_required", collect: { inputs: [boundSelection] } }, raw: { schema: "gentle-ai.review-integration.status/v7" } } as ReviewStatusV3;
|
|
1597
|
+
const changed = { ...initial, targetIdentity: `sha256:${"d".repeat(64)}` } as ReviewStatusV3;
|
|
1598
|
+
const requests: Array<Record<string, unknown>> = [];
|
|
1599
|
+
let starts = 0;
|
|
1600
|
+
const retained = new Map();
|
|
1601
|
+
const native = {
|
|
1602
|
+
targetStatus: async (request: Record<string, unknown>) => { requests.push(request); return requests.length === 1 ? initial : changed; },
|
|
1603
|
+
start: async () => { starts++; throw new Error("START must not run for a changed target"); },
|
|
1604
|
+
} as unknown as NativeReviewCli;
|
|
1605
|
+
const inspected = await __testing.executeReviewControllerOperation({ operation: "inspect", input: JSON.stringify({ baseRef: "HEAD", committedOnly: true }) }, cwd, native, undefined, undefined, undefined, retained);
|
|
1606
|
+
assert.equal(typeof inspected.selectionBinding, "string");
|
|
1607
|
+
const rejected = await __testing.executeReviewControllerOperation({ operation: "select-intended-untracked", selectionBinding: inspected.selectionBinding, intendedUntracked: [eligible] }, cwd, native, undefined, undefined, undefined, retained);
|
|
1608
|
+
assert.equal(rejected.status, "blocked");
|
|
1609
|
+
assert.equal(rejected.outcome, "intended-untracked-selection-binding-rejected");
|
|
1610
|
+
assert.equal(rejected.mutation_performed, false);
|
|
1611
|
+
assert.equal(rejected.mutation_outcome, "none");
|
|
1612
|
+
assert.equal(requests.length, 2);
|
|
1613
|
+
assert.equal(requests[1]!.baseRef, baseRef);
|
|
1614
|
+
assert.equal(requests[1]!.committedOnly, true);
|
|
1615
|
+
assert.equal(starts, 0);
|
|
1616
|
+
});
|
|
1617
|
+
|
|
1163
1618
|
// gentle-pi#706: inspect names the exact continuation for the intended-untracked
|
|
1164
1619
|
// stop and can resolve it in one call through top-level untrackedScope.
|
|
1165
1620
|
function untrackedStopFixture(
|
|
@@ -1467,6 +1922,7 @@ test("plain START adopts the pre-lineage selection retained by inspect and delet
|
|
|
1467
1922
|
retained,
|
|
1468
1923
|
);
|
|
1469
1924
|
assert.equal(started.operation, "start");
|
|
1925
|
+
assert.ok(typeof started.result === "object" && started.result !== null && "lineage_id" in started.result);
|
|
1470
1926
|
assert.equal(started.result.lineage_id, "review-started");
|
|
1471
1927
|
assert.equal(starts.length, 1);
|
|
1472
1928
|
assert.equal(requests.length, 3);
|
|
@@ -1562,6 +2018,300 @@ test("a fresh unsuccessful inspect invalidates its prior pre-lineage selection",
|
|
|
1562
2018
|
assert.equal(retained.has(`${cwd}\u0000`), false);
|
|
1563
2019
|
});
|
|
1564
2020
|
|
|
2021
|
+
// gentle-pi#1192: a committed-range inspect's retained pre-lineage selection
|
|
2022
|
+
// must remember its own baseRef/committedOnly too, so a later plain START
|
|
2023
|
+
// replays the same committed range instead of falling back to the
|
|
2024
|
+
// workspace-diff one.
|
|
2025
|
+
test("inspect with a committed-range selector retains baseRef/committedOnly, and a plain START adopts it", async (t) => {
|
|
2026
|
+
const cwd = repository(t), eligible = "selected.md";
|
|
2027
|
+
writeFileSync(join(cwd, eligible), "selected\n");
|
|
2028
|
+
const baseRef = execFileSync("git", ["rev-parse", "HEAD"], { cwd, encoding: "utf8" }).trim();
|
|
2029
|
+
// The inspect handler carries its committed-range selector through both the
|
|
2030
|
+
// pre-resolution and resolution STATUS calls, so a faithful fixture for
|
|
2031
|
+
// this scenario must project the committed range on both, not the
|
|
2032
|
+
// workspace-diff range untrackedStopFixture builds.
|
|
2033
|
+
const initialTarget = startStatus(cwd, baseRef),
|
|
2034
|
+
target = startStatus(cwd, baseRef, [eligible]);
|
|
2035
|
+
const selection: ReviewCollectInputV3 = {
|
|
2036
|
+
name: "intended_untracked_selection",
|
|
2037
|
+
schema: "gentle-ai.review-intended-untracked-selection/v1",
|
|
2038
|
+
captureOperation: "external.select_intended_untracked",
|
|
2039
|
+
arguments: [
|
|
2040
|
+
{ name: "target_identity", value: SHA },
|
|
2041
|
+
{ name: "projection", value: "workspace" },
|
|
2042
|
+
{ name: "base_tree", value: initialTarget.projection.baseTree },
|
|
2043
|
+
{ name: "candidate_tree", value: initialTarget.projection.currentCandidateTree },
|
|
2044
|
+
{ name: "eligible_paths_json", value: JSON.stringify([eligible]) },
|
|
2045
|
+
{ name: "expected_untracked_inventory", value: SHA },
|
|
2046
|
+
],
|
|
2047
|
+
submission: {
|
|
2048
|
+
operationToken: "status",
|
|
2049
|
+
argumentTokens: [
|
|
2050
|
+
"--contract=gentle-ai.review-integration/v2",
|
|
2051
|
+
"--next-transition=true",
|
|
2052
|
+
"--agent=pi",
|
|
2053
|
+
"--projection=workspace",
|
|
2054
|
+
"--intended-untracked-selection={{value}}",
|
|
2055
|
+
],
|
|
2056
|
+
values: [
|
|
2057
|
+
{
|
|
2058
|
+
slot: "intended_untracked_selection",
|
|
2059
|
+
domain: "schema_bound_json",
|
|
2060
|
+
schema: "gentle-ai.review-intended-untracked-selection/v1",
|
|
2061
|
+
substitutionLocation: 4,
|
|
2062
|
+
},
|
|
2063
|
+
],
|
|
2064
|
+
},
|
|
2065
|
+
};
|
|
2066
|
+
const initial = {
|
|
2067
|
+
...initialTarget,
|
|
2068
|
+
nextTransition: {
|
|
2069
|
+
kind: "collect",
|
|
2070
|
+
reasonCode: "intended_untracked_selection_required",
|
|
2071
|
+
collect: { inputs: [selection] },
|
|
2072
|
+
},
|
|
2073
|
+
raw: { schema: "gentle-ai.review-integration.status/v7" },
|
|
2074
|
+
} as ReviewStatusV3;
|
|
2075
|
+
const requests: Array<Record<string, unknown>> = [],
|
|
2076
|
+
starts: Array<Record<string, unknown>> = [],
|
|
2077
|
+
retained = new Map();
|
|
2078
|
+
const native = {
|
|
2079
|
+
targetStatus: async (request: Record<string, unknown>) => {
|
|
2080
|
+
requests.push(request);
|
|
2081
|
+
return "intendedUntrackedSelection" in request ? target : initial;
|
|
2082
|
+
},
|
|
2083
|
+
start: async (request: Record<string, unknown>) => {
|
|
2084
|
+
starts.push(request);
|
|
2085
|
+
return {
|
|
2086
|
+
lineageId: "review-started", state: "reviewing", riskLevel: "low", selectedLenses: [],
|
|
2087
|
+
changedFiles: 1, changedLines: 1, correctionBudget: 1, action: "created", lensesRequired: false,
|
|
2088
|
+
riskReasons: [], raw: {},
|
|
2089
|
+
};
|
|
2090
|
+
},
|
|
2091
|
+
} as unknown as NativeReviewCli;
|
|
2092
|
+
const resolved = await __testing.executeReviewControllerOperation(
|
|
2093
|
+
{
|
|
2094
|
+
operation: "inspect",
|
|
2095
|
+
input: JSON.stringify({ baseRef: "HEAD", committedOnly: true }),
|
|
2096
|
+
untrackedScope: "select",
|
|
2097
|
+
intendedUntracked: [eligible],
|
|
2098
|
+
},
|
|
2099
|
+
cwd, native, undefined, undefined, undefined, retained,
|
|
2100
|
+
);
|
|
2101
|
+
assert.equal(resolved.status, "ready");
|
|
2102
|
+
const retainedEntry = retained.get(`${cwd}\u0000`) as { baseRef?: string; committedOnly?: true } | undefined;
|
|
2103
|
+
assert.notEqual(retainedEntry, undefined);
|
|
2104
|
+
assert.equal(retainedEntry!.baseRef, baseRef);
|
|
2105
|
+
assert.equal(retainedEntry!.committedOnly, true);
|
|
2106
|
+
|
|
2107
|
+
const started = await __testing.executeReviewControllerOperation(
|
|
2108
|
+
{ operation: "start", input: JSON.stringify({ mode: "ordinary" }) },
|
|
2109
|
+
cwd, native, undefined, undefined, undefined, retained,
|
|
2110
|
+
);
|
|
2111
|
+
assert.equal(started.operation, "start");
|
|
2112
|
+
assert.equal((started.result as { lineage_id?: string }).lineage_id, "review-started");
|
|
2113
|
+
assert.equal(requests.length, 3);
|
|
2114
|
+
const startStatusRequest = requests[2]!;
|
|
2115
|
+
assert.equal(startStatusRequest.baseRef, baseRef);
|
|
2116
|
+
assert.equal(startStatusRequest.committedOnly, true);
|
|
2117
|
+
assert.equal(startStatusRequest.untrackedScope, "select");
|
|
2118
|
+
assert.deepEqual(startStatusRequest.intendedUntrackedSelection, {
|
|
2119
|
+
argumentTokens: selection.submission!.argumentTokens,
|
|
2120
|
+
value: JSON.stringify({
|
|
2121
|
+
schema: "gentle-ai.review-intended-untracked-selection/v1",
|
|
2122
|
+
untracked_scope: "select",
|
|
2123
|
+
expected_untracked_inventory: SHA,
|
|
2124
|
+
intended_untracked: [eligible],
|
|
2125
|
+
}),
|
|
2126
|
+
});
|
|
2127
|
+
assert.equal(starts.length, 1);
|
|
2128
|
+
assert.equal(starts[0]!.baseRef, baseRef);
|
|
2129
|
+
assert.equal(starts[0]!.committedOnly, true);
|
|
2130
|
+
assert.equal(starts[0]!.untrackedScope, "select");
|
|
2131
|
+
assert.equal(retained.has(`${cwd}\u0000`), false);
|
|
2132
|
+
const lineageEntry = retained.get(`${cwd}\u0000review-started`) as { untrackedScope: string; baseRef?: string } | undefined;
|
|
2133
|
+
assert.notEqual(lineageEntry, undefined);
|
|
2134
|
+
assert.equal(lineageEntry!.untrackedScope, "select");
|
|
2135
|
+
// The lineage-scoped entry must stay a plain untracked selection: a
|
|
2136
|
+
// leftover baseRef field would make readRetainedNativeUntrackedSelection
|
|
2137
|
+
// mistake it for a RetainedNativeCaptureRoute.
|
|
2138
|
+
assert.equal("baseRef" in lineageEntry!, false);
|
|
2139
|
+
});
|
|
2140
|
+
|
|
2141
|
+
// gentle-pi#review-ready-inspect-selector: a plain inspect that resolves ready
|
|
2142
|
+
// without ever needing an untrackedScope decision must still retain its own
|
|
2143
|
+
// committed-range selector, so the following plain START replays the exact
|
|
2144
|
+
// inspected range instead of adopting the native default base-ref (#874).
|
|
2145
|
+
test("a plain ready inspect retains its committed selector, and a plain START adopts it", async (t) => {
|
|
2146
|
+
const cwd = repository(t);
|
|
2147
|
+
const baseRef = execFileSync("git", ["rev-parse", "HEAD"], { cwd, encoding: "utf8" }).trim();
|
|
2148
|
+
const target = startStatus(cwd, baseRef);
|
|
2149
|
+
const requests: Array<Record<string, unknown>> = [],
|
|
2150
|
+
starts: Array<Record<string, unknown>> = [],
|
|
2151
|
+
retained = new Map();
|
|
2152
|
+
const native = {
|
|
2153
|
+
targetStatus: async (request: Record<string, unknown>) => { requests.push(request); return target; },
|
|
2154
|
+
start: async (request: Record<string, unknown>) => {
|
|
2155
|
+
starts.push(request);
|
|
2156
|
+
return {
|
|
2157
|
+
lineageId: "ready-started", state: "reviewing", riskLevel: "low", selectedLenses: [],
|
|
2158
|
+
changedFiles: 1, changedLines: 1, correctionBudget: 1, action: "created", lensesRequired: false,
|
|
2159
|
+
riskReasons: [], raw: {},
|
|
2160
|
+
};
|
|
2161
|
+
},
|
|
2162
|
+
} as unknown as NativeReviewCli;
|
|
2163
|
+
const inspected = await __testing.executeReviewControllerOperation(
|
|
2164
|
+
{ operation: "inspect", input: JSON.stringify({ baseRef: "HEAD", committedOnly: true }) },
|
|
2165
|
+
cwd, native, undefined, undefined, undefined, retained,
|
|
2166
|
+
);
|
|
2167
|
+
assert.equal(inspected.status, "ready");
|
|
2168
|
+
const retainedEntry = retained.get(`${cwd}\u0000`) as { baseRef?: string; committedOnly?: true } | undefined;
|
|
2169
|
+
assert.notEqual(retainedEntry, undefined);
|
|
2170
|
+
assert.equal(retainedEntry!.baseRef, baseRef);
|
|
2171
|
+
assert.equal(retainedEntry!.committedOnly, true);
|
|
2172
|
+
|
|
2173
|
+
const started = await __testing.executeReviewControllerOperation(
|
|
2174
|
+
{ operation: "start", input: JSON.stringify({ mode: "ordinary" }) },
|
|
2175
|
+
cwd, native, undefined, undefined, undefined, retained,
|
|
2176
|
+
);
|
|
2177
|
+
assert.equal(started.operation, "start");
|
|
2178
|
+
assert.equal((started.result as { lineage_id?: string }).lineage_id, "ready-started");
|
|
2179
|
+
assert.equal(requests.length, 2);
|
|
2180
|
+
const startStatusRequest = requests[1]!;
|
|
2181
|
+
assert.equal(startStatusRequest.baseRef, baseRef);
|
|
2182
|
+
assert.equal(startStatusRequest.committedOnly, true);
|
|
2183
|
+
assert.equal(starts.length, 1);
|
|
2184
|
+
assert.equal(starts[0]!.baseRef, baseRef);
|
|
2185
|
+
assert.equal(starts[0]!.committedOnly, true);
|
|
2186
|
+
assert.equal(retained.has(`${cwd}\u0000`), false);
|
|
2187
|
+
});
|
|
2188
|
+
|
|
2189
|
+
test("a later ready inspect with a different baseRef replaces the earlier retained selector", async (t) => {
|
|
2190
|
+
const cwd = repository(t);
|
|
2191
|
+
const baseRef1 = execFileSync("git", ["rev-parse", "HEAD"], { cwd, encoding: "utf8" }).trim();
|
|
2192
|
+
const target1 = startStatus(cwd, baseRef1);
|
|
2193
|
+
execFileSync("git", ["add", "tracked.txt"], { cwd, stdio: "ignore" });
|
|
2194
|
+
execFileSync("git", ["-c", "user.name=Routing Test", "-c", "user.email=routing@example.invalid", "commit", "-m", "second"], { cwd, stdio: "ignore" });
|
|
2195
|
+
const baseRef2 = execFileSync("git", ["rev-parse", "HEAD"], { cwd, encoding: "utf8" }).trim();
|
|
2196
|
+
const target2 = startStatus(cwd, baseRef2);
|
|
2197
|
+
const starts: Array<Record<string, unknown>> = [],
|
|
2198
|
+
retained = new Map();
|
|
2199
|
+
const native = {
|
|
2200
|
+
targetStatus: async (request: Record<string, unknown>) => (request.baseRef === baseRef2 ? target2 : target1),
|
|
2201
|
+
start: async (request: Record<string, unknown>) => {
|
|
2202
|
+
starts.push(request);
|
|
2203
|
+
return {
|
|
2204
|
+
lineageId: "second-started", state: "reviewing", riskLevel: "low", selectedLenses: [],
|
|
2205
|
+
changedFiles: 1, changedLines: 1, correctionBudget: 1, action: "created", lensesRequired: false,
|
|
2206
|
+
riskReasons: [], raw: {},
|
|
2207
|
+
};
|
|
2208
|
+
},
|
|
2209
|
+
} as unknown as NativeReviewCli;
|
|
2210
|
+
await __testing.executeReviewControllerOperation(
|
|
2211
|
+
{ operation: "inspect", input: JSON.stringify({ baseRef: baseRef1, committedOnly: true }) },
|
|
2212
|
+
cwd, native, undefined, undefined, undefined, retained,
|
|
2213
|
+
);
|
|
2214
|
+
await __testing.executeReviewControllerOperation(
|
|
2215
|
+
{ operation: "inspect", input: JSON.stringify({ baseRef: baseRef2, committedOnly: true }) },
|
|
2216
|
+
cwd, native, undefined, undefined, undefined, retained,
|
|
2217
|
+
);
|
|
2218
|
+
const retainedEntry = retained.get(`${cwd}\u0000`) as { baseRef?: string } | undefined;
|
|
2219
|
+
assert.equal(retainedEntry?.baseRef, baseRef2);
|
|
2220
|
+
|
|
2221
|
+
const started = await __testing.executeReviewControllerOperation(
|
|
2222
|
+
{ operation: "start", input: JSON.stringify({ mode: "ordinary" }) },
|
|
2223
|
+
cwd, native, undefined, undefined, undefined, retained,
|
|
2224
|
+
);
|
|
2225
|
+
assert.equal((started.result as { lineage_id?: string }).lineage_id, "second-started");
|
|
2226
|
+
assert.equal(starts.length, 1);
|
|
2227
|
+
assert.equal(starts[0]!.baseRef, baseRef2);
|
|
2228
|
+
});
|
|
2229
|
+
|
|
2230
|
+
test("START rejects a ready-inspect retained selector when fresh STATUS identifies a changed candidate", async (t) => {
|
|
2231
|
+
const cwd = repository(t);
|
|
2232
|
+
const baseRef = execFileSync("git", ["rev-parse", "HEAD"], { cwd, encoding: "utf8" }).trim();
|
|
2233
|
+
const candidateA = startStatus(cwd, baseRef);
|
|
2234
|
+
const candidateB = {
|
|
2235
|
+
...candidateA,
|
|
2236
|
+
targetIdentity: "target-b",
|
|
2237
|
+
projection: { ...candidateA.projection, currentCandidateTree: "candidate-b" },
|
|
2238
|
+
} as ReviewStatusV3;
|
|
2239
|
+
const retained = new Map();
|
|
2240
|
+
let statusCalls = 0;
|
|
2241
|
+
let starts = 0;
|
|
2242
|
+
const native = {
|
|
2243
|
+
targetStatus: async () => { statusCalls += 1; return statusCalls === 1 ? candidateA : candidateB; },
|
|
2244
|
+
start: async () => {
|
|
2245
|
+
starts += 1;
|
|
2246
|
+
return {
|
|
2247
|
+
lineageId: "unexpected", state: "reviewing", riskLevel: "low", selectedLenses: [],
|
|
2248
|
+
changedFiles: 1, changedLines: 1, correctionBudget: 1, action: "created", lensesRequired: false,
|
|
2249
|
+
riskReasons: [], raw: {},
|
|
2250
|
+
};
|
|
2251
|
+
},
|
|
2252
|
+
} as unknown as NativeReviewCli;
|
|
2253
|
+
await __testing.executeReviewControllerOperation(
|
|
2254
|
+
{ operation: "inspect", input: JSON.stringify({ baseRef: "HEAD", committedOnly: true }) },
|
|
2255
|
+
cwd, native, undefined, null, undefined, retained,
|
|
2256
|
+
);
|
|
2257
|
+
const rejected = await __testing.executeReviewControllerOperation(
|
|
2258
|
+
{ operation: "start", input: JSON.stringify({ mode: "ordinary" }) },
|
|
2259
|
+
cwd, native, undefined, null, undefined, retained,
|
|
2260
|
+
);
|
|
2261
|
+
assert.equal(rejected.outcome, "native-start-retained-selection-candidate-mismatch");
|
|
2262
|
+
assert.equal(starts, 0);
|
|
2263
|
+
assert.equal(retained.has(`${cwd}\u0000`), false);
|
|
2264
|
+
});
|
|
2265
|
+
|
|
2266
|
+
test("an explicit baseRef that conflicts with a retained committed-range selector is not adopted", async (t) => {
|
|
2267
|
+
const { cwd, eligible, initial, target } = untrackedStopFixture(t);
|
|
2268
|
+
const headRef = execFileSync("git", ["rev-parse", "HEAD"], { cwd, encoding: "utf8" }).trim();
|
|
2269
|
+
writeFileSync(join(cwd, "tracked.txt"), "second\n");
|
|
2270
|
+
execFileSync("git", ["add", "tracked.txt"], { cwd, stdio: "ignore" });
|
|
2271
|
+
execFileSync("git", ["-c", "user.name=Routing Test", "-c", "user.email=routing@example.invalid", "commit", "-m", "second"], { cwd, stdio: "ignore" });
|
|
2272
|
+
const otherRef = execFileSync("git", ["rev-parse", "HEAD"], { cwd, encoding: "utf8" }).trim();
|
|
2273
|
+
const readyTarget = startStatus(cwd, otherRef);
|
|
2274
|
+
const starts: Array<Record<string, unknown>> = [],
|
|
2275
|
+
retained = new Map();
|
|
2276
|
+
const native = {
|
|
2277
|
+
targetStatus: async (request: Record<string, unknown>) =>
|
|
2278
|
+
request.baseRef === otherRef ? readyTarget : ("intendedUntrackedSelection" in request ? target : initial),
|
|
2279
|
+
start: async (request: Record<string, unknown>) => {
|
|
2280
|
+
starts.push(request);
|
|
2281
|
+
return {
|
|
2282
|
+
lineageId: "review-started", state: "reviewing", riskLevel: "low", selectedLenses: [],
|
|
2283
|
+
changedFiles: 1, changedLines: 1, correctionBudget: 1, action: "created", lensesRequired: false,
|
|
2284
|
+
riskReasons: [], raw: {},
|
|
2285
|
+
};
|
|
2286
|
+
},
|
|
2287
|
+
} as unknown as NativeReviewCli;
|
|
2288
|
+
await __testing.executeReviewControllerOperation(
|
|
2289
|
+
{
|
|
2290
|
+
operation: "inspect",
|
|
2291
|
+
input: JSON.stringify({ baseRef: headRef, committedOnly: true }),
|
|
2292
|
+
untrackedScope: "select",
|
|
2293
|
+
intendedUntracked: [eligible],
|
|
2294
|
+
},
|
|
2295
|
+
cwd, native, undefined, undefined, undefined, retained,
|
|
2296
|
+
);
|
|
2297
|
+
const retainedEntry = retained.get(`${cwd}\u0000`) as { baseRef?: string } | undefined;
|
|
2298
|
+
assert.equal(retainedEntry?.baseRef, headRef);
|
|
2299
|
+
|
|
2300
|
+
const started = await __testing.executeReviewControllerOperation(
|
|
2301
|
+
{ operation: "start", input: JSON.stringify({ mode: "ordinary", baseRef: otherRef, committedOnly: true }) },
|
|
2302
|
+
cwd, native, undefined, undefined, undefined, retained,
|
|
2303
|
+
);
|
|
2304
|
+
assert.equal(started.operation, "start");
|
|
2305
|
+
assert.equal(starts.length, 1);
|
|
2306
|
+
assert.equal(starts[0]!.baseRef, otherRef);
|
|
2307
|
+
// The conflicting explicit START never adopts the retained scope/inventory.
|
|
2308
|
+
assert.equal("untrackedScope" in starts[0]!, false);
|
|
2309
|
+
// A successful START always clears the pre-lineage "" entry (gentle-pi#706),
|
|
2310
|
+
// whether or not it consumed it: a new lineage invalidates any stale
|
|
2311
|
+
// pre-lineage selection regardless of this START's own adoption choice.
|
|
2312
|
+
assert.equal(retained.has(`${cwd}\u0000`), false);
|
|
2313
|
+
});
|
|
2314
|
+
|
|
1565
2315
|
test("START rejects a retained pre-lineage selection when fresh STATUS identifies a changed candidate", async (t) => {
|
|
1566
2316
|
const { cwd, eligible, initial, target } = untrackedStopFixture(t);
|
|
1567
2317
|
const retained = new Map();
|
|
@@ -1628,6 +2378,7 @@ test("a failed START retains a pre-lineage selection for a same-candidate retry"
|
|
|
1628
2378
|
{ operation: "start", input: JSON.stringify({ mode: "ordinary" }) },
|
|
1629
2379
|
cwd, native, undefined, null, undefined, retained,
|
|
1630
2380
|
);
|
|
2381
|
+
assert.ok(typeof retried.result === "object" && retried.result !== null && "lineage_id" in retried.result);
|
|
1631
2382
|
assert.equal(retried.result.lineage_id, "retried");
|
|
1632
2383
|
assert.equal(starts, 2);
|
|
1633
2384
|
assert.equal(retained.has(`${cwd}\u0000`), false);
|
|
@@ -1833,6 +2584,115 @@ test("targeted-validator provider vectors preserve their nonuniform native closu
|
|
|
1833
2584
|
assert.equal("correction_target_identity" in result, false);
|
|
1834
2585
|
});
|
|
1835
2586
|
|
|
2587
|
+
test("approved capture closure envelope carries additive next_action alongside untouched raw acknowledgement", async () => {
|
|
2588
|
+
const closureLineage = "review-approved-closure-next-action";
|
|
2589
|
+
const input = {
|
|
2590
|
+
name: "provider_targeted_validator",
|
|
2591
|
+
schema: "https://gentle-ai.dev/schema/review/targeted-validator/v1",
|
|
2592
|
+
captureOperation: "review.capture-validation",
|
|
2593
|
+
arguments: [
|
|
2594
|
+
{ name: "lineage", value: closureLineage, token: `--lineage=${closureLineage}` },
|
|
2595
|
+
{ name: "target", value: SHA, token: `--target=${SHA}` },
|
|
2596
|
+
{ name: "agent", value: "pi", token: "--agent=pi" },
|
|
2597
|
+
{ name: "execute", value: "true", token: "--execute=true" },
|
|
2598
|
+
],
|
|
2599
|
+
} as unknown as ReviewCollectInputV3;
|
|
2600
|
+
const roleStatus = { ...status(closureLineage), nextTransition: { kind: "collect", reasonCode: "provider_role_required", collect: { inputs: [input] } } } as ReviewStatusV3;
|
|
2601
|
+
const ackContinuation = {
|
|
2602
|
+
operation: "review.acknowledge-approved",
|
|
2603
|
+
command: "gentle-ai review acknowledge-approved --provider-vector",
|
|
2604
|
+
arguments: [
|
|
2605
|
+
{ name: "cwd", value: process.cwd(), token: `--cwd=${process.cwd()}` },
|
|
2606
|
+
{ name: "lineage", value: closureLineage, token: `--lineage=${closureLineage}` },
|
|
2607
|
+
{ name: "target", value: SHA, token: `--target=${SHA}` },
|
|
2608
|
+
{ name: "expected-revision", value: SHA, token: `--expected-revision=${SHA}` },
|
|
2609
|
+
{ name: "token", value: "provider-tok", token: "--token=provider-tok" },
|
|
2610
|
+
],
|
|
2611
|
+
preconditions: [{ name: "state", value: "approved" }],
|
|
2612
|
+
binding: { lineageId: closureLineage, revision: SHA, targetIdentity: SHA },
|
|
2613
|
+
};
|
|
2614
|
+
const native = {
|
|
2615
|
+
targetStatus: async () => roleStatus,
|
|
2616
|
+
captureProviderRole: async () => ({
|
|
2617
|
+
schema: "gentle-ai.review-last-event-closure/v1",
|
|
2618
|
+
operation: "review/capture-validation",
|
|
2619
|
+
lineageId: closureLineage,
|
|
2620
|
+
state: "approved",
|
|
2621
|
+
action: "approved by validator",
|
|
2622
|
+
storeRevision: SHA,
|
|
2623
|
+
acknowledgement: {
|
|
2624
|
+
operation: "review.acknowledge-approved",
|
|
2625
|
+
command: "gentle-ai review acknowledge-approved --provider-vector",
|
|
2626
|
+
arguments: ackContinuation.arguments,
|
|
2627
|
+
preconditions: ackContinuation.preconditions,
|
|
2628
|
+
binding: ackContinuation.binding,
|
|
2629
|
+
raw: ackContinuation,
|
|
2630
|
+
},
|
|
2631
|
+
}),
|
|
2632
|
+
} as unknown as NativeReviewCli;
|
|
2633
|
+
const result = await __testing.executeReviewCaptureOperation({ lineageId: closureLineage, collectBinding: JSON.stringify(input) }, process.cwd(), native);
|
|
2634
|
+
assert.equal(result.status, "closed");
|
|
2635
|
+
assert.equal(result.outcome, "native-last-event-closure");
|
|
2636
|
+
assert.equal(result.next_action, `gentle_review {"operation":"acknowledge-approved","lineageId":"${closureLineage}"}`);
|
|
2637
|
+
assert.equal((result.closure as { next_action?: string }).next_action, `gentle_review {"operation":"acknowledge-approved","lineageId":"${closureLineage}"}`);
|
|
2638
|
+
assert.deepEqual((result.closure as { acknowledgement?: unknown }).acknowledgement, ackContinuation);
|
|
2639
|
+
});
|
|
2640
|
+
|
|
2641
|
+
test("approved capture closure envelope preserves workspaceRoot in next_action when distinct from process cwd", async (t) => {
|
|
2642
|
+
const closureLineage = "review-approved-closure-next-action-worktree";
|
|
2643
|
+
const worktreeRoot = repository(t);
|
|
2644
|
+
const input = {
|
|
2645
|
+
name: "provider_targeted_validator",
|
|
2646
|
+
schema: "https://gentle-ai.dev/schema/review/targeted-validator/v1",
|
|
2647
|
+
captureOperation: "review.capture-validation",
|
|
2648
|
+
arguments: [
|
|
2649
|
+
{ name: "lineage", value: closureLineage, token: `--lineage=${closureLineage}` },
|
|
2650
|
+
{ name: "target", value: SHA, token: `--target=${SHA}` },
|
|
2651
|
+
{ name: "agent", value: "pi", token: "--agent=pi" },
|
|
2652
|
+
{ name: "execute", value: "true", token: "--execute=true" },
|
|
2653
|
+
],
|
|
2654
|
+
} as unknown as ReviewCollectInputV3;
|
|
2655
|
+
const roleStatus = { ...status(closureLineage), nextTransition: { kind: "collect", reasonCode: "provider_role_required", collect: { inputs: [input] } } } as ReviewStatusV3;
|
|
2656
|
+
const ackContinuation = {
|
|
2657
|
+
operation: "review.acknowledge-approved",
|
|
2658
|
+
command: "gentle-ai review acknowledge-approved --provider-vector",
|
|
2659
|
+
arguments: [
|
|
2660
|
+
{ name: "cwd", value: worktreeRoot, token: `--cwd=${worktreeRoot}` },
|
|
2661
|
+
{ name: "lineage", value: closureLineage, token: `--lineage=${closureLineage}` },
|
|
2662
|
+
{ name: "target", value: SHA, token: `--target=${SHA}` },
|
|
2663
|
+
{ name: "expected-revision", value: SHA, token: `--expected-revision=${SHA}` },
|
|
2664
|
+
{ name: "token", value: "provider-tok", token: "--token=provider-tok" },
|
|
2665
|
+
],
|
|
2666
|
+
preconditions: [{ name: "state", value: "approved" }],
|
|
2667
|
+
binding: { lineageId: closureLineage, revision: SHA, targetIdentity: SHA },
|
|
2668
|
+
};
|
|
2669
|
+
const native = {
|
|
2670
|
+
targetStatus: async () => roleStatus,
|
|
2671
|
+
captureProviderRole: async () => ({
|
|
2672
|
+
schema: "gentle-ai.review-last-event-closure/v1",
|
|
2673
|
+
operation: "review/capture-validation",
|
|
2674
|
+
lineageId: closureLineage,
|
|
2675
|
+
state: "approved",
|
|
2676
|
+
action: "approved by validator",
|
|
2677
|
+
storeRevision: SHA,
|
|
2678
|
+
acknowledgement: {
|
|
2679
|
+
operation: "review.acknowledge-approved",
|
|
2680
|
+
command: "gentle-ai review acknowledge-approved --provider-vector",
|
|
2681
|
+
arguments: ackContinuation.arguments,
|
|
2682
|
+
preconditions: ackContinuation.preconditions,
|
|
2683
|
+
binding: ackContinuation.binding,
|
|
2684
|
+
raw: ackContinuation,
|
|
2685
|
+
},
|
|
2686
|
+
}),
|
|
2687
|
+
} as unknown as NativeReviewCli;
|
|
2688
|
+
const result = await __testing.executeReviewCaptureOperation({ lineageId: closureLineage, collectBinding: JSON.stringify(input), workspaceRoot: worktreeRoot }, worktreeRoot, native);
|
|
2689
|
+
assert.equal(result.status, "closed");
|
|
2690
|
+
assert.equal(result.outcome, "native-last-event-closure");
|
|
2691
|
+
assert.equal(result.next_action, `gentle_review {"operation":"acknowledge-approved","lineageId":"${closureLineage}","workspaceRoot":${JSON.stringify(worktreeRoot)}}`);
|
|
2692
|
+
assert.equal((result.closure as { next_action?: string }).next_action, `gentle_review {"operation":"acknowledge-approved","lineageId":"${closureLineage}","workspaceRoot":${JSON.stringify(worktreeRoot)}}`);
|
|
2693
|
+
assert.deepEqual((result.closure as { acknowledgement?: unknown }).acknowledgement, ackContinuation);
|
|
2694
|
+
});
|
|
2695
|
+
|
|
1836
2696
|
test("targeted-validator captures echo the distinct provider correction target identity", async () => {
|
|
1837
2697
|
const closureLineage = "review-validator-correction-target";
|
|
1838
2698
|
const correctionTarget = `sha256:${"d".repeat(64)}`;
|