gentle-pi 4.0.0 → 4.2.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 +157 -18
- package/assets/agents/gentle-ai-explore.md +11 -4
- package/assets/agents/gentle-ai-verify.md +23 -4
- package/assets/agents/gentle-ai-worker.md +6 -6
- 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 +70 -163
- package/assets/orchestrator-memory.md +11 -4
- package/assets/orchestrator-prompts.md +48 -0
- 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 +29 -25
- 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/bin/engram-guard +9 -0
- package/bin/gentle-shell-install.mjs +302 -0
- package/bin/gentle-shell.mjs +708 -22
- 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/auto-permission-answers.md +25 -0
- package/docs/auto-permission-evidence.md +39 -0
- package/docs/bundled-install.md +399 -0
- package/docs/delegated-verification.md +3 -5
- package/docs/gentle-agents-activity.md +513 -0
- package/docs/gentle-shell-uninstall.md +136 -0
- package/docs/gentle-shell.md +90 -33
- package/docs/install-wizard.md +2040 -0
- package/docs/native-authority-architecture.md +1 -1
- package/docs/permission-modes.md +127 -0
- package/docs/prompt-history.md +1 -1
- package/docs/readme-reference.md +310 -47
- package/docs/render-cache-performance.md +28 -0
- package/docs/session-profile-format.md +39 -0
- package/docs/sound-notifications-proposal.md +191 -0
- package/docs/sound-notifications.md +114 -0
- package/docs/yolo-mode.md +11 -10
- package/extensions/ask-user-choice.ts +4 -0
- package/extensions/ask-user-question.ts +6 -1
- package/extensions/child-context.ts +11 -2
- package/extensions/child-safety.ts +4 -2
- package/extensions/codegraph-tools.ts +18 -5
- package/extensions/gentle-agents.ts +718 -87
- package/extensions/gentle-ai.ts +991 -263
- package/extensions/gentle-herdr-activity.ts +81 -0
- package/extensions/gentle-notifications.ts +104 -0
- package/extensions/gentle-shell.ts +271 -80
- package/extensions/gentle-todo.ts +123 -18
- package/extensions/history/index.ts +11 -6
- package/extensions/pi-pretty.ts +39 -3
- package/extensions/quiet-tools.ts +91 -21
- package/extensions/skill-registry.ts +175 -46
- package/extensions/startup-banner.ts +11 -2
- package/lib/agent-home.ts +10 -1
- package/lib/agent-model-resolution.ts +80 -0
- package/lib/agent-profiles.ts +13 -3
- package/lib/agents-completion-delivery.ts +11 -3
- package/lib/agents-messaging.ts +1 -1
- package/lib/agents-protocol.ts +40 -1
- package/lib/agents-runner.ts +90 -15
- package/lib/auto-permission-answers.ts +133 -0
- package/lib/auto-permission-classifier.ts +459 -0
- package/lib/auto-permission-evidence.ts +88 -0
- package/lib/auto-permission-memory.ts +69 -0
- package/lib/auto-permission-runtime.ts +484 -0
- package/lib/auto-permission-tool-context.ts +61 -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 +83 -10
- package/lib/builtin-codemode-optout.ts +166 -0
- package/lib/card-content-policy.ts +85 -0
- package/lib/child-package-injection.ts +62 -0
- package/lib/codemode-renderer.ts +14 -4
- package/lib/command-palette-catalog.ts +3 -2
- package/lib/context-threshold.ts +26 -0
- package/lib/destructive-command-guard.ts +104 -28
- package/lib/gentle-ai-binary.ts +109 -4
- package/lib/gentle-shell-bundled.ts +535 -0
- package/lib/gentle-shell-launcher.ts +75 -24
- 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/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 +6 -1
- package/lib/nan-provider.ts +33 -11
- package/lib/native-review-cli.ts +194 -73
- 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.ts +1 -1
- 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/permission-mode-policy.ts +59 -0
- package/lib/review-candidate-view-owner.ts +8 -6
- package/lib/review-candidate-view.ts +3 -2
- 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-risk-assessment.ts +77 -51
- package/lib/review-sidebar-state.ts +2 -1
- 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 +22 -7
- package/lib/session-change-capture.ts +5 -4
- package/lib/session-changes.ts +27 -22
- package/lib/session-profile-binding.ts +83 -0
- package/lib/session-profile-freeze.ts +261 -0
- package/lib/session-profile-persistence.ts +187 -0
- package/lib/session-worktree-registry.ts +74 -8
- package/lib/shell-bar.ts +41 -11
- package/lib/shell-changes-view.ts +40 -13
- package/lib/shell-changes.ts +88 -71
- package/lib/shell-prompt.ts +6 -4
- package/lib/shell-sidebar-layout.ts +5 -1
- package/lib/shell-todo.ts +81 -26
- package/lib/shell-usage.ts +4 -3
- package/lib/status-timing-diagnostics.ts +201 -0
- package/lib/telemetry-trigger.ts +3 -1
- package/lib/vim-editor-adapter.ts +27 -5
- package/lib/visual-customization-policy.ts +20 -9
- package/lib/visual-customize-view.ts +171 -20
- package/lib/writer-surfaces.ts +162 -0
- package/lib/yolo-session-policy.ts +198 -121
- package/package.json +5 -5
- package/runtime/child-package-injection.mjs +63 -0
- package/runtime/gentle-ai-binary.mjs +109 -4
- package/runtime/gentle-shell-bundled.mjs +536 -0
- package/runtime/gentle-shell-launcher.mjs +74 -23
- 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 +191 -70
- package/runtime/review-integration-v2.mjs +100 -22
- package/runtime/review-risk-assessment.mjs +77 -51
- 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 +7 -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 +79 -32
- 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/npm-pack-result.mjs +31 -0
- package/scripts/pi-update.mjs +223 -0
- package/scripts/render-cache-performance-worker.mjs +205 -0
- package/scripts/render-cache-performance.mjs +115 -0
- package/scripts/run-test-suite.mjs +22 -8
- package/scripts/test-installer-acceptance.sh +161 -0
- package/scripts/test-packed-runner.mjs +23 -5
- package/scripts/types-baseline.json +2 -92
- package/scripts/verify-package-files.mjs +82 -18
- package/skills/gentle-ai/SKILL.md +12 -11
- package/tests/agent-home.test.ts +56 -3
- package/tests/agent-model-resolution.test.ts +113 -0
- package/tests/agent-profiles.test.ts +12 -0
- package/tests/agent-risk-escalation.test.ts +123 -0
- package/tests/agents-completion-delivery.test.ts +17 -0
- package/tests/agents-protocol.test.ts +57 -0
- package/tests/agents-queries.test.ts +9 -9
- package/tests/agents-runner-process.test.ts +31 -0
- package/tests/agents-runner.test.ts +89 -4
- package/tests/append-system-prompt-route.test.ts +18 -1
- package/tests/auto-mode-human-answers.test.ts +68 -0
- package/tests/auto-mode-invariants.test.ts +276 -0
- package/tests/auto-mode-sdk.test.ts +84 -0
- package/tests/auto-mode.test.ts +49 -0
- package/tests/auto-permission-answers.test.ts +170 -0
- package/tests/auto-permission-classifier.test.ts +634 -0
- package/tests/auto-permission-compound.test.ts +24 -0
- package/tests/auto-permission-evidence.test.ts +88 -0
- package/tests/auto-permission-memory.test.ts +46 -0
- package/tests/auto-permission-public-regressions.test.ts +111 -0
- package/tests/auto-permission-runtime.test.ts +454 -0
- package/tests/autonomous-guard.test.ts +86 -16
- 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 +52 -1
- 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 +34 -2
- package/tests/child-codegraph-bootstrap.test.ts +74 -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 +12 -0
- package/tests/child-web-package.test.ts +224 -0
- package/tests/codegraph-tools.test.ts +35 -0
- package/tests/codemode-rendering.test.ts +107 -2
- package/tests/command-palette.test.ts +5 -5
- 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 -2
- package/tests/devbinary/native-review-parity.devtest.ts +1 -1
- package/tests/devbinary/pi-host-relay.devtest.ts +173 -3
- package/tests/engram-safety.test.ts +316 -0
- package/tests/generic-agent-tools.test.ts +16 -1
- package/tests/gentle-agents.test.ts +1895 -76
- package/tests/gentle-ai-binary-isolation.test.ts +135 -0
- package/tests/gentle-ai-binary.test.ts +19 -2
- 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-installer.test.ts +548 -58
- package/tests/gentle-ai-prompt-model-routing.test.ts +123 -0
- package/tests/gentle-ai-renderer.test.ts +6 -6
- package/tests/gentle-ai.test.ts +999 -31
- package/tests/gentle-herdr-activity.test.ts +100 -0
- package/tests/gentle-notifications.test.ts +427 -0
- package/tests/gentle-shell-bin.test.ts +263 -19
- package/tests/gentle-shell-bundled.test.ts +1036 -0
- package/tests/gentle-shell-launcher.test.ts +251 -25
- 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 +611 -100
- package/tests/gentle-todo.test.ts +317 -14
- package/tests/herdr-activity.test.ts +243 -0
- package/tests/history-agent-home.test.ts +81 -0
- package/tests/history-child-guard.test.ts +70 -0
- package/tests/inprocess-reviewer.test.ts +20 -0
- 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 +2159 -0
- package/tests/installer-server.test.ts +1567 -0
- package/tests/installer-windows-bootstrap.test.ts +2410 -0
- package/tests/jobs-view.test.ts +160 -0
- package/tests/main-channel-upgrade.test.ts +395 -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-package.test.ts +10 -0
- package/tests/nan-provider-runtime.test.ts +156 -0
- package/tests/nan-provider.test.ts +28 -18
- package/tests/native-binary-gate.test.ts +2 -0
- package/tests/native-review-capability-contract.test.ts +7 -1
- package/tests/native-review-cli.test.ts +236 -1
- package/tests/native-review-consent.test.ts +2 -2
- package/tests/native-review-parity-runtime.test.ts +1 -1
- 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 +14 -5
- package/tests/odd-routing-canonical-ratchet.test.ts +53 -12
- package/tests/odd-routing-contract.test.ts +80 -23
- package/tests/odd-runtime-delegation-gate.test.ts +4 -1
- package/tests/orchestrator-budget.test.ts +29 -6
- 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 +6 -5
- 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/package-manifest.test.ts +105 -21
- package/tests/packed-runner-owned-path.test.ts +35 -1
- 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 +4 -12
- package/tests/pi-pretty.test.ts +56 -0
- package/tests/pi-update-launcher.test.ts +87 -0
- package/tests/pi-update.test.ts +186 -0
- package/tests/provider-defect-handoff.test.ts +4 -3
- package/tests/quiet-tool-rendering.test.ts +160 -3
- package/tests/rdd-aware-verification-contract.test.ts +29 -41
- package/tests/rdd-status-line.test.ts +52 -0
- package/tests/render-cache-performance.test.ts +48 -0
- package/tests/review-agent-end-preflight.test.ts +62 -10
- package/tests/review-candidate-view.test.ts +101 -12
- package/tests/review-contract-prompt.test.ts +33 -4
- package/tests/review-controller-native-recovery.test.ts +327 -17
- package/tests/review-controller-native-routing.test.ts +255 -1
- package/tests/review-controller-workspace-root.test.ts +43 -2
- 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 +5 -3
- package/tests/review-host-relay.test.ts +49 -2
- package/tests/review-integration-v2-forward.test.ts +21 -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 +5 -4
- 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-risk-assessment.test.ts +109 -33
- 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-test-fixtures.ts +7 -1
- package/tests/review-transaction.test.ts +18 -1
- package/tests/run-test-suite.test.ts +83 -5
- package/tests/runtime-harness.mjs +42 -15
- package/tests/runtime-metrics-extension.test.ts +1 -1
- package/tests/runtime-metrics-native.test.ts +31 -1
- package/tests/selection-engine.test.ts +65 -5
- 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-profile-binding.test.ts +163 -0
- package/tests/session-profile-freeze.test.ts +394 -0
- package/tests/session-profile-persistence.test.ts +334 -0
- package/tests/session-subject-routing-contract.test.ts +34 -0
- package/tests/session-worktree-registry.test.ts +57 -2
- package/tests/shell-bar.test.ts +40 -0
- package/tests/shell-changes-view.test.ts +85 -34
- package/tests/shell-changes.test.ts +79 -62
- 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 +27 -2
- package/tests/shell-todo.test.ts +132 -11
- package/tests/shell-usage-view.test.ts +1 -1
- package/tests/shell-usage.test.ts +35 -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 +311 -5
- package/tests/startup-banner.test.ts +117 -3
- package/tests/startup-home-repo.test.ts +170 -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 +27 -1
- package/tests/test-strength-contract.test.ts +124 -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.test.ts +47 -21
- package/tests/visual-customization-policy.test.ts +32 -3
- package/tests/visual-customize-view.test.ts +288 -10
- package/tests/writer-edit-surface-scope.test.ts +59 -2
- package/tests/writer-surfaces.test.ts +199 -0
- package/tests/yolo-customize.test.ts +18 -3
- package/tests/yolo-mode-runtime.test.ts +37 -10
- package/tests/yolo-mode.test.ts +38 -5
- package/tests/yolo-session-policy.test.ts +1 -1
- /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
|
@@ -2,6 +2,10 @@ import { createHash, randomUUID } from "node:crypto";
|
|
|
2
2
|
import * as fs from "node:fs";
|
|
3
3
|
import { dirname, join, resolve } from "node:path";
|
|
4
4
|
import { stripVTControlCharacters } from "node:util";
|
|
5
|
+
import { validRecordedScope, type RecordedScope } from "./orchestrator-scope.ts";
|
|
6
|
+
import { projectCatalog } from "./orchestrator-catalog.ts";
|
|
7
|
+
import { decodePublishedState, type PublishedState } from "./orchestrator-state.ts";
|
|
8
|
+
import { decodeTaskAliases, type TaskAliases } from "./orchestrator-alias.ts";
|
|
5
9
|
|
|
6
10
|
// Same-profile OS-user trust boundary, not an authorization channel. POSIX modes
|
|
7
11
|
// restrict newly created storage; Windows deployments must supply their own ACLs.
|
|
@@ -27,6 +31,19 @@ export interface Activity { tasks: { summary: ActivityInput["task"]; thread: {
|
|
|
27
31
|
version: number; dropped: number; items: ObjectValue[];
|
|
28
32
|
} }[] }
|
|
29
33
|
export interface Target { sessionHash: string; incarnation: string }
|
|
34
|
+
export interface DiscoveryMetadata {
|
|
35
|
+
aliases?: TaskAliases;
|
|
36
|
+
state?: PublishedState;
|
|
37
|
+
scope?: RecordedScope;
|
|
38
|
+
activation: string;
|
|
39
|
+
workspace: string;
|
|
40
|
+
tasks: { id: string; label: string; status: string; workspace: string }[];
|
|
41
|
+
omitted: number;
|
|
42
|
+
}
|
|
43
|
+
export function sessionHash(sessionId: string) { return digest(sessionId); }
|
|
44
|
+
export function activationHash(peer: { sessionId: string; endpoint: string; createdAt: number }) {
|
|
45
|
+
return digest(JSON.stringify([peer.sessionId, peer.endpoint, peer.createdAt]));
|
|
46
|
+
}
|
|
30
47
|
export interface Header extends Target {
|
|
31
48
|
schema: 1; label: string; heartbeat: number; generation: number;
|
|
32
49
|
counts: { running: number; queued: number; waiting: number; finished: number };
|
|
@@ -37,10 +54,11 @@ const object = (v: unknown): v is ObjectValue => !!v && typeof v === "object" &&
|
|
|
37
54
|
const integer = (v: unknown) => Number.isSafeInteger(v) && (v as number) >= 0;
|
|
38
55
|
const keys = (v: ObjectValue, names: string[]) => Object.keys(v).length === names.length && names.every((k) => Object.hasOwn(v, k));
|
|
39
56
|
const digest = (data: string | Buffer) => createHash("sha256").update(data).digest("hex");
|
|
40
|
-
function
|
|
57
|
+
export function sanitizeDisplayLabel(text: string) {
|
|
41
58
|
const clean = stripVTControlCharacters(text).replace(/[\p{Cc}\p{Cf}]/gu, " ").replace(/\s+/g, " ").trim();
|
|
42
59
|
return Array.from(clean).slice(0, 120).join("").trimEnd();
|
|
43
60
|
}
|
|
61
|
+
const label = sanitizeDisplayLabel;
|
|
44
62
|
const pick = (v: ObjectValue, names: string[]) => Object.fromEntries(names.map((k) => [k, v[k]]));
|
|
45
63
|
const summaryTextKeys = ["id", "agent", "label", "status", "model"];
|
|
46
64
|
const summaryKeys = [...summaryTextKeys, "createdAt", "startedAt", "endedAt", "lastActivityAt"];
|
|
@@ -78,6 +96,15 @@ function validTarget(value: Target) {
|
|
|
78
96
|
return object(value) && typeof value.sessionHash === "string" && HASH.test(value.sessionHash)
|
|
79
97
|
&& typeof value.incarnation === "string" && UUID.test(value.incarnation);
|
|
80
98
|
}
|
|
99
|
+
function validDiscovery(d: unknown): d is DiscoveryMetadata {
|
|
100
|
+
return object(d) && keys(d, ["activation", "workspace", "tasks", "omitted", ...["scope", "state", "aliases"].filter(k => Object.hasOwn(d, k))])
|
|
101
|
+
&& typeof d.activation === "string" && HASH.test(d.activation)
|
|
102
|
+
&& typeof d.workspace === "string" && d.workspace === label(d.workspace) && integer(d.omitted)
|
|
103
|
+
&& Array.isArray(d.tasks) && d.tasks.length <= 8 && d.tasks.every((t: unknown) => object(t)
|
|
104
|
+
&& keys(t, ["id", "label", "status", "workspace"])
|
|
105
|
+
&& ["id", "label", "workspace"].every(k => typeof t[k] === "string" && t[k] === label(t[k]))
|
|
106
|
+
&& STATUSES.includes(t.status));
|
|
107
|
+
}
|
|
81
108
|
function validHeader(h: unknown): h is Header {
|
|
82
109
|
if (!object(h) || !keys(h, ["schema", "sessionHash", "incarnation", "label", "heartbeat", "generation", "counts", "digest", "unavailable"])) return false;
|
|
83
110
|
return validTarget(h as Header) && h.schema === 1 && typeof h.label === "string" && h.label === label(h.label)
|
|
@@ -86,7 +113,7 @@ function validHeader(h: unknown): h is Header {
|
|
|
86
113
|
&& ((h.unavailable === null && typeof h.digest === "string" && HASH.test(h.digest))
|
|
87
114
|
|| (h.unavailable === "activity-too-large" && h.digest === null));
|
|
88
115
|
}
|
|
89
|
-
function filename(target: Target, kind: "header" | "activity") {
|
|
116
|
+
function filename(target: Target, kind: "header" | "activity" | "discovery") {
|
|
90
117
|
if (!validTarget(target)) throw new Error("malformed");
|
|
91
118
|
return `${target.sessionHash}.${target.incarnation}.${kind}.json`;
|
|
92
119
|
}
|
|
@@ -96,7 +123,7 @@ function directory(path: string, privateMode = false, owned = privateMode) {
|
|
|
96
123
|
if (!stat.isDirectory() || stat.isSymbolicLink() || (owned && process.platform !== "win32"
|
|
97
124
|
&& ((stat.mode & (privateMode ? 0o077 : 0o022)) !== 0 || stat.uid !== process.getuid?.()))) throw new Error("unsafe-directory");
|
|
98
125
|
}
|
|
99
|
-
function rootFor(profile: string, create = false) {
|
|
126
|
+
export function rootFor(profile: string, create = false, catalog = false) {
|
|
100
127
|
const absolute = resolve(profile);
|
|
101
128
|
// Reject symlink ancestors too. The caller supplies an existing profile root.
|
|
102
129
|
for (let path = absolute;; path = dirname(path)) {
|
|
@@ -104,7 +131,7 @@ function rootFor(profile: string, create = false) {
|
|
|
104
131
|
if (dirname(path) === path) break;
|
|
105
132
|
}
|
|
106
133
|
const shared = join(absolute, "gentle-agents");
|
|
107
|
-
const root = join(shared, "presence");
|
|
134
|
+
const root = join(shared, catalog ? "catalog" : "presence");
|
|
108
135
|
// History may already own a 0755 shared root. Never change its permissions.
|
|
109
136
|
for (const path of [shared, root]) {
|
|
110
137
|
if (create) {
|
|
@@ -118,7 +145,7 @@ function rootFor(profile: string, create = false) {
|
|
|
118
145
|
function regular(stat: fs.Stats) {
|
|
119
146
|
if (!stat.isFile() || stat.nlink !== 1 || (process.platform !== "win32" && stat.uid !== process.getuid?.())) throw new Error("unsafe-file");
|
|
120
147
|
}
|
|
121
|
-
function boundedRead(path: string, limit: number) {
|
|
148
|
+
export function boundedRead(path: string, limit: number) {
|
|
122
149
|
const before = fs.lstatSync(path);
|
|
123
150
|
regular(before);
|
|
124
151
|
if (before.size > limit) throw new Error("oversized");
|
|
@@ -236,11 +263,29 @@ export function readActivity(profile: string, selection: Header): { activity?: A
|
|
|
236
263
|
} catch (error) { return { unavailable: reason(error) }; }
|
|
237
264
|
}
|
|
238
265
|
|
|
266
|
+
/** Optional derived sidecar; failures never invalidate the schema-1 header. */
|
|
267
|
+
export function readDiscovery(profile: string, h: Header): DiscoveryMetadata | undefined {
|
|
268
|
+
try {
|
|
269
|
+
const value = JSON.parse(boundedRead(join(rootFor(profile), filename(h, "discovery")), HEADER_LIMIT).toString("utf8"));
|
|
270
|
+
if (!object(value) || !keys(value, ["schema", "sessionHash", "incarnation", "generation", "metadata"])
|
|
271
|
+
|| value.schema !== 1 || value.sessionHash !== h.sessionHash || value.incarnation !== h.incarnation
|
|
272
|
+
|| value.generation !== h.generation || !validDiscovery(value.metadata)) return undefined;
|
|
273
|
+
const { scope, state, aliases: rawAliases, ...legacy } = value.metadata;
|
|
274
|
+
const decoded = decodePublishedState(state);
|
|
275
|
+
const aliases = decodeTaskAliases(rawAliases);
|
|
276
|
+
return { ...legacy, ...(aliases ? { aliases } : {}), ...(validRecordedScope(scope) ? { scope } : {}), ...(decoded ? { state: decoded } : {}) };
|
|
277
|
+
} catch { return undefined; }
|
|
278
|
+
}
|
|
279
|
+
|
|
239
280
|
export class PresencePublisher {
|
|
240
281
|
readonly target: Readonly<Target>;
|
|
241
282
|
private readonly profile: string;
|
|
242
283
|
private readonly displayLabel: string;
|
|
284
|
+
private readonly labelSource?: () => string;
|
|
243
285
|
private header!: Header;
|
|
286
|
+
private discovery?: DiscoveryMetadata;
|
|
287
|
+
private catalog?: ReturnType<typeof projectCatalog>;
|
|
288
|
+
private catalogTokens: string[] = [];
|
|
244
289
|
private published = "";
|
|
245
290
|
private pending = "";
|
|
246
291
|
private timer?: ReturnType<typeof setTimeout>;
|
|
@@ -250,12 +295,13 @@ export class PresencePublisher {
|
|
|
250
295
|
/** Timer I/O failures stop publication; consumers still apply the recent TTL. */
|
|
251
296
|
error?: string;
|
|
252
297
|
|
|
253
|
-
private constructor(options: { profile: string; sessionId: string; label: string }) {
|
|
298
|
+
private constructor(options: { profile: string; sessionId: string; label: string; labelSource?: () => string }) {
|
|
254
299
|
this.profile = options.profile;
|
|
255
300
|
this.displayLabel = label(options.label);
|
|
301
|
+
this.labelSource = options.labelSource;
|
|
256
302
|
this.target = Object.freeze({ sessionHash: digest(options.sessionId), incarnation: randomUUID() });
|
|
257
303
|
}
|
|
258
|
-
static start(options: { profile: string; sessionId: string; label: string; activity: readonly ActivityInput[] }) {
|
|
304
|
+
static start(options: { profile: string; sessionId: string; label: string; labelSource?: () => string; activity: readonly ActivityInput[] }) {
|
|
259
305
|
const publisher = new PresencePublisher(options);
|
|
260
306
|
try {
|
|
261
307
|
rootFor(options.profile, true);
|
|
@@ -266,6 +312,50 @@ export class PresencePublisher {
|
|
|
266
312
|
return publisher;
|
|
267
313
|
} catch (error) { publisher.dispose(); throw error; }
|
|
268
314
|
}
|
|
315
|
+
/** Refresh through the existing publication path, without changing activity generation.
|
|
316
|
+
* The source must be a cheap canonical-name getter; throw when its session is stale. */
|
|
317
|
+
refreshLabel() {
|
|
318
|
+
this.guarded(() => this.publishHeader());
|
|
319
|
+
}
|
|
320
|
+
/** Metadata-only projection; never reads task prompts, results, or threads.
|
|
321
|
+
* Binding to the listener activation prevents reused session IDs from joining. */
|
|
322
|
+
updateDiscovery(peer: { sessionId: string; endpoint: string; createdAt: number }, input: {
|
|
323
|
+
workspace: string; tasks: readonly { id: string; label: string; status: string; cwd: string }[]; scope?: RecordedScope; registered?: readonly string[]; state?: PublishedState;
|
|
324
|
+
}) {
|
|
325
|
+
if (this.disposed) throw new Error("disposed");
|
|
326
|
+
if (sessionHash(peer.sessionId) !== this.target.sessionHash) throw new Error("malformed-discovery");
|
|
327
|
+
// A shortened path could denote a different workspace; do not advertise it.
|
|
328
|
+
const workspace = (path: string) => Array.from(path).length <= 120 && label(path) === path ? path : "";
|
|
329
|
+
const discovery: DiscoveryMetadata = { activation: activationHash(peer), workspace: workspace(input.workspace),
|
|
330
|
+
tasks: input.tasks.slice(0, 8).map(t => ({ id: label(t.id), label: label(t.label), status: t.status, workspace: workspace(t.cwd) })),
|
|
331
|
+
omitted: Math.max(0, input.tasks.length - 8) };
|
|
332
|
+
const state = decodePublishedState(input.state);
|
|
333
|
+
if (state?.sessionId === peer.sessionId) {
|
|
334
|
+
discovery.state = state;
|
|
335
|
+
if (state.aliases) discovery.aliases = structuredClone(state.aliases);
|
|
336
|
+
}
|
|
337
|
+
if (input.scope && validRecordedScope(input.scope)) {
|
|
338
|
+
discovery.scope = structuredClone(input.scope);
|
|
339
|
+
// Leave explicit gaps instead of publishing a sidecar readers cannot fit.
|
|
340
|
+
if (Buffer.byteLength(JSON.stringify(discovery)) > HEADER_LIMIT - 1024) {
|
|
341
|
+
const scope = discovery.scope;
|
|
342
|
+
discovery.scope = { ...scope, tasks: [], registered: [],
|
|
343
|
+
omittedTasks: scope.tasks.length + scope.omittedTasks,
|
|
344
|
+
omittedRegistered: scope.registered.length + scope.omittedRegistered, complete: false };
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
if (Buffer.byteLength(JSON.stringify(discovery)) > HEADER_LIMIT - 1024) delete discovery.state;
|
|
348
|
+
if (!validDiscovery(discovery)) throw new Error("malformed-discovery");
|
|
349
|
+
const catalog = projectCatalog(input.tasks, input.registered ?? []);
|
|
350
|
+
if (JSON.stringify(discovery) === JSON.stringify(this.discovery) && JSON.stringify(catalog) === JSON.stringify(this.catalog)) return;
|
|
351
|
+
// Only catalog or routing-activation changes rotate page tokens; private
|
|
352
|
+
// activity generations and unrelated legacy metadata do not invalidate them.
|
|
353
|
+
if (JSON.stringify(catalog) !== JSON.stringify(this.catalog) || discovery.activation !== this.discovery?.activation)
|
|
354
|
+
this.catalogTokens = Array.from({ length: 7 }, () => randomUUID());
|
|
355
|
+
this.catalog = catalog;
|
|
356
|
+
this.discovery = discovery;
|
|
357
|
+
this.writeDiscovery();
|
|
358
|
+
}
|
|
269
359
|
/** Eagerly projects/serializes each supplied snapshot to detach caller-owned data.
|
|
270
360
|
* Only disk publication is coalesced; callers should avoid unrelated invalidations. */
|
|
271
361
|
update(input: readonly ActivityInput[]) {
|
|
@@ -285,18 +375,29 @@ export class PresencePublisher {
|
|
|
285
375
|
if (this.disposed) return;
|
|
286
376
|
try { action(); } catch (error) { this.error = reason(error); this.dispose(); }
|
|
287
377
|
}
|
|
288
|
-
private write(kind: "header" | "activity", bytes: string) {
|
|
289
|
-
const root = rootFor(this.profile);
|
|
290
|
-
const name = filename(this.target, kind);
|
|
378
|
+
private write(kind: "header" | "activity" | "discovery" | "catalog", bytes: string) {
|
|
379
|
+
const root = rootFor(this.profile, kind === "catalog", kind === "catalog");
|
|
380
|
+
const name = kind === "catalog" ? `${this.target.sessionHash}.${this.target.incarnation}.json` : filename(this.target, kind);
|
|
291
381
|
atomicWrite(root, name, bytes);
|
|
292
382
|
const { dev, ino } = fs.lstatSync(join(root, name));
|
|
293
|
-
this.owned.set(name, { dev, ino });
|
|
383
|
+
this.owned.set(join(root, name), { dev, ino });
|
|
384
|
+
}
|
|
385
|
+
private writeDiscovery() {
|
|
386
|
+
if (!this.discovery) return;
|
|
387
|
+
try {
|
|
388
|
+
if (this.catalog) this.write("catalog", JSON.stringify({ schema: 1, ...this.target,
|
|
389
|
+
generation: this.header.generation, activation: this.discovery.activation, tokens: this.catalogTokens, catalog: this.catalog }));
|
|
390
|
+
} catch { /* Optional catalog failure must not suppress other metadata. */ }
|
|
391
|
+
try {
|
|
392
|
+
this.write("discovery", JSON.stringify({ schema: 1, ...this.target, generation: this.header.generation, metadata: this.discovery }));
|
|
393
|
+
} catch { /* Metadata failure must not withdraw an existing activity peer. */ }
|
|
294
394
|
}
|
|
295
395
|
private publishHeader() {
|
|
296
|
-
const header = { ...this.header, heartbeat: Date.now() };
|
|
396
|
+
const header = { ...this.header, label: label(this.labelSource?.() ?? this.displayLabel), heartbeat: Date.now() };
|
|
297
397
|
if (!validHeader(header)) throw new Error("malformed-header");
|
|
298
398
|
this.write("header", JSON.stringify(header));
|
|
299
399
|
this.header = header;
|
|
400
|
+
this.writeDiscovery();
|
|
300
401
|
}
|
|
301
402
|
private flush() {
|
|
302
403
|
if (this.pending === this.published) return;
|
|
@@ -324,9 +425,9 @@ export class PresencePublisher {
|
|
|
324
425
|
clearTimeout(this.timer);
|
|
325
426
|
clearInterval(this.heartbeat);
|
|
326
427
|
this.pending = this.published = "";
|
|
327
|
-
for (const [
|
|
428
|
+
for (const [path, identity] of this.owned) {
|
|
328
429
|
try {
|
|
329
|
-
|
|
430
|
+
rootFor(this.profile, false, dirname(path) === join(resolve(this.profile), "gentle-agents", "catalog"));
|
|
330
431
|
const stat = fs.lstatSync(path);
|
|
331
432
|
regular(stat);
|
|
332
433
|
if (stat.dev === identity.dev && stat.ino === identity.ino) fs.unlinkSync(path);
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { isAbsolute } from "node:path";
|
|
3
|
+
import { resolveSessionWorktree, type WorktreeResolver } from "./session-worktree-registry.ts";
|
|
4
|
+
|
|
5
|
+
export interface RepositoryFact {
|
|
6
|
+
root: string | null;
|
|
7
|
+
cloneHash: string | null;
|
|
8
|
+
resolvedAt: number;
|
|
9
|
+
source: "recorded-workspace/git";
|
|
10
|
+
}
|
|
11
|
+
export interface RecordedScope {
|
|
12
|
+
host: RepositoryFact;
|
|
13
|
+
tasks: { id: string; repository: RepositoryFact }[];
|
|
14
|
+
registered: RepositoryFact[];
|
|
15
|
+
omittedTasks: number;
|
|
16
|
+
omittedRegistered: number;
|
|
17
|
+
complete: boolean;
|
|
18
|
+
}
|
|
19
|
+
// Recorded cwd/root is literal, not a user spelling. The shared resolver maps
|
|
20
|
+
// these Unicode separators to ASCII space; reject them before resolving and on
|
|
21
|
+
// output/readback rather than aliasing a distinct directory or truncating a root.
|
|
22
|
+
const safeRoot = (root: string) => isAbsolute(root) && Buffer.byteLength(root) <= 256
|
|
23
|
+
&& !/[\p{Cc}\p{Cf}\u00a0\u2000-\u200a\u202f\u205f\u3000]/u.test(root);
|
|
24
|
+
const object = (v: any) => v && typeof v === "object" && !Array.isArray(v);
|
|
25
|
+
const keys = (v: object, names: string[]) => Object.keys(v).length === names.length && names.every(k => Object.hasOwn(v, k));
|
|
26
|
+
const count = (v: unknown) => Number.isSafeInteger(v) && (v as number) >= 0;
|
|
27
|
+
export function validRecordedScope(value: any): value is RecordedScope {
|
|
28
|
+
const fact = (v: any) => object(v) && keys(v, ["cloneHash", "resolvedAt", "root", "source"])
|
|
29
|
+
&& v.source === "recorded-workspace/git" && count(v.resolvedAt)
|
|
30
|
+
&& ((v.root === null && v.cloneHash === null) || (typeof v.root === "string" && safeRoot(v.root)
|
|
31
|
+
&& typeof v.cloneHash === "string" && /^[a-f0-9]{64}$/.test(v.cloneHash)));
|
|
32
|
+
return object(value) && keys(value, ["complete", "host", "omittedRegistered", "omittedTasks", "registered", "tasks"])
|
|
33
|
+
&& fact(value.host) && Array.isArray(value.tasks) && value.tasks.length <= 8
|
|
34
|
+
&& value.tasks.every((t: any) => object(t) && keys(t, ["id", "repository"])
|
|
35
|
+
&& typeof t.id === "string" && Buffer.byteLength(t.id) <= 512 && fact(t.repository))
|
|
36
|
+
&& Array.isArray(value.registered) && value.registered.length <= 8 && value.registered.every(fact)
|
|
37
|
+
&& count(value.omittedTasks) && count(value.omittedRegistered)
|
|
38
|
+
&& value.complete === (value.omittedTasks === 0 && value.omittedRegistered === 0);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** One bounded derivative snapshot, not a registry or authority. The caller supplies
|
|
42
|
+
* only admitted, unfinished, non-restored owned children and durable registry roots.
|
|
43
|
+
* Membership/cwd changes invalidate even unknown facts; session replacement clears.
|
|
44
|
+
* No resolution runs on a stable heartbeat/token update. */
|
|
45
|
+
export class OrchestratorScopeCache {
|
|
46
|
+
private key = "";
|
|
47
|
+
private snapshot?: RecordedScope;
|
|
48
|
+
private resolver: WorktreeResolver;
|
|
49
|
+
private now: () => number;
|
|
50
|
+
constructor(resolver = resolveSessionWorktree, now = Date.now) {
|
|
51
|
+
this.resolver = resolver;
|
|
52
|
+
this.now = now;
|
|
53
|
+
}
|
|
54
|
+
clear() {
|
|
55
|
+
this.key = "";
|
|
56
|
+
this.snapshot = undefined;
|
|
57
|
+
}
|
|
58
|
+
project(cwd: string, tasks: readonly { id: string; cwd: string }[], roots: readonly string[]): RecordedScope {
|
|
59
|
+
const registered = [...new Set(roots)];
|
|
60
|
+
const key = createHash("sha256").update(JSON.stringify([cwd, tasks.map(t => [t.id, t.cwd]), registered])).digest("hex");
|
|
61
|
+
if (key === this.key && this.snapshot) return this.snapshot;
|
|
62
|
+
const resolvedAt = this.now();
|
|
63
|
+
const facts = new Map<string, RepositoryFact>();
|
|
64
|
+
const fact = (path: string) => {
|
|
65
|
+
if (facts.has(path)) return facts.get(path)!;
|
|
66
|
+
const unknown: RepositoryFact = { root: null, cloneHash: null, resolvedAt, source: "recorded-workspace/git" };
|
|
67
|
+
let identity;
|
|
68
|
+
try { identity = safeRoot(path) ? this.resolver(path, path) : undefined; } catch { /* Unavailable is unknown. */ }
|
|
69
|
+
const result = identity && safeRoot(identity.root) ? { ...unknown, root: identity.root,
|
|
70
|
+
cloneHash: createHash("sha256").update(identity.commonDir).digest("hex") } : unknown;
|
|
71
|
+
facts.set(path, result);
|
|
72
|
+
return result;
|
|
73
|
+
};
|
|
74
|
+
this.snapshot = { host: fact(cwd), tasks: tasks.slice(0, 8).map(t => ({ id: t.id, repository: fact(t.cwd) })),
|
|
75
|
+
registered: registered.slice(0, 8).map(fact), omittedTasks: Math.max(0, tasks.length - 8),
|
|
76
|
+
omittedRegistered: Math.max(0, registered.length - 8), complete: tasks.length <= 8 && registered.length <= 8 };
|
|
77
|
+
this.key = key;
|
|
78
|
+
return this.snapshot;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { isAbsolute } from "node:path";
|
|
2
|
+
import { decodeTaskAliases, normalizeTaskSubject, type TaskAliases } from "./orchestrator-alias.ts";
|
|
3
|
+
import { decodeWork, type PublishedWork } from "./orchestrator-work.ts";
|
|
4
|
+
|
|
5
|
+
export const ORCHESTRATOR_STATE_ENTRY = "gentle-agents.published-state";
|
|
6
|
+
const fields = ["objective", "progress", "decisions", "blockers"] as const;
|
|
7
|
+
export type CuratedState = Partial<Record<typeof fields[number], string>> & { work?: PublishedWork };
|
|
8
|
+
export interface PublishedState {
|
|
9
|
+
schema: 1 | 2; sessionId: string; recordedAt: number; cwd: string | null;
|
|
10
|
+
source: "owner-curated"; ownerReply: false; authority: "none"; state: CuratedState | null;
|
|
11
|
+
aliases?: TaskAliases;
|
|
12
|
+
}
|
|
13
|
+
const object = (v: unknown): v is Record<string, unknown> => !!v && typeof v === "object" && !Array.isArray(v);
|
|
14
|
+
const safeText = (v: unknown): v is string => typeof v === "string" && !/[\p{Cc}\p{Cf}\p{Cs}]/u.test(v);
|
|
15
|
+
const safeCwd = (v: unknown): v is string => safeText(v) && isAbsolute(v) && Buffer.byteLength(v) <= 1024
|
|
16
|
+
&& !/[\u00a0\u2000-\u200a\u202f\u205f\u3000]/u.test(v);
|
|
17
|
+
/** Field whitelist, not secret redaction. Reject rather than truncate meaning. */
|
|
18
|
+
export function decodeCuratedState(value: unknown): CuratedState | null {
|
|
19
|
+
if (value === null) return null;
|
|
20
|
+
if (!object(value) || Object.keys(value).some(k => k !== "work" && !fields.includes(k as typeof fields[number])))
|
|
21
|
+
throw new Error("invalid-published-state");
|
|
22
|
+
const state: CuratedState = {};
|
|
23
|
+
let bytes = 0;
|
|
24
|
+
for (const key of fields) {
|
|
25
|
+
if (!Object.hasOwn(value, key)) continue;
|
|
26
|
+
if (!safeText(value[key])) throw new Error("invalid-published-state");
|
|
27
|
+
state[key] = value[key];
|
|
28
|
+
bytes += Buffer.byteLength(value[key]);
|
|
29
|
+
}
|
|
30
|
+
if (Object.hasOwn(value, "work")) {
|
|
31
|
+
state.work = decodeWork(value.work);
|
|
32
|
+
bytes += Buffer.byteLength(JSON.stringify(state.work));
|
|
33
|
+
}
|
|
34
|
+
if (bytes > 2048) throw new Error("invalid-published-state");
|
|
35
|
+
return state;
|
|
36
|
+
}
|
|
37
|
+
export function decodePublishedState(value: unknown): PublishedState | undefined {
|
|
38
|
+
try {
|
|
39
|
+
const keys = ["schema", "sessionId", "recordedAt", "cwd", "source", "ownerReply", "authority", "state",
|
|
40
|
+
...(object(value) && Object.hasOwn(value, "aliases") ? ["aliases"] : [])];
|
|
41
|
+
if (!object(value) || Object.keys(value).length !== keys.length || !keys.every(k => Object.hasOwn(value, k))
|
|
42
|
+
|| (value.schema !== 1 && value.schema !== 2) || !safeText(value.sessionId) || !value.sessionId || Buffer.byteLength(value.sessionId) > 256
|
|
43
|
+
|| !Number.isSafeInteger(value.recordedAt) || (value.recordedAt as number) < 0
|
|
44
|
+
|| !(value.cwd === null || safeCwd(value.cwd))
|
|
45
|
+
|| value.source !== "owner-curated" || value.ownerReply !== false || value.authority !== "none"
|
|
46
|
+
|| Buffer.byteLength(JSON.stringify(value)) > 4096) return undefined;
|
|
47
|
+
const state = decodeCuratedState(value.state);
|
|
48
|
+
const aliases = decodeTaskAliases(value.aliases);
|
|
49
|
+
if ((value.schema === 2) !== !!state?.work || (Object.hasOwn(value, "aliases") && !aliases)) return undefined;
|
|
50
|
+
return { schema: value.schema, sessionId: value.sessionId, recordedAt: value.recordedAt as number,
|
|
51
|
+
cwd: safeText(value.cwd) ? value.cwd : null, source: "owner-curated", ownerReply: false, authority: "none", state,
|
|
52
|
+
...(aliases ? { aliases } : {}) };
|
|
53
|
+
} catch { return undefined; }
|
|
54
|
+
}
|
|
55
|
+
interface StateManager {
|
|
56
|
+
getSessionId(): string; getCwd(): string;
|
|
57
|
+
getBranch(): readonly { type: string; customType?: string; data?: unknown }[];
|
|
58
|
+
}
|
|
59
|
+
/** One branch-local cache. Never accesses message bodies, summaries, or results. */
|
|
60
|
+
export class OrchestratorStateCache {
|
|
61
|
+
private manager?: StateManager;
|
|
62
|
+
private sessionId?: string;
|
|
63
|
+
private value?: PublishedState;
|
|
64
|
+
private epoch = 0;
|
|
65
|
+
clear() { this.epoch++; this.manager = undefined; this.sessionId = undefined; this.value = undefined; }
|
|
66
|
+
load(manager: StateManager) {
|
|
67
|
+
this.clear();
|
|
68
|
+
this.manager = manager;
|
|
69
|
+
this.sessionId = manager.getSessionId();
|
|
70
|
+
const branch = manager.getBranch();
|
|
71
|
+
for (let i = branch.length - 1; i >= 0; i--) {
|
|
72
|
+
const entry = branch[i];
|
|
73
|
+
if (entry.type !== "custom" || entry.customType !== ORCHESTRATOR_STATE_ENTRY) continue;
|
|
74
|
+
const decoded = decodePublishedState(entry.data);
|
|
75
|
+
// A malformed/foreign latest record suppresses older knowledge.
|
|
76
|
+
this.value = decoded?.sessionId === this.sessionId ? decoded : undefined;
|
|
77
|
+
break;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
get(manager: StateManager): PublishedState | undefined {
|
|
81
|
+
return this.manager === manager && this.sessionId === manager.getSessionId() && this.value
|
|
82
|
+
? structuredClone(this.value) : undefined;
|
|
83
|
+
}
|
|
84
|
+
publish(manager: StateManager, input: unknown, append: (type: string, data: PublishedState) => void, now = Date.now(), subject?: unknown) {
|
|
85
|
+
const state = input === undefined ? this.value?.state ?? null : decodeCuratedState(input);
|
|
86
|
+
const declared = normalizeTaskSubject(subject);
|
|
87
|
+
if (this.manager !== manager || this.sessionId !== manager.getSessionId()) throw new Error("stale-published-state");
|
|
88
|
+
const epoch = this.epoch;
|
|
89
|
+
const previous = this.value?.aliases;
|
|
90
|
+
// Null/null marks a new publication with no declaration yet; legacy absence
|
|
91
|
+
// cannot establish historical first-task knowledge.
|
|
92
|
+
let aliases = previous;
|
|
93
|
+
if (declared) {
|
|
94
|
+
let initialAlias = previous?.initialAlias ?? null;
|
|
95
|
+
if (!this.value || previous?.currentAlias === null) initialAlias = declared;
|
|
96
|
+
aliases = { initialAlias, currentAlias: declared };
|
|
97
|
+
} else if (!this.value) aliases = { initialAlias: null, currentAlias: null };
|
|
98
|
+
const cwd = input === undefined && this.value ? this.value.cwd : manager.getCwd();
|
|
99
|
+
const record = decodePublishedState({ schema: state?.work ? 2 : 1, sessionId: manager.getSessionId(),
|
|
100
|
+
recordedAt: input === undefined && this.value ? this.value.recordedAt : now, cwd: safeCwd(cwd) ? cwd : null,
|
|
101
|
+
source: "owner-curated", ownerReply: false, authority: "none", state, ...(aliases ? { aliases } : {}) });
|
|
102
|
+
if (!record) throw new Error("invalid-published-state");
|
|
103
|
+
append(ORCHESTRATOR_STATE_ENTRY, structuredClone(record));
|
|
104
|
+
if (this.epoch !== epoch || this.manager !== manager || this.sessionId !== record.sessionId || manager.getSessionId() !== record.sessionId) throw new Error("stale-published-state");
|
|
105
|
+
this.value = record;
|
|
106
|
+
}
|
|
107
|
+
}
|