gentle-pi 3.7.0 → 4.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +155 -10
- package/assets/agents/gentle-ai-explore.md +13 -6
- package/assets/agents/gentle-ai-verify.md +27 -6
- package/assets/agents/gentle-ai-worker.md +13 -15
- package/assets/install-wizard/index.html +47 -0
- package/assets/install-wizard/wizard.css +1279 -0
- package/assets/install-wizard/wizard.js +951 -0
- package/assets/orchestrator-delegation.md +75 -169
- package/assets/orchestrator-memory.md +12 -26
- package/assets/orchestrator-prompts.md +48 -0
- package/assets/orchestrator-skills.md +1 -1
- package/assets/orchestrator-tracking.md +53 -0
- package/assets/orchestrator-verification.md +28 -0
- package/assets/orchestrator-writer.md +38 -0
- package/assets/orchestrator.md +30 -38
- package/assets/sounds/LICENSE.md +52 -0
- package/assets/sounds/attention.wav +0 -0
- package/assets/sounds/error.wav +0 -0
- package/assets/sounds/success.wav +0 -0
- package/assets/support/strict-tdd-verify.md +4 -266
- package/assets/support/strict-tdd.md +8 -360
- package/bin/engram-guard +9 -0
- package/bin/gentle-shell-install.mjs +302 -0
- package/bin/gentle-shell.mjs +956 -50
- package/contracts/review-provider-contract-mirror/provider-contract.lock.json +7 -7
- package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/manifest.json +2 -2
- package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/orchestration/pi.md +16 -4
- package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/generated/provider-capabilities.baseline.json +2 -2
- package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/generated/provider-roles.baseline.json +1 -1
- package/docs/bundled-install.md +397 -0
- package/docs/delegated-verification.md +27 -4
- package/docs/gentle-agents-activity.md +537 -0
- package/docs/gentle-shell-uninstall.md +136 -0
- package/docs/gentle-shell.md +165 -51
- package/docs/install-wizard.md +2040 -0
- package/docs/native-authority-architecture.md +3 -3
- package/docs/permission-modes.md +82 -0
- package/docs/prompt-history.md +280 -0
- package/docs/readme-reference.md +364 -231
- package/docs/render-cache-performance.md +28 -0
- package/docs/sound-notifications-proposal.md +191 -0
- package/docs/sound-notifications.md +114 -0
- package/docs/telemetry.md +1 -1
- package/docs/yolo-mode.md +86 -0
- package/extensions/child-context.ts +35 -0
- package/extensions/child-safety.ts +25 -0
- package/extensions/codegraph-tools.ts +18 -5
- package/extensions/gentle-agents.ts +1093 -326
- package/extensions/gentle-ai.ts +1636 -890
- package/extensions/gentle-herdr-activity.ts +81 -0
- package/extensions/gentle-notifications.ts +104 -0
- package/extensions/gentle-shell.ts +1619 -136
- package/extensions/gentle-stats.ts +101 -0
- package/extensions/gentle-todo.ts +126 -20
- package/extensions/history/atomic-write.ts +38 -0
- package/extensions/history/hide-prompts.ts +183 -0
- package/extensions/history/index.ts +1424 -0
- package/extensions/history/load-shared-history.ts +39 -0
- package/extensions/history/selector-helpers.ts +538 -0
- package/extensions/history/session-scan.ts +233 -0
- package/extensions/history/store.ts +1119 -0
- package/extensions/nan-provider.ts +6 -0
- package/extensions/pi-pretty.ts +39 -3
- package/extensions/quiet-tools.ts +254 -92
- package/extensions/resume-hint.ts +60 -0
- package/extensions/skill-registry.ts +191 -58
- package/extensions/startup-banner.ts +71 -33
- package/lib/agent-assets.ts +604 -0
- package/lib/agent-home.ts +10 -1
- package/lib/agent-model-resolution.ts +80 -0
- package/lib/agent-profile-pin.ts +12 -0
- package/lib/agent-profiles.ts +13 -3
- package/lib/agents-completion-delivery.ts +11 -3
- package/lib/agents-message-delivery.ts +181 -0
- package/lib/agents-messaging.ts +1 -1
- package/lib/agents-protocol.ts +99 -0
- package/lib/agents-runner.ts +180 -107
- package/lib/agents-view.ts +26 -3
- package/lib/agents-widget.ts +16 -9
- package/lib/append-system-prompt.ts +21 -0
- package/lib/auto-permission-classifier.ts +280 -0
- package/lib/auto-permission-runtime.ts +268 -0
- package/lib/background-jobs-tools.ts +202 -0
- package/lib/background-jobs.ts +253 -0
- package/lib/background-monitor.ts +154 -0
- package/lib/bounded-writer-admission.ts +220 -0
- package/lib/builtin-codemode-optout.ts +166 -0
- package/lib/card-content-policy.ts +85 -0
- package/lib/card-style-policy.ts +60 -0
- package/lib/child-context-files.ts +166 -0
- package/lib/child-package-injection.ts +62 -0
- package/lib/codemode-renderer.ts +195 -0
- package/lib/command-palette-catalog.ts +5 -10
- package/lib/command-palette.ts +25 -14
- package/lib/context-threshold.ts +26 -0
- package/lib/destructive-command-guard.ts +144 -0
- package/lib/gentle-ai-binary.ts +109 -4
- package/lib/gentle-ai-elapsed-store.ts +87 -0
- package/lib/gentle-ai-renderer.ts +196 -39
- package/lib/gentle-shell-bundled.ts +517 -0
- package/lib/gentle-shell-launcher.ts +89 -27
- package/lib/gentle-shell-resume-hint.ts +176 -0
- package/lib/gentle-shell-startup-flow.ts +54 -0
- package/lib/gentle-shell-startup-update.ts +92 -0
- package/lib/gentle-shell-uninstall.ts +840 -0
- package/lib/gentle-shell-update-panel.ts +231 -0
- package/lib/herdr-activity.ts +183 -0
- package/lib/history-capture-policy.ts +95 -0
- package/lib/inprocess-reviewer.ts +3 -0
- package/lib/jobs-sidebar-state.ts +20 -0
- package/lib/jobs-view.ts +161 -0
- package/lib/model-price-ratio.ts +83 -0
- package/lib/model-routing-authority.ts +5 -1
- package/lib/nan-provider.ts +249 -0
- package/lib/native-review-cli.ts +242 -180
- package/lib/notification-audio-native.ts +162 -0
- package/lib/notification-audio-windows.ts +399 -0
- package/lib/notification-audio.ts +332 -0
- package/lib/notification-customize.ts +241 -0
- package/lib/notification-events.ts +113 -0
- package/lib/notification-policy.ts +148 -0
- package/lib/notification-pulse-client.ts +367 -0
- package/lib/notification-pulse-protocol.ts +305 -0
- package/lib/notification-pulse-stream.ts +213 -0
- package/lib/notification-pulse-worker.ts +66 -0
- package/lib/notification-scheduler.ts +173 -0
- package/lib/notification-service.ts +202 -0
- package/lib/notification-ui.ts +87 -0
- package/lib/odd-phase-inference.ts +231 -0
- package/lib/odd-phase.ts +141 -0
- package/lib/orchestrator-alias.ts +20 -0
- package/lib/orchestrator-catalog.ts +79 -0
- package/lib/orchestrator-consultation.ts +61 -0
- package/lib/orchestrator-discovery.ts +49 -0
- package/lib/orchestrator-helper-consent.ts +122 -0
- package/lib/orchestrator-helper.ts +167 -0
- package/lib/orchestrator-message-card.ts +105 -0
- package/lib/orchestrator-presence.ts +115 -14
- package/lib/orchestrator-scope.ts +80 -0
- package/lib/orchestrator-state.ts +107 -0
- package/lib/orchestrator-tool-card.ts +280 -0
- package/lib/orchestrator-work-search.ts +218 -0
- package/lib/orchestrator-work.ts +79 -0
- package/lib/overlay-repaint.ts +26 -0
- package/lib/permission-mode-policy.ts +59 -0
- package/lib/pi-tui-keys.ts +53 -0
- package/lib/review-candidate-view-owner.ts +73 -21
- package/lib/review-candidate-view.ts +115 -27
- package/lib/review-compact-contract.ts +2 -2
- package/lib/review-host-relay.ts +7 -2
- package/lib/review-integration-v2.ts +100 -22
- package/lib/review-publication-gate.ts +1 -1
- package/lib/review-reminder-receipt.ts +48 -8
- package/lib/review-risk-assessment.ts +227 -56
- package/lib/review-sidebar-state.ts +224 -0
- package/lib/review-transaction.ts +72 -3
- package/lib/runtime-metrics-children.ts +1 -1
- package/lib/runtime-metrics-native.ts +3 -2
- package/lib/selection-engine.ts +527 -0
- package/lib/session-change-capture.ts +5 -4
- package/lib/session-changes.ts +27 -22
- package/lib/session-messaging-grants.ts +135 -0
- package/lib/session-profile-binding.ts +83 -0
- package/lib/session-profile-freeze.ts +266 -0
- package/lib/session-worktree-registry.ts +87 -9
- package/lib/shell-bar.ts +215 -30
- package/lib/shell-card.ts +287 -18
- package/lib/shell-changes-view.ts +42 -14
- package/lib/shell-changes.ts +86 -71
- package/lib/shell-prompt.ts +102 -6
- package/lib/shell-sidebar-layout.ts +83 -15
- package/lib/shell-sidebar.ts +15 -1
- package/lib/shell-todo.ts +84 -31
- package/lib/shell-usage-view.ts +9 -4
- package/lib/shell-usage.ts +70 -13
- package/lib/stats-collector.ts +381 -0
- package/lib/stats-view.ts +431 -0
- package/lib/status-timing-diagnostics.ts +201 -0
- package/lib/telemetry-trigger.ts +3 -1
- package/lib/theme-customization.ts +52 -0
- package/lib/vim-editor-adapter.ts +383 -0
- package/lib/vim-normal-engine.ts +154 -0
- package/lib/vim-operator-engine.ts +416 -0
- package/lib/vim-policy.ts +49 -0
- package/lib/vim-visual-engine.ts +107 -0
- package/lib/visual-customization-policy.ts +119 -0
- package/lib/visual-customize-view.ts +481 -0
- package/lib/visual-profiles.ts +228 -0
- package/lib/writer-surfaces.ts +162 -0
- package/lib/yolo-session-policy.ts +317 -0
- package/package.json +20 -8
- package/runtime/child-package-injection.mjs +63 -0
- package/runtime/gentle-ai-binary.mjs +109 -4
- package/runtime/gentle-shell-bundled.mjs +518 -0
- package/runtime/gentle-shell-launcher.mjs +87 -25
- package/runtime/gentle-shell-resume-hint.mjs +177 -0
- package/runtime/gentle-shell-startup-flow.mjs +55 -0
- package/runtime/gentle-shell-startup-update.mjs +93 -0
- package/runtime/gentle-shell-uninstall.mjs +841 -0
- package/runtime/gentle-shell-update-panel.mjs +232 -0
- package/runtime/native-review-cli.mjs +239 -177
- package/runtime/review-integration-v2.mjs +100 -22
- package/runtime/review-risk-assessment.mjs +225 -54
- package/runtime/status-timing-diagnostics.mjs +202 -0
- package/runtime/telemetry-trigger.mjs +3 -1
- package/scripts/bootstrap.cmd +255 -0
- package/scripts/bootstrap.sh +168 -0
- package/scripts/build-distribution.mjs +157 -0
- package/scripts/build-installer-bundles.mjs +136 -0
- package/scripts/build-runtime-modules.mjs +8 -0
- package/scripts/bundled-install.mjs +1308 -0
- package/scripts/bundled-wizard.mjs +399 -0
- package/scripts/check-types.mjs +4 -6
- package/scripts/gentle-ai-installer.mjs +83 -35
- package/scripts/install-gentle-ai.mjs +48 -1
- package/scripts/install-tui-mode-setting.mjs +10 -1
- package/scripts/install-wizard-preview.mjs +154 -0
- package/scripts/installer-downloads.mjs +648 -0
- package/scripts/installer-preflight.mjs +387 -0
- package/scripts/installer-probes.mjs +564 -0
- package/scripts/installer-runner.mjs +1102 -0
- package/scripts/installer-server.mjs +881 -0
- package/scripts/installer-windows-artifacts.json +15 -0
- package/scripts/installer-windows.mjs +752 -0
- package/scripts/main-channel.mjs +452 -0
- package/scripts/mirror-odd-routing.mjs +2 -2
- package/scripts/npm-pack-result.mjs +31 -0
- package/scripts/render-cache-performance-worker.mjs +205 -0
- package/scripts/render-cache-performance.mjs +115 -0
- package/scripts/run-test-suite.mjs +90 -0
- package/scripts/test-installer-acceptance.sh +161 -0
- package/scripts/test-packed-runner.mjs +54 -19
- package/scripts/types-baseline.json +2 -92
- package/scripts/verify-package-files.mjs +91 -38
- package/skills/branch-pr/SKILL.md +24 -52
- package/skills/chained-pr/SKILL.md +31 -15
- package/skills/chained-pr/references/chaining-details.md +31 -20
- package/skills/gentle-ai/SKILL.md +16 -21
- package/skills/issue-creation/SKILL.md +8 -2
- package/skills/issue-creation/references/delegated-workflow-actions.md +19 -0
- package/skills/work-unit-commits/SKILL.md +4 -3
- package/tests/agent-home.test.ts +56 -3
- package/tests/agent-model-resolution.test.ts +113 -0
- package/tests/agent-profiles.test.ts +30 -0
- package/tests/agent-risk-escalation.test.ts +123 -0
- package/tests/agents-completion-delivery.test.ts +17 -0
- package/tests/agents-fake-child.ts +2 -2
- package/tests/agents-message-delivery.test.ts +106 -0
- package/tests/agents-protocol.test.ts +97 -0
- package/tests/agents-queries.test.ts +9 -9
- package/tests/agents-runner-process.test.ts +31 -0
- package/tests/agents-runner.test.ts +467 -93
- package/tests/agents-view-thread-identity.test.ts +169 -0
- package/tests/agents-view.test.ts +8 -2
- package/tests/agents-widget.test.ts +154 -15
- package/tests/append-system-prompt-route.test.ts +160 -0
- package/tests/append-system-prompt.test.ts +46 -0
- package/tests/artifact-language.test.ts +19 -213
- package/tests/ask-user-question.test.ts +44 -1
- package/tests/asset-installation-runtime.test.ts +5 -16
- package/tests/auto-mode-invariants.test.ts +258 -0
- package/tests/auto-mode-sdk.test.ts +57 -0
- package/tests/auto-mode.test.ts +49 -0
- package/tests/auto-permission-classifier.test.ts +283 -0
- package/tests/auto-permission-runtime.test.ts +196 -0
- package/tests/autonomous-guard.test.ts +69 -1
- package/tests/background-cache-warming.test.ts +2 -1
- package/tests/background-jobs.test.ts +227 -0
- package/tests/background-monitor.test.ts +195 -0
- package/tests/background-subagents-default-mode.test.ts +28 -5
- package/tests/bounded-stop-contract.test.ts +85 -0
- package/tests/bounded-writer-admission.test.ts +146 -0
- package/tests/branch-pr-skill.test.ts +43 -0
- package/tests/bridge-wake-lifecycle.test.ts +129 -0
- package/tests/bridge-wake-rendering.test.ts +56 -0
- package/tests/build-distribution.test.ts +269 -0
- package/tests/builtin-codemode-optout.test.ts +242 -0
- package/tests/bundled-install.test.ts +1319 -0
- package/tests/bundled-wizard.test.ts +505 -0
- package/tests/card-content-policy.test.ts +68 -0
- package/tests/card-style-policy.test.ts +87 -0
- package/tests/chained-pr-skill.test.ts +124 -0
- package/tests/child-codegraph-bootstrap.test.ts +74 -0
- package/tests/child-context-files.test.ts +255 -0
- package/tests/child-package-entrypoints.test.ts +65 -0
- package/tests/child-package-injection.test.ts +74 -0
- package/tests/child-safety.test.ts +94 -0
- package/tests/child-web-package.test.ts +224 -0
- package/tests/codegraph-tools.test.ts +35 -0
- package/tests/codemode-rendering.test.ts +596 -0
- package/tests/command-palette.test.ts +43 -7
- package/tests/context-threshold.test.ts +31 -0
- package/tests/delegate-for-reason-contract.test.ts +327 -0
- package/tests/delegated-key-learnings-contract.test.ts +3 -78
- package/tests/destructive-command-guard.test.ts +84 -0
- package/tests/devbinary/native-review-parity.devtest.ts +171 -3
- package/tests/devbinary/non-git-subagent-bootstrap.devtest.ts +193 -0
- package/tests/devbinary/pi-host-relay.devtest.ts +173 -3
- package/tests/engram-safety.test.ts +288 -0
- package/tests/fixtures/stats/sessions/--work-alpha--/2026-09-28T10-00-00-000Z_aaa.jsonl +7 -0
- package/tests/fixtures/stats/sessions/--work-alpha--/2026-09-29T23-00-00-000Z_bbb.jsonl +3 -0
- package/tests/fixtures/stats/sessions/--work-alpha--/2026-09-30T08-00-00-000Z_ddd.jsonl +2 -0
- package/tests/fixtures/stats/sessions/--work-alpha--/2026-09-30T09-00-00-000Z_eee.jsonl +3 -0
- package/tests/fixtures/stats/sessions/--work-alpha--/run-1/session.jsonl +2 -0
- package/tests/fixtures/stats/sessions/--work-beta--/2026-09-01T12-00-00-000Z_ccc.jsonl +2 -0
- package/tests/fixtures/stats/user-pi/sessions/--work-alpha--/2026-09-28T10-00-00-000Z_aaa.jsonl +2 -0
- package/tests/fixtures/stats/user-pi/sessions/--work-alpha--/2026-09-29T23-00-00-000Z_bbb.jsonl +4 -0
- package/tests/fixtures/stats/user-pi/sessions/--work-gamma--/2026-09-20T09-00-00-000Z_fff.jsonl +3 -0
- package/tests/generic-agent-tools.test.ts +69 -0
- package/tests/gentle-agents.test.ts +2804 -346
- package/tests/gentle-ai-binary-isolation.test.ts +135 -0
- package/tests/gentle-ai-binary.test.ts +21 -4
- package/tests/gentle-ai-child-guards.test.ts +202 -0
- package/tests/gentle-ai-dev-binary-surfacing.test.ts +125 -15
- package/tests/gentle-ai-elapsed-store.test.ts +68 -0
- package/tests/gentle-ai-installer.test.ts +566 -62
- package/tests/gentle-ai-prompt-model-routing.test.ts +123 -0
- package/tests/gentle-ai-renderer.test.ts +493 -14
- package/tests/gentle-ai.test.ts +1427 -236
- package/tests/gentle-card-text.ts +2 -1
- package/tests/gentle-herdr-activity.test.ts +100 -0
- package/tests/gentle-notifications.test.ts +427 -0
- package/tests/gentle-shell-bin.test.ts +896 -115
- package/tests/gentle-shell-bundled.test.ts +1002 -0
- package/tests/gentle-shell-launcher.test.ts +333 -65
- package/tests/gentle-shell-resume-hint.test.ts +270 -0
- package/tests/gentle-shell-startup-flow.test.ts +75 -0
- package/tests/gentle-shell-startup-update.test.ts +85 -0
- package/tests/gentle-shell-uninstall.test.ts +978 -0
- package/tests/gentle-shell-update-panel.test.ts +158 -0
- package/tests/gentle-shell.test.ts +4216 -119
- package/tests/gentle-stats.test.ts +152 -0
- package/tests/gentle-theme.test.ts +4 -1
- package/tests/gentle-todo.test.ts +333 -10
- package/tests/herdr-activity.test.ts +243 -0
- package/tests/history-agent-home.test.ts +81 -0
- package/tests/history-atomic-write.test.ts +57 -0
- package/tests/history-capture-policy.test.ts +102 -0
- package/tests/history-child-guard.test.ts +70 -0
- package/tests/history-command-registration.test.ts +164 -0
- package/tests/history-dedupe-entries.test.ts +123 -0
- package/tests/history-delete-backfill.test.ts +190 -0
- package/tests/history-delete-confirm.test.ts +460 -0
- package/tests/history-dispatch.test.ts +180 -0
- package/tests/history-drain-hidden.test.ts +110 -0
- package/tests/history-drain-order.test.ts +98 -0
- package/tests/history-expanded-globals.test.ts +62 -0
- package/tests/history-gc.test.ts +832 -0
- package/tests/history-header-layout.test.ts +265 -0
- package/tests/history-hide-prompts.test.ts +275 -0
- package/tests/history-lazy-windowing.test.ts +508 -0
- package/tests/history-legacy-migrate-v2.test.ts +297 -0
- package/tests/history-load-shared-history.test.ts +53 -0
- package/tests/history-max-results-cap.test.ts +76 -0
- package/tests/history-multi-reader.test.ts +203 -0
- package/tests/history-off-path.test.ts +170 -0
- package/tests/history-openflow-integration.test.ts +173 -0
- package/tests/history-overlay-margin.test.ts +326 -0
- package/tests/history-preview-layout.test.ts +93 -0
- package/tests/history-registry.test.ts +143 -0
- package/tests/history-scope-delete.test.ts +411 -0
- package/tests/history-search-caret-keys.test.ts +142 -0
- package/tests/history-seed-bootstrap.test.ts +170 -0
- package/tests/history-seed-regen.test.ts +129 -0
- package/tests/history-selector-windowing.test.ts +94 -0
- package/tests/history-session-scan-directory.test.ts +87 -0
- package/tests/history-session-scan-extract.test.ts +583 -0
- package/tests/history-session-writer.test.ts +351 -0
- package/tests/history-store-paths.test.ts +79 -0
- package/tests/history-tombstone-exact.test.ts +139 -0
- package/tests/history-wheel-mouse.test.ts +242 -0
- package/tests/inprocess-reviewer.test.ts +49 -19
- package/tests/install-gentle-ai-failure.test.ts +69 -0
- package/tests/install-tui-mode-setting.test.ts +15 -0
- package/tests/install-wizard.test.ts +1223 -0
- package/tests/installer-bundles.test.ts +122 -0
- package/tests/installer-posix-bootstrap.test.ts +1201 -0
- package/tests/installer-preflight.test.ts +750 -0
- package/tests/installer-probes.test.ts +1106 -0
- package/tests/installer-runner.test.ts +2158 -0
- package/tests/installer-server.test.ts +1567 -0
- package/tests/installer-windows-bootstrap.test.ts +2410 -0
- package/tests/issue-creation-skill.test.ts +61 -0
- package/tests/jobs-view.test.ts +160 -0
- package/tests/main-channel-upgrade.test.ts +389 -0
- package/tests/main-channel.test.ts +344 -0
- package/tests/maintainer/provider-relay.maintest.ts +13 -12
- package/tests/model-price-ratio.test.ts +116 -0
- package/tests/model-profiles-preservation.test.ts +153 -0
- package/tests/model-routing-authority.test.ts +16 -0
- package/tests/model-routing-package.test.ts +10 -0
- package/tests/nan-provider-runtime.test.ts +156 -0
- package/tests/nan-provider.test.ts +481 -0
- package/tests/native-binary-gate.test.ts +2 -0
- package/tests/native-review-capability-contract.test.ts +13 -1
- package/tests/native-review-cli.test.ts +239 -118
- package/tests/native-review-consent.test.ts +2 -2
- package/tests/native-review-parity-runtime.test.ts +101 -4
- package/tests/native-review-parity.test.ts +12 -48
- package/tests/native-risk-evidence-parity.test.ts +21 -0
- package/tests/no-self-pause-contract.test.ts +87 -0
- package/tests/notification-audio-native.test.ts +281 -0
- package/tests/notification-audio-windows.test.ts +581 -0
- package/tests/notification-audio.test.ts +548 -0
- package/tests/notification-customize.test.ts +655 -0
- package/tests/notification-events.test.ts +115 -0
- package/tests/notification-policy.test.ts +201 -0
- package/tests/notification-pulse-client.test.ts +356 -0
- package/tests/notification-pulse-protocol.test.ts +213 -0
- package/tests/notification-pulse-stream.test.ts +320 -0
- package/tests/notification-scheduler.test.ts +258 -0
- package/tests/notification-ui.test.ts +157 -0
- package/tests/npm-pack-result.test.ts +41 -0
- package/tests/odd-integration.test.ts +76 -0
- package/tests/odd-phase-inference.test.ts +213 -0
- package/tests/odd-phase-loader.test.ts +253 -0
- package/tests/odd-phase.test.ts +307 -0
- package/tests/odd-routing-canonical-ratchet.test.ts +60 -14
- package/tests/odd-routing-contract.test.ts +158 -50
- package/tests/orchestrator-budget.test.ts +43 -45
- package/tests/orchestrator-catalog.test.ts +146 -0
- package/tests/orchestrator-consultation-sdk.test.ts +614 -0
- package/tests/orchestrator-consultation.test.ts +111 -0
- package/tests/orchestrator-discovery.test.ts +241 -0
- package/tests/orchestrator-helper-consent.test.ts +171 -0
- package/tests/orchestrator-helper.test.ts +297 -0
- package/tests/orchestrator-message-card.test.ts +190 -0
- package/tests/orchestrator-presence.test.ts +23 -0
- package/tests/orchestrator-rdd-ownership.test.ts +8 -7
- package/tests/orchestrator-scope.test.ts +107 -0
- package/tests/orchestrator-state.test.ts +114 -0
- package/tests/orchestrator-status-publication-contract.test.ts +34 -0
- package/tests/orchestrator-tool-card.test.ts +398 -0
- package/tests/orchestrator-work-search.test.ts +318 -0
- package/tests/orchestrator-work.test.ts +147 -0
- package/tests/overlay-repaint.test.ts +74 -0
- package/tests/package-manifest.test.ts +344 -123
- package/tests/packed-runner-owned-path.test.ts +80 -0
- package/tests/parallel-reason-contract.test.ts +21 -0
- package/tests/parallel-writer-admission.test.ts +210 -0
- package/tests/permission-mode-policy.test.ts +194 -0
- package/tests/persona-single-channel.test.ts +10 -18
- package/tests/pi-pretty.test.ts +56 -0
- package/tests/provider-defect-handoff.test.ts +7 -14
- package/tests/quiet-bash-runtime.test.ts +76 -0
- package/tests/quiet-tool-rendering.test.ts +569 -187
- package/tests/rdd-aware-verification-contract.test.ts +103 -40
- package/tests/rdd-status-line.test.ts +9 -4
- package/tests/render-cache-performance.test.ts +48 -0
- package/tests/resume-hint-extension.test.ts +122 -0
- package/tests/review-agent-end-preflight.test.ts +237 -21
- package/tests/review-candidate-owner-retry.test.ts +22 -1
- package/tests/review-candidate-view.test.ts +399 -12
- package/tests/review-contract-prompt.test.ts +138 -44
- package/tests/review-controller-lock-status.test.ts +0 -1
- package/tests/review-controller-native-recovery.test.ts +327 -17
- package/tests/review-controller-native-routing.test.ts +866 -6
- package/tests/review-controller-workspace-root.test.ts +206 -6
- package/tests/review-controller.test.ts +2 -2
- package/tests/review-correction-lifecycle.test.ts +2 -1
- package/tests/review-gate.test.ts +7 -4
- package/tests/review-graph-schema.test.ts +2 -2
- package/tests/review-host-relay-routing.test.ts +342 -4
- package/tests/review-host-relay.test.ts +49 -2
- package/tests/review-integration-v2-forward.test.ts +221 -1
- package/tests/review-json-arguments.test.ts +167 -0
- package/tests/review-last-event-closure.test.ts +157 -8
- package/tests/review-ledger-contract.test.ts +14 -37
- package/tests/review-policy-judgment-day.test.ts +1 -1
- package/tests/review-policy-ordinary.test.ts +2 -2
- package/tests/review-relay-transport-agent.test.ts +66 -1
- package/tests/review-reminder-receipt.test.ts +47 -1
- package/tests/review-risk-assessment.test.ts +594 -26
- package/tests/review-session-standing-permission-controller.test.ts +16 -17
- package/tests/review-session-standing-permission-ipc.test.ts +6 -5
- package/tests/review-sidebar-state.test.ts +402 -0
- package/tests/review-test-fixtures.ts +7 -1
- package/tests/review-transaction.test.ts +18 -1
- package/tests/run-test-suite.test.ts +202 -0
- package/tests/runtime-harness.mjs +186 -800
- package/tests/runtime-metrics-children.test.ts +16 -23
- package/tests/runtime-metrics-extension.test.ts +1 -1
- package/tests/runtime-metrics-native.test.ts +31 -1
- package/tests/selection-engine.test.ts +451 -0
- package/tests/session-change-capture.test.ts +10 -10
- package/tests/session-changes-shell.test.ts +242 -48
- package/tests/session-changes.test.ts +17 -24
- package/tests/session-messaging-grants.test.ts +255 -0
- package/tests/session-profile-binding.test.ts +163 -0
- package/tests/session-profile-freeze.test.ts +381 -0
- package/tests/session-subject-routing-contract.test.ts +34 -0
- package/tests/session-worktree-registry.test.ts +134 -2
- package/tests/shell-bar.test.ts +422 -1
- package/tests/shell-card.test.ts +353 -1
- package/tests/shell-changes-view.test.ts +135 -32
- package/tests/shell-changes.test.ts +61 -62
- package/tests/shell-prompt.test.ts +94 -2
- package/tests/shell-render-cache-runtime.test.ts +69 -0
- package/tests/shell-render-cache.test.ts +129 -0
- package/tests/shell-sidebar-layout.test.ts +351 -22
- package/tests/shell-sidebar-scroll-benchmark.test.ts +255 -0
- package/tests/shell-todo.test.ts +207 -3
- package/tests/shell-usage-view.test.ts +28 -1
- package/tests/shell-usage.test.ts +108 -0
- package/tests/short-check-routing-contract.test.ts +49 -0
- package/tests/skill-registry-watcher.test.ts +119 -0
- package/tests/skill-registry.test.ts +357 -2
- package/tests/startup-banner.test.ts +246 -4
- package/tests/startup-home-repo.test.ts +170 -0
- package/tests/stats-collector.test.ts +195 -0
- package/tests/stats-view.test.ts +202 -0
- package/tests/status-timing-diagnostics.test.ts +283 -0
- package/tests/support/orchestrator-modules.ts +19 -0
- package/tests/task-size-routing-contract.test.ts +241 -0
- package/tests/telemetry-trigger.test.ts +107 -20
- package/tests/test-strength-contract.test.ts +124 -0
- package/tests/theme-customization.test.ts +72 -0
- package/tests/verify-agent-contract.test.ts +90 -0
- package/tests/verify-first-launch-contract.test.ts +139 -0
- package/tests/verify-package-files.test.ts +56 -0
- package/tests/vim-editor-adapter-host-resolution.test.ts +37 -0
- package/tests/vim-editor-adapter.test.ts +804 -0
- package/tests/vim-normal-engine.test.ts +101 -0
- package/tests/vim-operator-engine.test.ts +215 -0
- package/tests/vim-policy.test.ts +19 -0
- package/tests/vim-visual-engine.test.ts +52 -0
- package/tests/visual-customization-policy.test.ts +139 -0
- package/tests/visual-customize-view.test.ts +696 -0
- package/tests/visual-profiles.test.ts +87 -0
- package/tests/writer-edit-surface-scope.test.ts +59 -2
- package/tests/writer-surfaces.test.ts +199 -0
- package/tests/yolo-customize.test.ts +271 -0
- package/tests/yolo-mode-runtime.test.ts +188 -0
- package/tests/yolo-mode.test.ts +289 -0
- package/tests/yolo-session-policy.test.ts +59 -0
- package/themes/Gentle.json +2 -1
- package/themes/Gentleman-Cute.json +2 -1
- package/themes/Gentleman-Sexy.json +2 -1
- package/assets/agents/sdd-apply.md +0 -159
- package/assets/agents/sdd-archive.md +0 -228
- package/assets/agents/sdd-design.md +0 -49
- package/assets/agents/sdd-explore.md +0 -48
- package/assets/agents/sdd-init.md +0 -56
- package/assets/agents/sdd-onboard.md +0 -52
- package/assets/agents/sdd-proposal.md +0 -64
- package/assets/agents/sdd-remediate.md +0 -37
- package/assets/agents/sdd-research.md +0 -49
- package/assets/agents/sdd-spec.md +0 -192
- package/assets/agents/sdd-status.md +0 -54
- package/assets/agents/sdd-tasks.md +0 -108
- package/assets/agents/sdd-verify.md +0 -124
- package/assets/chains/sdd-full.chain.md +0 -83
- package/assets/chains/sdd-plan.chain.md +0 -56
- package/assets/chains/sdd-verify.chain.md +0 -43
- package/assets/sdd-orchestrator-workflow.md +0 -319
- package/assets/support/sdd-status-contract.md +0 -77
- package/docs/assets/diagrams/sdd-cycle.svg +0 -14
- package/extensions/sdd-init.ts +0 -816
- package/lib/openspec-deltas.ts +0 -156
- package/lib/sdd-preflight.ts +0 -1066
- package/lib/sdd-research-capabilities.ts +0 -94
- package/lib/sdd-status.ts +0 -26
- package/tests/fixtures/legacy/sdd-research-v2.5.0.md +0 -54
- package/tests/fixtures/native-review-cli/v2.1.3/bind-sdd.json +0 -25
- package/tests/fixtures/v0.10.7/assets/agents/sdd-apply.md +0 -132
- package/tests/openspec-deltas.test.ts +0 -209
- package/tests/sdd-agent-tools.test.ts +0 -156
- package/tests/sdd-archive-replay.test.ts +0 -82
- package/tests/sdd-classical-continuation.test.ts +0 -74
- package/tests/sdd-execution-routing-contract.test.ts +0 -44
- package/tests/sdd-managed-runtime-settlement.test.ts +0 -155
- package/tests/sdd-native-managed-uptake.test.ts +0 -243
- package/tests/sdd-no-attempts-contract.test.ts +0 -15
- package/tests/sdd-odd-integration.test.ts +0 -33
- package/tests/sdd-optional-research.test.ts +0 -124
- package/tests/sdd-planning-routing-contract.test.ts +0 -45
- package/tests/sdd-preflight-rpc-input.test.ts +0 -125
- package/tests/sdd-preflight.test.ts +0 -541
- package/tests/sdd-research-capabilities.test.ts +0 -114
- package/tests/sdd-research-live.test.ts +0 -241
- package/tests/sdd-selection-transport.test.ts +0 -653
- package/tests/sdd-status.test.ts +0 -9
- package/tests/sdd-task-truth.test.ts +0 -43
- /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/README.md +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/schemas/lens.schema.json +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/schemas/refuter.schema.json +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/schemas/targeted-validator.schema.json +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/vectors/lens.json +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/vectors/refuter.json +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/vectors/targeted-validator.json +0 -0
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
// Deterministic ODD phase inference from observed tool activity. The Gentle
|
|
2
|
+
// Shell working label used to depend only on the model calling
|
|
3
|
+
// gentle_odd_phase, which models routinely skip; the tools the primary
|
|
4
|
+
// session actually runs are a reliable, observable signal instead.
|
|
5
|
+
//
|
|
6
|
+
// The mapping is deliberately conservative: an unknown tool, unknown subagent
|
|
7
|
+
// role, or an ambiguous shell command returns undefined so the caller leaves
|
|
8
|
+
// the current label unchanged. This module is pure (no Pi or registry
|
|
9
|
+
// imports); precedence against explicit reports lives in OddPhaseRegistry.
|
|
10
|
+
|
|
11
|
+
import type { OddPhase } from "./odd-phase.ts";
|
|
12
|
+
|
|
13
|
+
const TOOL_PHASES: Readonly<Record<string, OddPhase>> = {
|
|
14
|
+
read: "exploring",
|
|
15
|
+
grep: "exploring",
|
|
16
|
+
find: "exploring",
|
|
17
|
+
ls: "exploring",
|
|
18
|
+
codegraph: "exploring",
|
|
19
|
+
ask_user_choice: "deciding",
|
|
20
|
+
ask_user_question: "deciding",
|
|
21
|
+
todo: "planning",
|
|
22
|
+
gentle_review: "checking",
|
|
23
|
+
gentle_review_scope: "checking",
|
|
24
|
+
gentle_review_capture: "checking",
|
|
25
|
+
gentle_review_capture_group: "checking",
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const DELEGATED_PHASES: Readonly<Record<string, OddPhase>> = {
|
|
29
|
+
"gentle-ai-worker": "implementing",
|
|
30
|
+
"gentle-ai-verify": "checking",
|
|
31
|
+
"gentle-ai-explore": "exploring",
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const WRITE_TOOLS = new Set(["edit", "write"]);
|
|
35
|
+
const SHELL_TOOLS = new Set(["bash", "powershell"]);
|
|
36
|
+
|
|
37
|
+
// Test, typecheck, lint, and build runners. Matched per shell segment.
|
|
38
|
+
const CHECKING_COMMANDS: readonly RegExp[] = [
|
|
39
|
+
/^(pnpm|npm|yarn|bun)\s+(run\s+)?(test|typecheck|lint|build|check)\b/,
|
|
40
|
+
/^node\b.*\s--test\b/,
|
|
41
|
+
/^node\s+\S*(test|check|lint|typecheck)[\w-]*\.m?[jt]s\b/,
|
|
42
|
+
/^((npx|pnpx|bunx)\s+|(pnpm|yarn)\s+exec\s+)?(vitest|jest|mocha|tsc|eslint|pytest)\b/,
|
|
43
|
+
/^python3?\s+-m\s+pytest\b/,
|
|
44
|
+
/^go\s+(test|build|vet)\b/,
|
|
45
|
+
/^cargo\s+(test|build|check|clippy)\b/,
|
|
46
|
+
/^make\b/,
|
|
47
|
+
// Waiting on or reading CI results verifies work; it is not exploration.
|
|
48
|
+
/^gh\s+pr\s+checks\b/,
|
|
49
|
+
/^gh\s+run\s+(view|watch|list)\b/,
|
|
50
|
+
];
|
|
51
|
+
|
|
52
|
+
// Git listing flags that may take one non-flag argument (`--contains <rev>`)
|
|
53
|
+
// without turning `git branch`/`git tag` into a create.
|
|
54
|
+
const GIT_LIST_FLAG = String.raw`(\s+(-a|-r|-l|-v|-vv|-n\d*|--all|--remotes|--list|--show-current|--contains|--merged|--no-merged|--points-at|--sort=\S+)(\s+[^-\s]\S*)?)*$`;
|
|
55
|
+
|
|
56
|
+
// Read-only inspection. Matched per shell segment.
|
|
57
|
+
const EXPLORING_COMMANDS: readonly RegExp[] = [
|
|
58
|
+
/^git\s+(status|log|diff|show|blame|rev-parse|rev-list|merge-base|ls-files|ls-remote|ls-tree|cat-file|describe|shortlog|reflog|grep|check-ignore|for-each-ref|name-rev|count-objects)\b/,
|
|
59
|
+
new RegExp(String.raw`^git\s+(branch|tag)${GIT_LIST_FLAG}`),
|
|
60
|
+
/^git\s+remote(\s+(-v|--verbose|show|get-url)\b.*)?$/,
|
|
61
|
+
/^git\s+(worktree|stash)\s+list\b/,
|
|
62
|
+
/^git\s+config\s+(--get|--get-all|--get-regexp|-l|--list)\b/,
|
|
63
|
+
/^gh\s+(pr|issue|repo|release)\s+(view|list|diff|status)\b/,
|
|
64
|
+
/^(ls|cat|head|tail|grep|egrep|rg|wc|pwd|tree|stat|file|which|type|readlink|realpath|dirname|basename|cut|uniq|tr|nl|column|strings|diff|cmp|od|xxd|hexdump|shasum|sha256sum|md5|md5sum|du|df|date|uname|whoami|ps|pgrep|lsof|jq|awk)\b/,
|
|
65
|
+
/^command\s+-v\b/,
|
|
66
|
+
/^(\[\[?|test)\s/,
|
|
67
|
+
/^sort\b(?!.*\s(-o|--output)\b)/,
|
|
68
|
+
/^sed\b(?!.*\s(-[a-zA-Z]*i|--in-place)\b)/,
|
|
69
|
+
/^find\b(?!.*\s-(delete|exec|execdir|ok|okdir|fprint\w*)\b)/,
|
|
70
|
+
];
|
|
71
|
+
|
|
72
|
+
// Segments that neither inspect nor change anything worth labeling: shell
|
|
73
|
+
// control words, no-op builtins, and bare variable assignments (their
|
|
74
|
+
// command substitutions are classified as commands of their own).
|
|
75
|
+
const NEUTRAL_COMMANDS: readonly RegExp[] = [
|
|
76
|
+
/^(cd|pushd|popd|echo|printf|sleep|true|false|export|set)\b/,
|
|
77
|
+
/^(done|fi|esac|\}|\))$/,
|
|
78
|
+
/^for\s+\w+(\s+in\b.*)?$/,
|
|
79
|
+
// Each assignment must end at whitespace or the end: an optional separator
|
|
80
|
+
// would let `a=a=a=…` split anywhere and backtrack exponentially.
|
|
81
|
+
/^([A-Za-z_][A-Za-z0-9_]*=("[^"]*"|'[^']*'|[^\s"'])*(\s+|$))+$/,
|
|
82
|
+
];
|
|
83
|
+
|
|
84
|
+
// Shell keywords that prefix the command they introduce (`do grep x`).
|
|
85
|
+
const LEADING_KEYWORDS = /^(do|then|else|elif|if|while|until|time|!|\{|\()\s+/;
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Infers the ODD phase implied by a tool call, or undefined when the call
|
|
89
|
+
* carries no reliable phase signal and the label must stay as it is.
|
|
90
|
+
*/
|
|
91
|
+
export function inferOddPhase(toolName: string, args: unknown): OddPhase | undefined {
|
|
92
|
+
const name = normalizeToolName(toolName);
|
|
93
|
+
if (name === "subagent_run") {
|
|
94
|
+
const agent = stringArg(args, "agent");
|
|
95
|
+
return agent && Object.hasOwn(DELEGATED_PHASES, agent) ? DELEGATED_PHASES[agent] : undefined;
|
|
96
|
+
}
|
|
97
|
+
if (WRITE_TOOLS.has(name)) return isOddTaskPath(stringArg(args, "path") ?? stringArg(args, "file_path")) ? "planning" : "implementing";
|
|
98
|
+
if (SHELL_TOOLS.has(name)) return inferShellPhase(stringArg(args, "command"));
|
|
99
|
+
return Object.hasOwn(TOOL_PHASES, name) ? TOOL_PHASES[name] : undefined;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Some runtimes expose tools through an MCP proxy as `mcp__<server>__<tool>`.
|
|
103
|
+
function normalizeToolName(toolName: string): string {
|
|
104
|
+
return toolName.replace(/^mcp__.+?__/, "");
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function stringArg(args: unknown, key: string): string | undefined {
|
|
108
|
+
if (typeof args !== "object" || args === null) return undefined;
|
|
109
|
+
const value = (args as Record<string, unknown>)[key];
|
|
110
|
+
return typeof value === "string" ? value : undefined;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function isOddTaskPath(path: string | undefined): boolean {
|
|
114
|
+
return path !== undefined && /(^|\/)odd\/tasks\//.test(path.replaceAll("\\", "/"));
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* A command checks when any segment runs a checker; it explores only when
|
|
119
|
+
* every segment is read-only inspection (or neutral like `cd`). Anything
|
|
120
|
+
* else — a mutation, an install, an unknown program — is ambiguous.
|
|
121
|
+
*/
|
|
122
|
+
function inferShellPhase(command: string | undefined): OddPhase | undefined {
|
|
123
|
+
if (command === undefined) return undefined;
|
|
124
|
+
const segments = splitShellCommands(command)
|
|
125
|
+
.map(stripSegment)
|
|
126
|
+
.filter((segment) => segment.length > 0 && !isNeutral(segment));
|
|
127
|
+
if (segments.length === 0) return undefined;
|
|
128
|
+
if (segments.some((segment) => matchesAny(CHECKING_COMMANDS, segment))) return "checking";
|
|
129
|
+
return segments.every(isReadOnlyInspection) ? "exploring" : undefined;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Splits a shell command into simple commands. Top-level `&&`, `||`, `;`,
|
|
134
|
+
* `|`, `&`, and newlines separate them; quoted text never does; and every
|
|
135
|
+
* `$(...)` substitution contributes its own commands, leaving a `$_`
|
|
136
|
+
* placeholder behind. Not a full shell parser: whatever it cannot follow
|
|
137
|
+
* lands in a segment that matches no pattern, so the label stays unchanged.
|
|
138
|
+
*/
|
|
139
|
+
function splitShellCommands(command: string): string[] {
|
|
140
|
+
const commands: string[] = [];
|
|
141
|
+
let current = "";
|
|
142
|
+
let quote: "'" | '"' | undefined;
|
|
143
|
+
for (let i = 0; i < command.length; i++) {
|
|
144
|
+
const char = command[i];
|
|
145
|
+
if (quote === "'") {
|
|
146
|
+
current += char;
|
|
147
|
+
if (char === "'") quote = undefined;
|
|
148
|
+
} else if (char === "\\") {
|
|
149
|
+
current += char + (command[i + 1] ?? "");
|
|
150
|
+
i++;
|
|
151
|
+
} else if (char === "$" && command[i + 1] === "(") {
|
|
152
|
+
const end = findSubstitutionEnd(command, i + 2);
|
|
153
|
+
if (end < 0) return [...commands, current + command.slice(i)];
|
|
154
|
+
commands.push(...splitShellCommands(command.slice(i + 2, end)));
|
|
155
|
+
current += "$_";
|
|
156
|
+
i = end;
|
|
157
|
+
} else if (quote === '"') {
|
|
158
|
+
current += char;
|
|
159
|
+
if (char === '"') quote = undefined;
|
|
160
|
+
} else if (char === "'" || char === '"') {
|
|
161
|
+
quote = char;
|
|
162
|
+
current += char;
|
|
163
|
+
} else if (isSeparator(command, i)) {
|
|
164
|
+
commands.push(current);
|
|
165
|
+
current = "";
|
|
166
|
+
if ((char === "|" || char === "&") && command[i + 1] === char) i++;
|
|
167
|
+
} else {
|
|
168
|
+
current += char;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
commands.push(current);
|
|
172
|
+
return commands;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// `&` inside a redirection (`2>&1`, `&>/dev/null`) is not a separator.
|
|
176
|
+
function isSeparator(command: string, i: number): boolean {
|
|
177
|
+
const char = command[i];
|
|
178
|
+
if (char === "&") return command[i - 1] !== ">" && command[i + 1] !== ">";
|
|
179
|
+
return char === ";" || char === "|" || char === "\n";
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Index of the `)` closing a `$(` whose body starts at `start`, or -1.
|
|
183
|
+
function findSubstitutionEnd(command: string, start: number): number {
|
|
184
|
+
let depth = 1;
|
|
185
|
+
let quote: "'" | '"' | undefined;
|
|
186
|
+
for (let i = start; i < command.length; i++) {
|
|
187
|
+
const char = command[i];
|
|
188
|
+
if (quote) {
|
|
189
|
+
if (char === quote) quote = undefined;
|
|
190
|
+
else if (char === "\\" && quote === '"') i++;
|
|
191
|
+
} else if (char === "\\") i++;
|
|
192
|
+
else if (char === "'" || char === '"') quote = char;
|
|
193
|
+
else if (char === "(") depth++;
|
|
194
|
+
else if (char === ")" && --depth === 0) return i;
|
|
195
|
+
}
|
|
196
|
+
return -1;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Drops harmless redirections (`2>&1`, `2>/dev/null`), leading shell
|
|
200
|
+
// keywords (`do grep x`), environment assignments (`CI=1 pnpm test`), git
|
|
201
|
+
// global options (`git -C dir status`), and whitespace. A bare assignment
|
|
202
|
+
// is left intact for NEUTRAL_COMMANDS.
|
|
203
|
+
function stripSegment(segment: string): string {
|
|
204
|
+
let stripped = segment.replace(HARMLESS_REDIRECTS, "").trim();
|
|
205
|
+
while (LEADING_KEYWORDS.test(stripped)) stripped = stripped.replace(LEADING_KEYWORDS, "");
|
|
206
|
+
return stripped
|
|
207
|
+
.replace(/^([A-Za-z_][A-Za-z0-9_]*=("[^"]*"|'[^']*'|[^\s"'])*\s+)+(?=\S)/, "")
|
|
208
|
+
.replace(/^git\s+((-C|-c)\s+\S+\s+|--no-pager\s+)+/, "git ");
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// A segment that writes a file (`echo x > out`) is never a no-op.
|
|
212
|
+
function isNeutral(segment: string): boolean {
|
|
213
|
+
return !writesFile(segment) && matchesAny(NEUTRAL_COMMANDS, segment);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function isReadOnlyInspection(segment: string): boolean {
|
|
217
|
+
return !writesFile(segment) && matchesAny(EXPLORING_COMMANDS, segment);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Stream merges and discarded output; neither writes a file.
|
|
221
|
+
const HARMLESS_REDIRECTS = /\s*(\d?>&\d|&?\d?>\s*\/dev\/null)/g;
|
|
222
|
+
|
|
223
|
+
// Output redirection to a file is a write. Deliberately naive: a `>` inside
|
|
224
|
+
// quoted text also counts, which only ever leaves the label unchanged.
|
|
225
|
+
function writesFile(segment: string): boolean {
|
|
226
|
+
return segment.replace(HARMLESS_REDIRECTS, "").includes(">");
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function matchesAny(patterns: readonly RegExp[], segment: string): boolean {
|
|
230
|
+
return patterns.some((pattern) => pattern.test(segment));
|
|
231
|
+
}
|
package/lib/odd-phase.ts
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
// Bounded ODD phase signal for the Gentle prompt's working label. There is
|
|
2
|
+
// no Pi runtime event for ODD phases, so the label comes from two sources:
|
|
3
|
+
// phases inferred deterministically from the primary session's tool activity
|
|
4
|
+
// (lib/odd-phase-inference.ts, wired in extensions/gentle-shell.ts) and
|
|
5
|
+
// explicit reports by the orchestrator through gentle_odd_phase, which
|
|
6
|
+
// refine it with phases tools cannot show (see
|
|
7
|
+
// assets/orchestrator-tracking.md). Never inferred from assistant prose;
|
|
8
|
+
// the prompt falls back to the generic "working…" label when nothing applies.
|
|
9
|
+
|
|
10
|
+
// Covers the ODD protocol steps in AGENTS.md (1 authorize .. 7 close); not a
|
|
11
|
+
// strict one-to-one mapping. "researching"/"deciding" both cover step 3
|
|
12
|
+
// (resolve uncertainty), and "deciding" also covers step 4's classify
|
|
13
|
+
// decision (no dedicated label: classifying is typically an instantaneous
|
|
14
|
+
// internal decision, not standalone visible work). "checking"/"closing"
|
|
15
|
+
// both cover step 7 (close).
|
|
16
|
+
export const ODD_PHASES = [
|
|
17
|
+
"authorizing",
|
|
18
|
+
"exploring",
|
|
19
|
+
"researching",
|
|
20
|
+
"deciding",
|
|
21
|
+
"planning",
|
|
22
|
+
"implementing",
|
|
23
|
+
"checking",
|
|
24
|
+
"closing",
|
|
25
|
+
] as const;
|
|
26
|
+
|
|
27
|
+
export type OddPhase = (typeof ODD_PHASES)[number];
|
|
28
|
+
|
|
29
|
+
export function isOddPhase(value: unknown): value is OddPhase {
|
|
30
|
+
return typeof value === "string" && (ODD_PHASES as readonly string[]).includes(value);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function oddPhaseLabel(phase: OddPhase): string {
|
|
34
|
+
return `${phase}…`;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Who set a session's phase: the orchestrator through gentle_odd_phase, or tool-activity inference. */
|
|
38
|
+
export type OddPhaseSource = "explicit" | "inferred";
|
|
39
|
+
|
|
40
|
+
interface ReportedOddPhase {
|
|
41
|
+
phase: OddPhase;
|
|
42
|
+
source: OddPhaseSource;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Session-scoped, best-effort ODD phase signal. Keyed by Pi session id so a
|
|
47
|
+
* background/child agent — which runs as its own OS process with its own
|
|
48
|
+
* module state (see lib/agents-runner.ts) — can never see or override the
|
|
49
|
+
* primary session's reported phase. The owning extension clears a session's
|
|
50
|
+
* entry at turn and session boundaries (agent_start, agent_settled,
|
|
51
|
+
* session_shutdown) so a stale phase from a finished turn never leaks into
|
|
52
|
+
* the next one.
|
|
53
|
+
*/
|
|
54
|
+
export class OddPhaseRegistry {
|
|
55
|
+
private readonly phases = new Map<string, ReportedOddPhase>();
|
|
56
|
+
// The WORKING-state pulse loop does not run at all under the "potato"
|
|
57
|
+
// animation policy (see GentlePromptEditor.startPulse), and workingLabel
|
|
58
|
+
// is otherwise only read on the next incidental render. Pi's own docs
|
|
59
|
+
// (docs/tui.md) instruct components to call requestRender() themselves
|
|
60
|
+
// after a state change; no host re-render is implicitly guaranteed. The
|
|
61
|
+
// owning prompt registers its own redraw callback here so a phase change
|
|
62
|
+
// is visible immediately regardless of animation policy.
|
|
63
|
+
private readonly renderRequests = new Map<string, () => void>();
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Sets the current ODD phase for a session. Only accepts an
|
|
67
|
+
* already-validated OddPhase; callers must validate untrusted input
|
|
68
|
+
* with isOddPhase() (or use clear() for the explicit "clear" token)
|
|
69
|
+
* before calling this. Returns undefined, without mutating any state or
|
|
70
|
+
* requesting a redraw, when there is no session id to scope the report
|
|
71
|
+
* to. The source defaults to "explicit"; tool-activity inference goes
|
|
72
|
+
* through infer() so its precedence rule applies.
|
|
73
|
+
*/
|
|
74
|
+
report(sessionId: string | undefined, phase: OddPhase, source: OddPhaseSource = "explicit"): OddPhase | undefined {
|
|
75
|
+
if (!sessionId) return undefined;
|
|
76
|
+
this.phases.set(sessionId, { phase, source });
|
|
77
|
+
this.renderRequests.get(sessionId)?.();
|
|
78
|
+
return phase;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Applies a phase inferred from tool activity and returns the session's
|
|
83
|
+
* resulting phase. An explicit report is not overridden by an inferred
|
|
84
|
+
* "exploring" (read-only tools are routine while researching or
|
|
85
|
+
* deciding). Incidental reads and planning bookkeeping also cannot
|
|
86
|
+
* displace active implementation/checking. A known delegated exploration
|
|
87
|
+
* is a deliberate transition, not an incidental read; later edits and
|
|
88
|
+
* checks can still move between active work phases. Inferring the phase
|
|
89
|
+
* already shown changes nothing,
|
|
90
|
+
* keeps an explicit source explicit, and never requests a redraw, so
|
|
91
|
+
* repeated tool calls do not repaint the prompt.
|
|
92
|
+
*/
|
|
93
|
+
infer(sessionId: string | undefined, phase: OddPhase, cause: "tool" | "delegation" = "tool"): OddPhase | undefined {
|
|
94
|
+
if (!sessionId) return undefined;
|
|
95
|
+
const current = this.phases.get(sessionId);
|
|
96
|
+
if (current?.phase === phase) return phase;
|
|
97
|
+
if (phase === "exploring" && cause !== "delegation" &&
|
|
98
|
+
(current?.source === "explicit" || current?.phase === "implementing" || current?.phase === "checking")) return current?.phase;
|
|
99
|
+
if (phase === "planning" && (current?.phase === "implementing" || current?.phase === "checking")) return current.phase;
|
|
100
|
+
return this.report(sessionId, phase, "inferred");
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Clears a session's reported phase (turn/session boundary, or the
|
|
105
|
+
* explicit "clear" token). An invalid/unrecognized phase report is
|
|
106
|
+
* never a reason to clear: only this method resets the session, so a
|
|
107
|
+
* malformed report leaves the previously reported phase in place.
|
|
108
|
+
* Clearing also drops the report's source.
|
|
109
|
+
*/
|
|
110
|
+
clear(sessionId: string | undefined): void {
|
|
111
|
+
if (!sessionId) return;
|
|
112
|
+
if (this.phases.delete(sessionId)) this.renderRequests.get(sessionId)?.();
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
get(sessionId: string | undefined): OddPhase | undefined {
|
|
116
|
+
return sessionId ? this.phases.get(sessionId)?.phase : undefined;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** Working label for the reported phase, or undefined to fall back to the generic "working…" label. */
|
|
120
|
+
label(sessionId: string | undefined): string | undefined {
|
|
121
|
+
const phase = this.get(sessionId);
|
|
122
|
+
return phase ? oddPhaseLabel(phase) : undefined;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** Registers (replacing any previous registration) the callback used to request an immediate redraw when this session's phase changes. */
|
|
126
|
+
setRenderRequest(sessionId: string | undefined, requestRender: () => void): void {
|
|
127
|
+
if (sessionId) this.renderRequests.set(sessionId, requestRender);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
clearRenderRequest(sessionId: string | undefined): void {
|
|
131
|
+
if (sessionId) this.renderRequests.delete(sessionId);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Pi loads extensions with separate jiti moduleCache:false loaders, so a
|
|
136
|
+
// module-local singleton is duplicated. The global symbol bridges only those
|
|
137
|
+
// loaders inside this process; subagents run in separate OS processes. Keep
|
|
138
|
+
// the session key inside the registry and clear it at turn/session boundaries.
|
|
139
|
+
const ODD_PHASE_REGISTRY = Symbol.for("gentle-pi.odd-phase-registry");
|
|
140
|
+
const processState = globalThis as typeof globalThis & { [ODD_PHASE_REGISTRY]?: OddPhaseRegistry };
|
|
141
|
+
export const oddPhaseRegistry = processState[ODD_PHASE_REGISTRY] ??= new OddPhaseRegistry();
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { stripVTControlCharacters } from "node:util";
|
|
2
|
+
|
|
3
|
+
export interface TaskAliases { initialAlias: string | null; currentAlias: string | null }
|
|
4
|
+
/** Explicit public topic only; never infer from names or private conversation. */
|
|
5
|
+
export function normalizeTaskSubject(value: unknown): string | undefined {
|
|
6
|
+
if (value === undefined) return undefined;
|
|
7
|
+
if (typeof value !== "string" || /\p{Cs}/u.test(value)) throw new Error("invalid-task-subject");
|
|
8
|
+
const clean = stripVTControlCharacters(value).replace(/[\p{Cc}\p{Cf}]/gu, " ").replace(/\s+/g, " ").trim();
|
|
9
|
+
return Array.from(clean).slice(0, 120).join("").trimEnd() || undefined;
|
|
10
|
+
}
|
|
11
|
+
export function decodeTaskAliases(value: unknown): TaskAliases | undefined {
|
|
12
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
|
|
13
|
+
const input = value as Record<string, unknown>;
|
|
14
|
+
const valid = (v: unknown): v is string => typeof v === "string" && !!v && !/[\p{Cc}\p{Cf}\p{Cs}]/u.test(v)
|
|
15
|
+
&& Array.from(v).length <= 120 && normalizeTaskSubject(v) === v;
|
|
16
|
+
if (Object.keys(input).length !== 2 || !Object.hasOwn(input, "initialAlias") || !Object.hasOwn(input, "currentAlias")
|
|
17
|
+
|| !(input.initialAlias === null || valid(input.initialAlias))
|
|
18
|
+
|| !(valid(input.currentAlias) || (input.currentAlias === null && input.initialAlias === null))) return undefined;
|
|
19
|
+
return { initialAlias: input.initialAlias as string | null, currentAlias: input.currentAlias as string | null };
|
|
20
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { isAbsolute, join } from "node:path";
|
|
3
|
+
import { boundedRead, rootFor, sanitizeDisplayLabel, type Header } from "./orchestrator-presence.ts";
|
|
4
|
+
|
|
5
|
+
// Private bounded derivative, never an identity/authority registry. Each request
|
|
6
|
+
// reads at most one 64 KiB snapshot and returns eight entries of each category.
|
|
7
|
+
export const CATALOG_BYTES = 64 * 1024;
|
|
8
|
+
export const CATALOG_ENTRIES = 64;
|
|
9
|
+
export const CATALOG_PAGES = 8;
|
|
10
|
+
type Task = { id: string; label: string; status: string; cwd: string | null };
|
|
11
|
+
type Catalog = { tasks: Task[]; registered: (string | null)[]; omittedTasks: number; omittedRegistered: number };
|
|
12
|
+
export type CatalogPage = Catalog & { cursor?: string };
|
|
13
|
+
const hash = (bytes: string | Buffer) => createHash("sha256").update(bytes).digest("hex");
|
|
14
|
+
const safePath = (path: string) => isAbsolute(path) && Buffer.byteLength(path) <= 256
|
|
15
|
+
&& !/[\p{Cc}\p{Cf}\u00a0\u2000-\u200a\u202f\u205f\u3000]/u.test(path);
|
|
16
|
+
const pathFact = (path: string) => safePath(path) ? path : null;
|
|
17
|
+
const exact = (value: any, keys: string[]) => value && typeof value === "object" && !Array.isArray(value)
|
|
18
|
+
&& Object.keys(value).length === keys.length && keys.every(k => Object.hasOwn(value, k));
|
|
19
|
+
const count = (n: unknown) => Number.isSafeInteger(n) && (n as number) >= 0;
|
|
20
|
+
const text = (s: unknown) => typeof s === "string" && s === sanitizeDisplayLabel(s);
|
|
21
|
+
const path = (s: unknown) => s === null || typeof s === "string" && safePath(s);
|
|
22
|
+
function valid(c: any): c is Catalog {
|
|
23
|
+
return exact(c, ["tasks", "registered", "omittedTasks", "omittedRegistered"])
|
|
24
|
+
&& count(c.omittedTasks) && count(c.omittedRegistered)
|
|
25
|
+
&& Array.isArray(c.tasks) && c.tasks.length <= CATALOG_ENTRIES
|
|
26
|
+
&& c.tasks.every((t: any) => exact(t, ["id", "label", "status", "cwd"])
|
|
27
|
+
&& text(t.id) && text(t.label) && ["running", "queued", "waiting"].includes(t.status) && path(t.cwd))
|
|
28
|
+
&& Array.isArray(c.registered) && c.registered.length <= CATALOG_ENTRIES && c.registered.every(path);
|
|
29
|
+
}
|
|
30
|
+
/** Explicit whitelist; eagerly detached, no task spreads or thread access. */
|
|
31
|
+
export function projectCatalog(tasks: readonly { id: string; label: string; status: string; cwd: string }[], roots: readonly string[]): Catalog {
|
|
32
|
+
const c: Catalog = { tasks: [], registered: [], omittedTasks: tasks.length, omittedRegistered: roots.length };
|
|
33
|
+
// Reserve envelope space; byte overflow is an exact omission, not truncation.
|
|
34
|
+
for (let i = 0; i < Math.min(CATALOG_ENTRIES, Math.max(tasks.length, roots.length)); i++) {
|
|
35
|
+
if (i < tasks.length) {
|
|
36
|
+
const t = tasks[i];
|
|
37
|
+
c.tasks.push({ id: sanitizeDisplayLabel(t.id), label: sanitizeDisplayLabel(t.label), status: t.status, cwd: pathFact(t.cwd) });
|
|
38
|
+
if (Buffer.byteLength(JSON.stringify(c)) > CATALOG_BYTES - 1024) c.tasks.pop();
|
|
39
|
+
else c.omittedTasks--;
|
|
40
|
+
}
|
|
41
|
+
if (i < roots.length) {
|
|
42
|
+
c.registered.push(pathFact(roots[i]));
|
|
43
|
+
if (Buffer.byteLength(JSON.stringify(c)) > CATALOG_BYTES - 1024) c.registered.pop();
|
|
44
|
+
else c.omittedRegistered--;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
if (!valid(c)) throw new Error("malformed-catalog");
|
|
48
|
+
return c;
|
|
49
|
+
}
|
|
50
|
+
/** Cursor binds only public catalog fields and producer activation, not private
|
|
51
|
+
* activity churn. Current envelope/header generation must still agree on every
|
|
52
|
+
* read; no cached pages, paths or authority are carried by the cursor. */
|
|
53
|
+
export function readCatalog(profile: string, h: Header, activation: string, cursor?: string): { page?: CatalogPage; unavailable?: string } {
|
|
54
|
+
try {
|
|
55
|
+
const bytes = boundedRead(join(rootFor(profile, false, true), `${h.sessionHash}.${h.incarnation}.json`), CATALOG_BYTES);
|
|
56
|
+
const value = JSON.parse(bytes.toString("utf8"));
|
|
57
|
+
if (!exact(value, ["schema", "sessionHash", "incarnation", "generation", "activation", "tokens", "catalog"])
|
|
58
|
+
|| value.schema !== 1 || value.sessionHash !== h.sessionHash || value.incarnation !== h.incarnation
|
|
59
|
+
|| value.generation !== h.generation || value.activation !== activation || !valid(value.catalog)
|
|
60
|
+
|| !Array.isArray(value.tokens) || value.tokens.length !== 7 || !value.tokens.every((s: unknown) => typeof s === "string" && /^[a-f0-9-]{36}$/.test(s))) throw new Error("unavailable");
|
|
61
|
+
const c: Catalog = value.catalog;
|
|
62
|
+
const publicBytes = JSON.stringify({ tasks: c.tasks.map(t => ({ id: t.id, label: t.label, status: t.status, cwd: t.cwd })),
|
|
63
|
+
registered: c.registered, omittedTasks: c.omittedTasks, omittedRegistered: c.omittedRegistered });
|
|
64
|
+
const binding = [h.sessionHash, h.incarnation, activation, hash(publicBytes)];
|
|
65
|
+
let offset = 0;
|
|
66
|
+
const token = (n: number) => Buffer.from(JSON.stringify([...binding, n, value.tokens[n / 8 - 1]])).toString("base64url");
|
|
67
|
+
if (cursor !== undefined) {
|
|
68
|
+
if (typeof cursor !== "string" || cursor.length > 1024) throw new Error("invalid-cursor");
|
|
69
|
+
const decoded = JSON.parse(Buffer.from(cursor, "base64url").toString("utf8"));
|
|
70
|
+
offset = decoded?.[4];
|
|
71
|
+
if (!count(offset) || offset < 8 || offset >= CATALOG_ENTRIES || offset % 8 || token(offset) !== cursor) throw new Error("invalid-cursor");
|
|
72
|
+
}
|
|
73
|
+
if (cursor && offset >= Math.max(c.tasks.length, c.registered.length)) throw new Error("invalid-cursor");
|
|
74
|
+
const end = offset + 8;
|
|
75
|
+
return { page: { tasks: c.tasks.slice(offset, end), registered: c.registered.slice(offset, end),
|
|
76
|
+
omittedTasks: c.omittedTasks, omittedRegistered: c.omittedRegistered,
|
|
77
|
+
...(end < Math.max(c.tasks.length, c.registered.length) ? { cursor: token(end) } : {}) } };
|
|
78
|
+
} catch { return { unavailable: "catalog-unknown-or-invalid-cursor" }; }
|
|
79
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import type { PresenceRecord } from "./agents-session-transport.ts";
|
|
3
|
+
import { discoverOrchestrators } from "./orchestrator-discovery.ts";
|
|
4
|
+
|
|
5
|
+
export const CONSULTATION_BYTES = 16 * 1024;
|
|
6
|
+
export interface MetadataSelection { recipientSessionId: string; cursor?: string }
|
|
7
|
+
type Candidate = ReturnType<typeof discoverOrchestrators>[number];
|
|
8
|
+
export interface MetadataReceipt {
|
|
9
|
+
schema: "gentle-agents.consultation/v1";
|
|
10
|
+
kind: "metadata"; status: "available" | "unavailable";
|
|
11
|
+
source: "published_snapshot"; ownerReply: false; authority: "none";
|
|
12
|
+
targetSessionId: string; observedAt: number;
|
|
13
|
+
freshness: "unknown" | "recent" | "stale";
|
|
14
|
+
presenceObservedAt?: number;
|
|
15
|
+
digest?: string;
|
|
16
|
+
snapshot?: { label: string | null; aliases?: Candidate["aliases"]; workspace: string | null; tasks: Candidate["tasks"];
|
|
17
|
+
omittedTasks: number; scope: Candidate["scope"] | null; catalog: Candidate["catalog"] | null;
|
|
18
|
+
state: Candidate["state"] | null };
|
|
19
|
+
unknowns: string[]; omissions: string[];
|
|
20
|
+
}
|
|
21
|
+
function freeze<T>(value: T): T {
|
|
22
|
+
if (value && typeof value === "object") {
|
|
23
|
+
for (const child of Object.values(value)) freeze(child);
|
|
24
|
+
Object.freeze(value);
|
|
25
|
+
}
|
|
26
|
+
return value;
|
|
27
|
+
}
|
|
28
|
+
export function unavailableMetadata(targetSessionId: string, reason: string, now = Date.now(), freshness: MetadataReceipt["freshness"] = "unknown"): MetadataReceipt {
|
|
29
|
+
return freeze({ schema: "gentle-agents.consultation/v1", kind: "metadata", status: "unavailable",
|
|
30
|
+
source: "published_snapshot", ownerReply: false, authority: "none", targetSessionId, observedAt: now,
|
|
31
|
+
freshness, unknowns: [reason], omissions: ["private-context", "owner-decision", "reachability", "exclusive-writer-ownership"] });
|
|
32
|
+
}
|
|
33
|
+
/** Reads the existing bounded, selected-activation projection, never activity.
|
|
34
|
+
* Identity excludes private activity generation/digests and observation clocks.
|
|
35
|
+
* Existing readers validate exact field whitelists before this detached capture. */
|
|
36
|
+
export function consultPublishedMetadata(profile: string, peers: readonly PresenceRecord[], selection: MetadataSelection, now = Date.now()): MetadataReceipt {
|
|
37
|
+
const peer = discoverOrchestrators(profile, peers, now, { recipientSessionId: selection.recipientSessionId,
|
|
38
|
+
cursor: selection.cursor, consultation: true })[0];
|
|
39
|
+
if (!peer?.publication || peer.freshness !== "recent") return unavailableMetadata(selection.recipientSessionId, "publication-unknown-or-stale", now, peer?.freshness);
|
|
40
|
+
if (selection.cursor !== undefined && !peer.catalog) return unavailableMetadata(selection.recipientSessionId, "catalog-unknown-or-invalid-cursor", now);
|
|
41
|
+
const snapshot = {
|
|
42
|
+
label: peer.label || null, ...(peer.aliases ? { aliases: peer.aliases } : {}), workspace: peer.workspace || null,
|
|
43
|
+
tasks: peer.tasks?.map(t => ({ id: t.id, label: t.label, status: t.status, workspace: t.workspace })) ?? [],
|
|
44
|
+
omittedTasks: peer.omitted ?? 0, scope: peer.scope ?? null, catalog: peer.catalog ?? null, state: peer.state ?? null,
|
|
45
|
+
};
|
|
46
|
+
const unknowns = ["reachability", "exclusive-writer-ownership", "owner-decision"];
|
|
47
|
+
if (!snapshot.workspace) unknowns.push("workspace");
|
|
48
|
+
if (!snapshot.scope) unknowns.push("repository-scope");
|
|
49
|
+
if (!snapshot.catalog) unknowns.push("catalog");
|
|
50
|
+
if (!snapshot.state) unknowns.push("curated-state");
|
|
51
|
+
const digest = createHash("sha256").update(JSON.stringify([selection.recipientSessionId,
|
|
52
|
+
peer.publication.incarnation, peer.publication.activation, snapshot])).digest("hex");
|
|
53
|
+
const receipt: MetadataReceipt = { schema: "gentle-agents.consultation/v1", kind: "metadata", status: "available",
|
|
54
|
+
source: "published_snapshot", ownerReply: false, authority: "none", targetSessionId: selection.recipientSessionId,
|
|
55
|
+
observedAt: now, freshness: peer.freshness, presenceObservedAt: peer.publication.heartbeat, digest, snapshot,
|
|
56
|
+
unknowns, omissions: ["private-context", "git-facts-beyond-published-prefix"] };
|
|
57
|
+
// JSON capture detaches every nested list/record and guarantees plain JSON.
|
|
58
|
+
const bytes = JSON.stringify(receipt);
|
|
59
|
+
if (Buffer.byteLength(bytes) > CONSULTATION_BYTES) return unavailableMetadata(selection.recipientSessionId, "snapshot-too-large", now);
|
|
60
|
+
return freeze(JSON.parse(bytes) as MetadataReceipt);
|
|
61
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { readCatalog, type CatalogPage } from "./orchestrator-catalog.ts";
|
|
2
|
+
import type { PublishedWork } from "./orchestrator-work.ts";
|
|
3
|
+
import type { PresenceRecord } from "./agents-session-transport.ts";
|
|
4
|
+
import { activationHash, listPresence, readDiscovery, sessionHash, type DiscoveryMetadata } from "./orchestrator-presence.ts";
|
|
5
|
+
|
|
6
|
+
export interface OrchestratorCandidate {
|
|
7
|
+
sessionId: string;
|
|
8
|
+
reachability: "unknown";
|
|
9
|
+
freshness: "unknown" | "recent" | "stale";
|
|
10
|
+
label?: string;
|
|
11
|
+
aliases?: DiscoveryMetadata["aliases"];
|
|
12
|
+
workspace?: string;
|
|
13
|
+
tasks?: DiscoveryMetadata["tasks"];
|
|
14
|
+
omitted?: number;
|
|
15
|
+
scope?: DiscoveryMetadata["scope"];
|
|
16
|
+
state?: DiscoveryMetadata["state"];
|
|
17
|
+
workRecord?: { work: PublishedWork | null; recordedAt: number };
|
|
18
|
+
catalog?: CatalogPage;
|
|
19
|
+
catalogUnavailable?: string;
|
|
20
|
+
/** Internal binding for targeted consultation; never expose transport records. */
|
|
21
|
+
publication?: { incarnation: string; activation: string; heartbeat: number };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** One bounded metadata page, no thread reads or transport probes. The registry
|
|
25
|
+
* selects a canonical routing activation; metadata must bind to that exact one.
|
|
26
|
+
* Incomplete scans and duplicate presence headers fail closed. */
|
|
27
|
+
export function discoverOrchestrators(profile: string, peers: readonly PresenceRecord[], now = Date.now(), selection?: { recipientSessionId?: string; cursor?: string; consultation?: boolean; includeWork?: boolean }): OrchestratorCandidate[] {
|
|
28
|
+
const page = listPresence(profile, now);
|
|
29
|
+
const ids = [...new Set(peers.map(peer => peer.sessionId))];
|
|
30
|
+
return ids.filter(id => selection?.recipientSessionId === undefined || selection.recipientSessionId === id).map(sessionId => {
|
|
31
|
+
const unknown: OrchestratorCandidate = { sessionId, reachability: "unknown", freshness: "unknown" };
|
|
32
|
+
const activations = peers.filter(peer => peer.sessionId === sessionId);
|
|
33
|
+
if (page.unavailable || page.overflow || page.rejected || activations.length !== 1) return unknown;
|
|
34
|
+
const matches = page.entries.filter(h => h.sessionHash === sessionHash(sessionId));
|
|
35
|
+
// Even a stale duplicate could represent another process with the same ID.
|
|
36
|
+
if (matches.length !== 1) return unknown;
|
|
37
|
+
const header = matches[0];
|
|
38
|
+
const metadata = readDiscovery(profile, header);
|
|
39
|
+
if (metadata?.activation !== activationHash(activations[0])) return unknown;
|
|
40
|
+
if (!header.recent) return { ...unknown, freshness: "stale" };
|
|
41
|
+
const catalog = readCatalog(profile, header, metadata.activation, selection?.recipientSessionId === sessionId ? selection.cursor : undefined);
|
|
42
|
+
return { ...unknown, ...(catalog.page ? { catalog: catalog.page } : { catalogUnavailable: catalog.unavailable }), freshness: "recent", label: header.label, workspace: metadata.workspace,
|
|
43
|
+
tasks: metadata.tasks, omitted: metadata.omitted, ...(metadata.aliases ? { aliases: metadata.aliases } : {}), ...(metadata.scope ? { scope: metadata.scope } : {}),
|
|
44
|
+
...(selection?.consultation ? { publication: { incarnation: header.incarnation, activation: metadata.activation, heartbeat: header.heartbeat } } : {}),
|
|
45
|
+
...(selection?.includeWork && metadata.state?.sessionId === sessionId
|
|
46
|
+
? { workRecord: { work: metadata.state.state?.work ?? null, recordedAt: metadata.state.recordedAt } } : {}),
|
|
47
|
+
...(!selection?.includeWork && selection?.recipientSessionId === sessionId && metadata.state?.sessionId === sessionId ? { state: metadata.state } : {}) };
|
|
48
|
+
});
|
|
49
|
+
}
|