pi-crew 0.9.53 → 0.9.55
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/CHANGELOG.md +45 -0
- package/package.json +6 -3
- package/scripts/clean-strip-types.mjs +60 -0
- package/src/adapters/claude-adapter.ts +23 -0
- package/src/adapters/codex-adapter.ts +21 -0
- package/src/adapters/cursor-adapter.ts +17 -0
- package/src/adapters/export-util.ts +143 -0
- package/src/adapters/index.ts +15 -0
- package/src/adapters/registry.ts +18 -0
- package/src/adapters/types.ts +23 -0
- package/src/agents/agent-config.ts +194 -0
- package/src/agents/agent-search.ts +98 -0
- package/src/agents/agent-serializer.ts +38 -0
- package/src/agents/discover-agents.ts +623 -0
- package/src/benchmark/benchmark-runner.ts +313 -0
- package/src/benchmark/feedback-loop.ts +73 -0
- package/src/config/config.ts +1270 -0
- package/src/config/defaults.ts +193 -0
- package/src/config/drift-detector.ts +290 -0
- package/src/config/markers.ts +330 -0
- package/src/config/resilient-parser.ts +117 -0
- package/src/config/role-tools.ts +118 -0
- package/src/config/suggestions.ts +75 -0
- package/src/config/types.ts +280 -0
- package/src/errors.ts +194 -0
- package/src/extension/action-suggestions.ts +117 -0
- package/src/extension/async-notifier.ts +179 -0
- package/src/extension/autonomous-policy.ts +209 -0
- package/src/extension/command-completions.ts +120 -0
- package/src/extension/context-status-injection.ts +185 -0
- package/src/extension/crew-autocomplete.ts +131 -0
- package/src/extension/crew-cleanup.ts +168 -0
- package/src/extension/crew-input-router.ts +93 -0
- package/src/extension/crew-shortcuts.ts +87 -0
- package/src/extension/crew-vibes/cat-frames.ts +18 -0
- package/src/extension/crew-vibes/config.ts +195 -0
- package/src/extension/crew-vibes/figures.ts +101 -0
- package/src/extension/crew-vibes/font-detect.ts +71 -0
- package/src/extension/crew-vibes/footer.ts +292 -0
- package/src/extension/crew-vibes/index.ts +432 -0
- package/src/extension/crew-vibes/provider-usage.ts +396 -0
- package/src/extension/crew-vibes/render.ts +206 -0
- package/src/extension/crew-vibes/speed.ts +286 -0
- package/src/extension/cross-extension-rpc.ts +350 -0
- package/src/extension/help.ts +62 -0
- package/src/extension/import-index.ts +74 -0
- package/src/extension/knowledge-injection.ts +402 -0
- package/src/extension/management.ts +571 -0
- package/src/extension/message-renderers.ts +108 -0
- package/src/extension/notification-router.ts +152 -0
- package/src/extension/notification-sink.ts +54 -0
- package/src/extension/pi-api.ts +56 -0
- package/src/extension/plan-orchestrate.ts +302 -0
- package/src/extension/project-init.ts +170 -0
- package/src/extension/register.ts +113 -0
- package/src/extension/registration/artifact-cleanup.ts +20 -0
- package/src/extension/registration/command-registration.ts +58 -0
- package/src/extension/registration/command-utils.ts +58 -0
- package/src/extension/registration/commands.ts +1228 -0
- package/src/extension/registration/compaction-guard.ts +333 -0
- package/src/extension/registration/context-builder.ts +135 -0
- package/src/extension/registration/crash-recovery-cache.ts +54 -0
- package/src/extension/registration/foreground-run-controller.ts +238 -0
- package/src/extension/registration/hook-registration.ts +119 -0
- package/src/extension/registration/lazy-configurers.ts +124 -0
- package/src/extension/registration/lifecycle-handlers.ts +923 -0
- package/src/extension/registration/lifecycle.ts +259 -0
- package/src/extension/registration/observability.ts +322 -0
- package/src/extension/registration/registration-types.ts +158 -0
- package/src/extension/registration/runtime-cleanup.ts +230 -0
- package/src/extension/registration/subagent-helpers.ts +125 -0
- package/src/extension/registration/subagent-manager-setup.ts +330 -0
- package/src/extension/registration/subagent-tools.ts +456 -0
- package/src/extension/registration/team-tool.ts +225 -0
- package/src/extension/registration/tool-registration.ts +50 -0
- package/src/extension/registration/ui.ts +172 -0
- package/src/extension/registration/viewers.ts +121 -0
- package/src/extension/registration/wire-cross-extension.ts +37 -0
- package/src/extension/result-watcher.ts +139 -0
- package/src/extension/rpc-hmac.ts +256 -0
- package/src/extension/run-bundle-schema.ts +104 -0
- package/src/extension/run-export.ts +97 -0
- package/src/extension/run-import.ts +168 -0
- package/src/extension/run-index.ts +131 -0
- package/src/extension/run-maintenance.ts +190 -0
- package/src/extension/session-summary.ts +18 -0
- package/src/extension/team-manager-command.ts +153 -0
- package/src/extension/team-onboard.ts +180 -0
- package/src/extension/team-recommendation.ts +288 -0
- package/src/extension/team-tool/anchor.ts +172 -0
- package/src/extension/team-tool/api.ts +1244 -0
- package/src/extension/team-tool/auto-summarize.ts +142 -0
- package/src/extension/team-tool/cache-control.ts +19 -0
- package/src/extension/team-tool/cancel.ts +337 -0
- package/src/extension/team-tool/chain-dispatch.ts +95 -0
- package/src/extension/team-tool/chain-executor.ts +379 -0
- package/src/extension/team-tool/config-patch.ts +50 -0
- package/src/extension/team-tool/context.ts +149 -0
- package/src/extension/team-tool/destructive-gate.ts +52 -0
- package/src/extension/team-tool/dispatch/automate.ts +80 -0
- package/src/extension/team-tool/dispatch/control.ts +42 -0
- package/src/extension/team-tool/dispatch/index.ts +95 -0
- package/src/extension/team-tool/dispatch/manage.ts +161 -0
- package/src/extension/team-tool/dispatch/run.ts +53 -0
- package/src/extension/team-tool/dispatch/status.ts +187 -0
- package/src/extension/team-tool/doctor.ts +431 -0
- package/src/extension/team-tool/explain.ts +282 -0
- package/src/extension/team-tool/failure-patterns.ts +118 -0
- package/src/extension/team-tool/goal-wrap.ts +330 -0
- package/src/extension/team-tool/goal.ts +556 -0
- package/src/extension/team-tool/handle-schedule.ts +332 -0
- package/src/extension/team-tool/handle-settings.ts +520 -0
- package/src/extension/team-tool/health-monitor.ts +481 -0
- package/src/extension/team-tool/inspect.ts +88 -0
- package/src/extension/team-tool/intent-policy.ts +45 -0
- package/src/extension/team-tool/lifecycle-actions.ts +615 -0
- package/src/extension/team-tool/orchestrate.ts +98 -0
- package/src/extension/team-tool/parallel-dispatch.ts +183 -0
- package/src/extension/team-tool/plan.ts +50 -0
- package/src/extension/team-tool/respond.ts +150 -0
- package/src/extension/team-tool/run-deadline.ts +66 -0
- package/src/extension/team-tool/run-not-found.ts +49 -0
- package/src/extension/team-tool/run.ts +1051 -0
- package/src/extension/team-tool/status.ts +260 -0
- package/src/extension/team-tool/workflow-manage.ts +261 -0
- package/src/extension/team-tool-types.ts +26 -0
- package/src/extension/team-tool.ts +750 -0
- package/src/extension/tool-result.ts +16 -0
- package/src/extension/validate-resources.ts +108 -0
- package/src/hooks/registry.ts +200 -0
- package/src/hooks/types.ts +69 -0
- package/src/i18n.ts +212 -0
- package/src/observability/correlation.ts +50 -0
- package/src/observability/event-bus.ts +86 -0
- package/src/observability/event-to-metric.ts +170 -0
- package/src/observability/exporters/adapter.ts +27 -0
- package/src/observability/exporters/otlp-exporter.ts +356 -0
- package/src/observability/exporters/prometheus-exporter.ts +54 -0
- package/src/observability/metric-registry.ts +100 -0
- package/src/observability/metric-retention.ts +64 -0
- package/src/observability/metric-sink.ts +99 -0
- package/src/observability/metrics-primitives.ts +219 -0
- package/src/plugins/plugin-define.ts +6 -0
- package/src/plugins/plugin-registry.ts +32 -0
- package/src/plugins/plugins/index.ts +3 -0
- package/src/plugins/plugins/nextjs.ts +19 -0
- package/src/plugins/plugins/vite.ts +10 -0
- package/src/plugins/plugins/vitest.ts +9 -0
- package/src/prompt/prompt-runtime.ts +380 -0
- package/src/runtime/adaptive-plan.ts +563 -0
- package/src/runtime/agent-control.ts +215 -0
- package/src/runtime/agent-memory.ts +81 -0
- package/src/runtime/agent-observability.ts +122 -0
- package/src/runtime/anchor-manager.ts +475 -0
- package/src/runtime/async-marker.ts +32 -0
- package/src/runtime/async-runner.ts +381 -0
- package/src/runtime/attention-events.ts +28 -0
- package/src/runtime/auto-summarize.ts +344 -0
- package/src/runtime/background-runner.ts +840 -0
- package/src/runtime/batch-barrier.ts +147 -0
- package/src/runtime/broker-issuer.ts +39 -0
- package/src/runtime/cancellation-token.ts +99 -0
- package/src/runtime/cancellation.ts +99 -0
- package/src/runtime/capability-inventory.ts +137 -0
- package/src/runtime/chain-parser.ts +237 -0
- package/src/runtime/chain-runner.ts +606 -0
- package/src/runtime/checkpoint.ts +291 -0
- package/src/runtime/child-pi-constants.ts +42 -0
- package/src/runtime/child-pi-kill.ts +180 -0
- package/src/runtime/child-pi-pool.ts +68 -0
- package/src/runtime/child-pi-spawn.ts +287 -0
- package/src/runtime/child-pi-steering.ts +128 -0
- package/src/runtime/child-pi-streams.ts +296 -0
- package/src/runtime/child-pi-transcript.ts +169 -0
- package/src/runtime/child-pi.ts +1105 -0
- package/src/runtime/coalesce-tasks.ts +268 -0
- package/src/runtime/code-summary.ts +292 -0
- package/src/runtime/command-trace.ts +105 -0
- package/src/runtime/compact-pipeline.ts +56 -0
- package/src/runtime/compact-stages/ansi-strip-stage.ts +25 -0
- package/src/runtime/compact-stages/blank-collapse-stage.ts +31 -0
- package/src/runtime/compact-stages/deduplicate-stage.ts +34 -0
- package/src/runtime/compact-stages/head-snap-stage.ts +57 -0
- package/src/runtime/compact-stages/index.ts +23 -0
- package/src/runtime/compact-stages/tail-capture-stage.ts +77 -0
- package/src/runtime/compact-stages/truncation-stage.ts +74 -0
- package/src/runtime/compaction-summary.ts +278 -0
- package/src/runtime/completion-guard.ts +207 -0
- package/src/runtime/concurrency.ts +58 -0
- package/src/runtime/crash-classification.ts +229 -0
- package/src/runtime/crash-recovery.ts +594 -0
- package/src/runtime/crew-agent-records.ts +595 -0
- package/src/runtime/crew-agent-runtime.ts +62 -0
- package/src/runtime/crew-broker-child.ts +88 -0
- package/src/runtime/crew-broker-client.ts +673 -0
- package/src/runtime/crew-broker-tokens.ts +159 -0
- package/src/runtime/crew-broker.ts +1304 -0
- package/src/runtime/crew-hooks.ts +227 -0
- package/src/runtime/cross-extension-rpc.ts +143 -0
- package/src/runtime/custom-tools/irc-tool.ts +282 -0
- package/src/runtime/custom-tools/submit-result-tool.ts +102 -0
- package/src/runtime/deadletter.ts +47 -0
- package/src/runtime/delivery-coordinator.ts +211 -0
- package/src/runtime/delta-conflict.ts +348 -0
- package/src/runtime/deterministic-ast.ts +161 -0
- package/src/runtime/diagnostic-export.ts +171 -0
- package/src/runtime/direct-run.ts +45 -0
- package/src/runtime/dwf-state-store.ts +102 -0
- package/src/runtime/dynamic-workflow-context.ts +1013 -0
- package/src/runtime/dynamic-workflow-runner.ts +325 -0
- package/src/runtime/effectiveness.ts +90 -0
- package/src/runtime/errors/crew-errors.ts +162 -0
- package/src/runtime/event-stream-bridge.ts +98 -0
- package/src/runtime/foreground-control.ts +193 -0
- package/src/runtime/foreground-watchdog.ts +132 -0
- package/src/runtime/global-worker-cap.ts +96 -0
- package/src/runtime/goal-achievement.ts +148 -0
- package/src/runtime/goal-evaluator.ts +365 -0
- package/src/runtime/goal-loop-runner.ts +825 -0
- package/src/runtime/goal-state-store.ts +219 -0
- package/src/runtime/green-contract.ts +55 -0
- package/src/runtime/group-join.ts +267 -0
- package/src/runtime/handoff-manager.ts +598 -0
- package/src/runtime/heartbeat-gradient.ts +36 -0
- package/src/runtime/heartbeat-watcher.ts +239 -0
- package/src/runtime/hidden-handoff.ts +407 -0
- package/src/runtime/important-line-classifier.ts +140 -0
- package/src/runtime/intercom-bridge.ts +187 -0
- package/src/runtime/iteration-hooks.ts +305 -0
- package/src/runtime/live-agent-control.ts +136 -0
- package/src/runtime/live-agent-manager.ts +704 -0
- package/src/runtime/live-control-realtime.ts +56 -0
- package/src/runtime/live-extension-bridge.ts +148 -0
- package/src/runtime/live-irc.ts +97 -0
- package/src/runtime/live-session-health.ts +108 -0
- package/src/runtime/live-session-runtime.ts +1146 -0
- package/src/runtime/loop-gates.ts +128 -0
- package/src/runtime/manifest-cache.ts +375 -0
- package/src/runtime/mcp-proxy.ts +105 -0
- package/src/runtime/metric-parser.ts +36 -0
- package/src/runtime/model-fallback.ts +409 -0
- package/src/runtime/model-resolver.ts +126 -0
- package/src/runtime/model-scope.ts +158 -0
- package/src/runtime/orphan-worker-registry.ts +465 -0
- package/src/runtime/output-validator.ts +202 -0
- package/src/runtime/overflow-recovery.ts +206 -0
- package/src/runtime/parallel-research.ts +68 -0
- package/src/runtime/parallel-utils.ts +160 -0
- package/src/runtime/parent-guard.ts +134 -0
- package/src/runtime/path-overlap.ts +150 -0
- package/src/runtime/peer-dep.ts +292 -0
- package/src/runtime/per-write-validator.ts +181 -0
- package/src/runtime/phase-progress.ts +217 -0
- package/src/runtime/phase-tracker.ts +385 -0
- package/src/runtime/pi-args.ts +655 -0
- package/src/runtime/pi-json-output.ts +170 -0
- package/src/runtime/pi-spawn.ts +273 -0
- package/src/runtime/pipeline-runner.ts +523 -0
- package/src/runtime/plan-templates.ts +201 -0
- package/src/runtime/policy-engine.ts +115 -0
- package/src/runtime/post-checks.ts +142 -0
- package/src/runtime/post-exit-stdio-guard.ts +109 -0
- package/src/runtime/process-lifecycle.ts +491 -0
- package/src/runtime/process-status.ts +154 -0
- package/src/runtime/progress-event-coalescer.ts +44 -0
- package/src/runtime/progress-tracker.ts +124 -0
- package/src/runtime/prose-compressor.ts +162 -0
- package/src/runtime/recovery-recipes.ts +203 -0
- package/src/runtime/replace.ts +570 -0
- package/src/runtime/resilient-edit.ts +153 -0
- package/src/runtime/result-extractor.ts +217 -0
- package/src/runtime/retry-executor.ts +109 -0
- package/src/runtime/retry-runner.ts +336 -0
- package/src/runtime/role-permission.ts +59 -0
- package/src/runtime/run-coalesced-task-group.ts +308 -0
- package/src/runtime/run-drift.ts +219 -0
- package/src/runtime/run-tracker.ts +116 -0
- package/src/runtime/run-worker.ts +78 -0
- package/src/runtime/runtime-policy.ts +29 -0
- package/src/runtime/runtime-resolver.ts +172 -0
- package/src/runtime/runtime-warmup.ts +160 -0
- package/src/runtime/scheduler.ts +353 -0
- package/src/runtime/semaphore.ts +141 -0
- package/src/runtime/sensitive-paths.ts +93 -0
- package/src/runtime/session-resources.ts +25 -0
- package/src/runtime/session-snapshot.ts +59 -0
- package/src/runtime/session-usage.ts +79 -0
- package/src/runtime/settings-store.ts +155 -0
- package/src/runtime/sidechain-output.ts +35 -0
- package/src/runtime/single-agent-compose.ts +84 -0
- package/src/runtime/skill-effectiveness.ts +524 -0
- package/src/runtime/skill-instructions.ts +394 -0
- package/src/runtime/stale-reconciler.ts +680 -0
- package/src/runtime/stream-preview.ts +184 -0
- package/src/runtime/streaming-output.ts +50 -0
- package/src/runtime/subagent-manager.ts +561 -0
- package/src/runtime/subprocess-tool-registry.ts +70 -0
- package/src/runtime/supervisor-contact.ts +64 -0
- package/src/runtime/task-display.ts +52 -0
- package/src/runtime/task-graph-scheduler.ts +227 -0
- package/src/runtime/task-graph.ts +290 -0
- package/src/runtime/task-health.ts +83 -0
- package/src/runtime/task-id.ts +161 -0
- package/src/runtime/task-output-context.ts +602 -0
- package/src/runtime/task-packet.ts +268 -0
- package/src/runtime/task-quality.ts +199 -0
- package/src/runtime/task-runner/capabilities.ts +78 -0
- package/src/runtime/task-runner/child-executor.ts +790 -0
- package/src/runtime/task-runner/context-retrieval.ts +163 -0
- package/src/runtime/task-runner/live-executor.ts +227 -0
- package/src/runtime/task-runner/output-splitter.ts +152 -0
- package/src/runtime/task-runner/post-execution.ts +480 -0
- package/src/runtime/task-runner/pre-execution.ts +365 -0
- package/src/runtime/task-runner/progress.ts +157 -0
- package/src/runtime/task-runner/prompt-builder.ts +278 -0
- package/src/runtime/task-runner/prompt-pipeline.ts +88 -0
- package/src/runtime/task-runner/result-utils.ts +16 -0
- package/src/runtime/task-runner/retrieval-orchestrator.ts +310 -0
- package/src/runtime/task-runner/run-projection.ts +125 -0
- package/src/runtime/task-runner/scaffold-executor.ts +25 -0
- package/src/runtime/task-runner/state-helpers.ts +176 -0
- package/src/runtime/task-runner/tail-read.ts +34 -0
- package/src/runtime/task-runner.ts +216 -0
- package/src/runtime/team-runner-artifacts.ts +13 -0
- package/src/runtime/team-runner.ts +2396 -0
- package/src/runtime/tool-output-pruner.ts +333 -0
- package/src/runtime/tool-progress.ts +278 -0
- package/src/runtime/usage-tracker.ts +73 -0
- package/src/runtime/verification-gates.ts +579 -0
- package/src/runtime/verification-integrity.ts +108 -0
- package/src/runtime/verification-worktree.ts +186 -0
- package/src/runtime/worker-heartbeat.ts +25 -0
- package/src/runtime/worker-startup.ts +83 -0
- package/src/runtime/workflow-state.ts +195 -0
- package/src/runtime/workspace-lock.ts +443 -0
- package/src/runtime/workspace-tree.ts +312 -0
- package/src/runtime/yield-handler.ts +224 -0
- package/src/runtime/zombie-scanner.ts +298 -0
- package/src/schema/config-schema.ts +327 -0
- package/src/schema/team-tool-schema.ts +492 -0
- package/src/schema/validation-types.ts +159 -0
- package/src/skills/discover-skills.ts +203 -0
- package/src/skills/skill-templates.ts +456 -0
- package/src/skills/validate.ts +285 -0
- package/src/state/active-run-registry.ts +439 -0
- package/src/state/artifact-store.ts +176 -0
- package/src/state/atomic-write.ts +970 -0
- package/src/state/blob-store.ts +308 -0
- package/src/state/contracts.ts +160 -0
- package/src/state/crew-init.ts +269 -0
- package/src/state/decision-ledger.ts +372 -0
- package/src/state/event-log-rotation.ts +399 -0
- package/src/state/event-log.ts +1465 -0
- package/src/state/event-reconstructor.ts +229 -0
- package/src/state/gitignore-manager.ts +46 -0
- package/src/state/health-store.ts +79 -0
- package/src/state/hook-instinct-bridge.ts +94 -0
- package/src/state/hook-integrations.ts +51 -0
- package/src/state/instinct-store.ts +275 -0
- package/src/state/jsonl-writer.ts +107 -0
- package/src/state/locks.ts +562 -0
- package/src/state/mailbox.ts +916 -0
- package/src/state/observation-store.ts +176 -0
- package/src/state/run-cache.ts +193 -0
- package/src/state/run-graph.ts +183 -0
- package/src/state/run-metrics.ts +165 -0
- package/src/state/schedule.ts +166 -0
- package/src/state/session-state-map.ts +51 -0
- package/src/state/state-store.ts +973 -0
- package/src/state/task-claims.ts +61 -0
- package/src/state/tiered-eval.ts +480 -0
- package/src/state/types-eval.ts +58 -0
- package/src/state/types.ts +447 -0
- package/src/state/usage.ts +138 -0
- package/src/state/worker-atomic-writer.ts +198 -0
- package/src/subagents/async-entry.ts +1 -0
- package/src/subagents/index.ts +3 -0
- package/src/subagents/live/control.ts +1 -0
- package/src/subagents/live/manager.ts +1 -0
- package/src/subagents/live/realtime.ts +1 -0
- package/src/subagents/live/session-runtime.ts +1 -0
- package/src/subagents/manager.ts +1 -0
- package/src/subagents/spawn.ts +1 -0
- package/src/teams/discover-teams.ts +187 -0
- package/src/teams/team-config.ts +27 -0
- package/src/teams/team-serializer.ts +38 -0
- package/src/tools/safe-bash-extension.ts +54 -0
- package/src/tools/safe-bash.ts +505 -0
- package/src/types/diff.d.ts +18 -0
- package/src/types/new-api-types.ts +35 -0
- package/src/ui/agent-management-overlay.ts +160 -0
- package/src/ui/card-colors.ts +139 -0
- package/src/ui/crew-footer.ts +102 -0
- package/src/ui/crew-select-list.ts +112 -0
- package/src/ui/dashboard-panes/agents-pane.ts +164 -0
- package/src/ui/dashboard-panes/cancellation-pane.ts +66 -0
- package/src/ui/dashboard-panes/capability-pane.ts +77 -0
- package/src/ui/dashboard-panes/health-pane.ts +31 -0
- package/src/ui/dashboard-panes/mailbox-pane.ts +35 -0
- package/src/ui/dashboard-panes/metrics-pane.ts +37 -0
- package/src/ui/dashboard-panes/progress-pane.ts +38 -0
- package/src/ui/dashboard-panes/transcript-pane.ts +10 -0
- package/src/ui/deploy-bundled-themes.ts +71 -0
- package/src/ui/dwf-phase-display.ts +151 -0
- package/src/ui/dynamic-border.ts +35 -0
- package/src/ui/format-helpers.ts +31 -0
- package/src/ui/heartbeat-aggregator.ts +74 -0
- package/src/ui/key-utils.ts +42 -0
- package/src/ui/keybinding-map.ts +248 -0
- package/src/ui/layout-primitives.ts +106 -0
- package/src/ui/live-conversation-overlay.ts +183 -0
- package/src/ui/live-duration.ts +55 -0
- package/src/ui/live-run-sidebar.ts +293 -0
- package/src/ui/loaders.ts +6 -0
- package/src/ui/mascot.ts +449 -0
- package/src/ui/overlays/agent-picker-overlay.ts +66 -0
- package/src/ui/overlays/confirm-overlay.ts +60 -0
- package/src/ui/overlays/help-overlay.ts +177 -0
- package/src/ui/overlays/mailbox-compose-overlay.ts +181 -0
- package/src/ui/overlays/mailbox-compose-preview.ts +78 -0
- package/src/ui/overlays/mailbox-detail-overlay.ts +155 -0
- package/src/ui/pi-ui-compat.ts +68 -0
- package/src/ui/powerbar-publisher.ts +459 -0
- package/src/ui/render-coalescer.ts +101 -0
- package/src/ui/render-diff.ts +160 -0
- package/src/ui/render-scheduler.ts +278 -0
- package/src/ui/run-action-dispatcher.ts +182 -0
- package/src/ui/run-dashboard.ts +910 -0
- package/src/ui/run-event-bus.ts +381 -0
- package/src/ui/run-snapshot-cache.ts +1057 -0
- package/src/ui/settings-overlay.ts +1067 -0
- package/src/ui/shared-overlay-scheduler.ts +96 -0
- package/src/ui/snapshot-types.ts +90 -0
- package/src/ui/spinner.ts +17 -0
- package/src/ui/status-colors.ts +143 -0
- package/src/ui/syntax-highlight.ts +136 -0
- package/src/ui/terminal-status.ts +269 -0
- package/src/ui/theme-adapter.ts +271 -0
- package/src/ui/theme-discovery.ts +199 -0
- package/src/ui/tool-progress-formatter.ts +93 -0
- package/src/ui/tool-renderers/brief-mode.ts +225 -0
- package/src/ui/tool-renderers/index.ts +726 -0
- package/src/ui/transcript-cache.ts +128 -0
- package/src/ui/transcript-entries.ts +256 -0
- package/src/ui/transcript-viewer.ts +424 -0
- package/src/ui/widget/index.ts +418 -0
- package/src/ui/widget/widget-formatters.ts +182 -0
- package/src/ui/widget/widget-model.ts +110 -0
- package/src/ui/widget/widget-renderer.ts +182 -0
- package/src/ui/widget/widget-types.ts +36 -0
- package/src/utils/bm25-search.ts +235 -0
- package/src/utils/completion-dedupe.ts +63 -0
- package/src/utils/conflict-detect.ts +668 -0
- package/src/utils/env-allowlist.ts +30 -0
- package/src/utils/env-filter.ts +155 -0
- package/src/utils/file-coalescer.ts +98 -0
- package/src/utils/fingerprint.ts +180 -0
- package/src/utils/frontmatter.ts +70 -0
- package/src/utils/fs-watch.ts +37 -0
- package/src/utils/gh-protocol.ts +556 -0
- package/src/utils/git.ts +260 -0
- package/src/utils/guards.ts +107 -0
- package/src/utils/ids.ts +24 -0
- package/src/utils/incremental-reader.ts +220 -0
- package/src/utils/internal-error.ts +10 -0
- package/src/utils/names.ts +36 -0
- package/src/utils/ndjson.ts +115 -0
- package/src/utils/paths.ts +227 -0
- package/src/utils/project-detector.ts +160 -0
- package/src/utils/redaction.ts +370 -0
- package/src/utils/resolve-shell.ts +36 -0
- package/src/utils/run-watcher-registry.ts +152 -0
- package/src/utils/safe-paths.ts +393 -0
- package/src/utils/scan-cache.ts +144 -0
- package/src/utils/session-utils.ts +110 -0
- package/src/utils/sleep.ts +54 -0
- package/src/utils/socket-path.ts +164 -0
- package/src/utils/sse-parser.ts +131 -0
- package/src/utils/task-name-generator.ts +337 -0
- package/src/utils/timings.ts +33 -0
- package/src/utils/token-counter.ts +200 -0
- package/src/utils/visual.ts +207 -0
- package/src/workflows/cost-estimator.ts +34 -0
- package/src/workflows/discover-workflows.ts +308 -0
- package/src/workflows/intermediate-store.ts +166 -0
- package/src/workflows/preflight-validator.ts +168 -0
- package/src/workflows/topology-analyzer.ts +203 -0
- package/src/workflows/validate-workflow.ts +50 -0
- package/src/workflows/workflow-config.ts +75 -0
- package/src/workflows/workflow-serializer.ts +32 -0
- package/src/worktree/branch-freshness.ts +112 -0
- package/src/worktree/cleanup.ts +341 -0
- package/src/worktree/worktree-manager.ts +1204 -0
|
@@ -0,0 +1,393 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
|
|
4
|
+
export function isSafePathId(value: string): boolean {
|
|
5
|
+
return /^[A-Za-z0-9_-]+$/.test(value);
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function assertSafePathId(kind: string, value: string): string {
|
|
9
|
+
if (!isSafePathId(value)) throw new Error(`Invalid ${kind}: ${value}`);
|
|
10
|
+
return value;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Resolve `targetPath` against `baseDir` and verify the result stays inside it.
|
|
15
|
+
*
|
|
16
|
+
* @deprecated This returns a non-canonical `resolved` path and only validates
|
|
17
|
+
* the final resolved location, so callers can follow symlink components
|
|
18
|
+
* (TOCTOU risk). Prefer {@link resolveRealContainedPath}, which performs an
|
|
19
|
+
* O_NOFOLLOW ancestor walk plus post-realpath revalidation. The internal
|
|
20
|
+
* callers in this module (resolveRealContainedPath / resolveContainedRelativePath)
|
|
21
|
+
* still use this for the initial containment check by design.
|
|
22
|
+
*/
|
|
23
|
+
export function resolveContainedPath(baseDir: string, targetPath: string): string {
|
|
24
|
+
if (targetPath.includes("\0")) {
|
|
25
|
+
throw new Error(`Security: path contains null byte`);
|
|
26
|
+
}
|
|
27
|
+
const base = path.resolve(baseDir);
|
|
28
|
+
const resolved = path.isAbsolute(targetPath) ? path.resolve(targetPath) : path.resolve(base, targetPath);
|
|
29
|
+
// Normalize BOTH paths to canonical form on ALL platforms. This resolves
|
|
30
|
+
// symlinks so that a base and target that refer to the same physical dir
|
|
31
|
+
// via different paths (Windows 8.3 short-name; macOS /var → /private/var)
|
|
32
|
+
// compare equal. Without this, a legitimately-contained target under an
|
|
33
|
+
// OS-managed symlink is wrongly rejected as "outside" the base.
|
|
34
|
+
const baseNorm = resolveCanonicalPath(base);
|
|
35
|
+
const resolvedNorm = resolveCanonicalPath(resolved);
|
|
36
|
+
const relative =
|
|
37
|
+
process.platform === "win32"
|
|
38
|
+
? path.relative(baseNorm.toLowerCase(), resolvedNorm.toLowerCase())
|
|
39
|
+
: path.relative(baseNorm, resolvedNorm);
|
|
40
|
+
if (relative.startsWith("..") || path.isAbsolute(relative)) throw new Error(`Path is outside ${baseDir}: ${targetPath}`);
|
|
41
|
+
return resolved;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Platform-agnostic canonical path resolution. Resolves symlinks (via
|
|
46
|
+
* realpathSync) to normalize paths so the same physical location always
|
|
47
|
+
* yields the same string regardless of how it was referenced.
|
|
48
|
+
*
|
|
49
|
+
* - Windows: delegates to resolveWindowsCanonical (canonical LONG-NAME form,
|
|
50
|
+
* resolving 8.3 short-name aliases like RUNNER~1 → runneradmin).
|
|
51
|
+
* - POSIX: uses realpathSync, which resolves system symlinks such as the
|
|
52
|
+
* macOS /var → /private/var mapping.
|
|
53
|
+
*
|
|
54
|
+
* For non-existent paths (write targets), walks up to the deepest existing
|
|
55
|
+
* ancestor and joins the remaining components, so the canonical prefix is
|
|
56
|
+
* still comparable.
|
|
57
|
+
*/
|
|
58
|
+
function resolveCanonicalPath(p: string): string {
|
|
59
|
+
if (process.platform === "win32") return resolveWindowsCanonical(p);
|
|
60
|
+
try {
|
|
61
|
+
return fs.realpathSync(p);
|
|
62
|
+
} catch {
|
|
63
|
+
const parts: string[] = [];
|
|
64
|
+
let current = p;
|
|
65
|
+
while (current !== path.dirname(current)) {
|
|
66
|
+
try {
|
|
67
|
+
const real = fs.realpathSync(current);
|
|
68
|
+
let acc = real;
|
|
69
|
+
for (let i = parts.length - 1; i >= 0; i--) acc = path.join(acc, parts[i]);
|
|
70
|
+
return acc;
|
|
71
|
+
} catch {
|
|
72
|
+
/* keep walking up */
|
|
73
|
+
}
|
|
74
|
+
parts.push(path.basename(current));
|
|
75
|
+
current = path.dirname(current);
|
|
76
|
+
}
|
|
77
|
+
return p;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* On Windows, resolve a path to its canonical (long-name) form.
|
|
83
|
+
* Walks up ancestors until finding one that exists, then joins back down.
|
|
84
|
+
* This handles paths where intermediate directories don't exist yet but
|
|
85
|
+
* their ancestors do (and may use short-name aliases).
|
|
86
|
+
*
|
|
87
|
+
* Uses fs.realpathSync.native (canonical long-name form) as the primary
|
|
88
|
+
* resolver, falling back to non-native realpathSync if .native fails.
|
|
89
|
+
*/
|
|
90
|
+
function resolveWindowsCanonical(p: string): string {
|
|
91
|
+
try {
|
|
92
|
+
// Use the NATIVE realpath to resolve to canonical LONG-NAME form.
|
|
93
|
+
// On Windows, fs.realpathSync.native resolves 8.3 short-name aliases
|
|
94
|
+
// (e.g. RUNNER~1) to long-name (runneradmin). This is essential for
|
|
95
|
+
// containment checks: base (from cwd, often long-name) and target
|
|
96
|
+
// (from os.tmpdir()/mkdtempSync, often short-name) must normalize to
|
|
97
|
+
// the SAME form or a contained target is wrongly rejected as "outside".
|
|
98
|
+
const real = fs.realpathSync.native(p);
|
|
99
|
+
// Guard against NTFS internal paths (e.g. C:\$Extend\$Deleted)
|
|
100
|
+
if (real.includes("$Extend") || real.includes("$Deleted")) throw new Error("NTFS internal path");
|
|
101
|
+
return real;
|
|
102
|
+
} catch {
|
|
103
|
+
// Fallback: try realpathSync (non-native) which may succeed where .native fails
|
|
104
|
+
try {
|
|
105
|
+
const real = fs.realpathSync(p);
|
|
106
|
+
return real;
|
|
107
|
+
} catch {
|
|
108
|
+
/* proceed to ancestor walk */
|
|
109
|
+
}
|
|
110
|
+
// Walk up to find the deepest existing ancestor
|
|
111
|
+
const parts: string[] = [];
|
|
112
|
+
let current = p;
|
|
113
|
+
while (current !== path.dirname(current)) {
|
|
114
|
+
try {
|
|
115
|
+
let real: string;
|
|
116
|
+
try {
|
|
117
|
+
real = fs.realpathSync.native(current);
|
|
118
|
+
} catch {
|
|
119
|
+
real = fs.realpathSync(current);
|
|
120
|
+
}
|
|
121
|
+
// Guard against NTFS internal paths
|
|
122
|
+
if (real.includes("$Extend") || real.includes("$Deleted")) throw new Error("NTFS internal path");
|
|
123
|
+
// Found existing ancestor — join with remaining parts in reverse order
|
|
124
|
+
// (parts were pushed bottom-up, so iterate from last to first)
|
|
125
|
+
for (let i = parts.length - 1; i >= 0; i--) {
|
|
126
|
+
real = path.join(real, parts[i]);
|
|
127
|
+
}
|
|
128
|
+
return real;
|
|
129
|
+
} catch {
|
|
130
|
+
/* keep walking */
|
|
131
|
+
}
|
|
132
|
+
parts.push(path.basename(current));
|
|
133
|
+
current = path.dirname(current);
|
|
134
|
+
}
|
|
135
|
+
// Couldn't resolve any ancestor — return original
|
|
136
|
+
return p;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Resolve a target path to its real (symlink-resolved) absolute path while
|
|
142
|
+
* guaranteeing the result stays inside `baseDir`.
|
|
143
|
+
*
|
|
144
|
+
* ## Security model — asymmetric ancestor handling
|
|
145
|
+
*
|
|
146
|
+
* `baseDir` and `targetPath` are validated with different policies because
|
|
147
|
+
* they play different roles:
|
|
148
|
+
*
|
|
149
|
+
* - **baseDir** (the container): all ancestors MUST exist and MUST NOT be
|
|
150
|
+
* symlinks. We refuse to operate if any component is missing or symlinked,
|
|
151
|
+
* because a symlinked container could point the caller outside the
|
|
152
|
+
* intended trust boundary (e.g. `/var/run -> /run` resolving into an
|
|
153
|
+
* attacker-controlled directory).
|
|
154
|
+
*
|
|
155
|
+
* - **targetPath** (the contained file): the FINAL component may be
|
|
156
|
+
* non-existent (for write operations creating a new file) and EXISTING
|
|
157
|
+
* ancestors of the target may also be missing. We DO require that any
|
|
158
|
+
* ancestor that DOES exist must not be a symlink — an attacker who can
|
|
159
|
+
* create a directory in the container must not be able to redirect the
|
|
160
|
+
* file being created.
|
|
161
|
+
*
|
|
162
|
+
* This asymmetry is intentional: callers that need to create a new file
|
|
163
|
+
* pass a non-existent targetPath. Callers that operate on an existing file
|
|
164
|
+
* get full symlink protection. Callers MUST NOT pass a symlinked
|
|
165
|
+
* intermediate component; if you need to, use `resolveContainedPath`
|
|
166
|
+
* instead (which only checks the resolved path, not the chain).
|
|
167
|
+
*
|
|
168
|
+
* Throws on:
|
|
169
|
+
* - null byte in targetPath
|
|
170
|
+
* - targetPath resolves outside baseDir
|
|
171
|
+
* - any existing ancestor (base or target) is a symlink
|
|
172
|
+
* - baseDir itself does not exist
|
|
173
|
+
*
|
|
174
|
+
* Returns the resolved real path on success, or the resolved (but not
|
|
175
|
+
* realpathed) path when the target does not exist yet.
|
|
176
|
+
*
|
|
177
|
+
* NOTE: There is a race condition window between validation and use where an
|
|
178
|
+
* attacker could create a directory component after validation but before the
|
|
179
|
+
* file is created. Callers MUST create parent directories atomically
|
|
180
|
+
* (e.g., mkdirSync with { recursive: true }) and use O_CREAT | O_NOFOLLOW | O_EXCL
|
|
181
|
+
* for atomic file creation, as atomicWriteFile does. This ensures the entire
|
|
182
|
+
* operation is atomic and prevents TOCTOU attacks.
|
|
183
|
+
*/
|
|
184
|
+
export function resolveRealContainedPath(baseDir: string, targetPath: string): string {
|
|
185
|
+
if (targetPath.includes("\0")) {
|
|
186
|
+
throw new Error(`Security: path contains null byte`);
|
|
187
|
+
}
|
|
188
|
+
const resolved = resolveContainedPath(baseDir, targetPath);
|
|
189
|
+
|
|
190
|
+
// Open baseDir with O_NOFOLLOW to atomically validate no symlinks in the path.
|
|
191
|
+
// O_NOFOLLOW makes the open fail with ELOOP if any path component is a symlink.
|
|
192
|
+
let baseFd: number | undefined;
|
|
193
|
+
try {
|
|
194
|
+
baseFd = fs.openSync(baseDir, fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW);
|
|
195
|
+
} catch (error) {
|
|
196
|
+
const errCode = (error as NodeJS.ErrnoException).code;
|
|
197
|
+
if (errCode === "ENOENT") {
|
|
198
|
+
// baseDir doesn't exist yet — create it and retry
|
|
199
|
+
try {
|
|
200
|
+
fs.mkdirSync(baseDir, { recursive: true });
|
|
201
|
+
baseFd = fs.openSync(baseDir, fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW);
|
|
202
|
+
} catch (retryError) {
|
|
203
|
+
throw new Error(
|
|
204
|
+
`Cannot open base directory ${baseDir}: ${retryError instanceof Error ? retryError.message : String(retryError)}`,
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
} else if (errCode === "ELOOP") {
|
|
208
|
+
// On macOS, system directories like /var → /private/var contain symlinks.
|
|
209
|
+
// If baseDir is under such a path, resolve through realpath and retry.
|
|
210
|
+
if (process.platform === "darwin") {
|
|
211
|
+
try {
|
|
212
|
+
const realBaseDir = fs.realpathSync(baseDir);
|
|
213
|
+
if (realBaseDir !== baseDir) {
|
|
214
|
+
baseFd = fs.openSync(realBaseDir, fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW);
|
|
215
|
+
baseDir = realBaseDir; // update for later use
|
|
216
|
+
// Fall through to fstatSync below
|
|
217
|
+
}
|
|
218
|
+
} catch {
|
|
219
|
+
/* throw original */
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
if (baseFd === undefined) throw new Error("Refusing to resolve: baseDir path contains a symlink: " + baseDir);
|
|
223
|
+
} else {
|
|
224
|
+
throw new Error(`Cannot open base directory ${baseDir}: ${error instanceof Error ? error.message : String(error)}`);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
let realBase: string;
|
|
228
|
+
try {
|
|
229
|
+
const stat = fs.fstatSync(baseFd);
|
|
230
|
+
if (!stat.isDirectory()) throw new Error(`baseDir ${baseDir} is not a directory`);
|
|
231
|
+
// Use regular realpathSync (not .native) to preserve input path form.
|
|
232
|
+
// On Windows CI, .native always returns long-name form (runneradmin)
|
|
233
|
+
// while non-native preserves short-name (RUNNER~1). Using non-native
|
|
234
|
+
// ensures the result matches what the caller passed in.
|
|
235
|
+
realBase = fs.realpathSync(baseDir);
|
|
236
|
+
} catch (error) {
|
|
237
|
+
// baseDir MUST exist and be resolvable for the containment guarantee to hold.
|
|
238
|
+
// Callers creating new directories must create baseDir atomically (e.g.,
|
|
239
|
+
// mkdirSync with { recursive: true }) BEFORE calling this function, and use
|
|
240
|
+
// O_NOFOLLOW|O_CREAT|O_EXCL for the actual file creation to ensure atomicity.
|
|
241
|
+
// The safe-paths validation and the file creation are two separate operations
|
|
242
|
+
// with a gap between them — callers must close this gap with atomic primitives.
|
|
243
|
+
throw new Error(`Cannot resolve real path of base directory ${baseDir}: ${error instanceof Error ? error.message : String(error)}`);
|
|
244
|
+
} finally {
|
|
245
|
+
fs.closeSync(baseFd);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Walk the ancestor chain of the resolved target path, using O_NOFOLLOW
|
|
249
|
+
// on each ancestor to atomically validate none are symlinks.
|
|
250
|
+
const O_NOFOLLOW = fs.constants.O_NOFOLLOW;
|
|
251
|
+
const O_RDONLY = fs.constants.O_RDONLY;
|
|
252
|
+
const resolvedParts = resolved.split(path.sep);
|
|
253
|
+
let resolvedAccumulated = "";
|
|
254
|
+
if (resolvedParts[0] === "") resolvedAccumulated = "/"; // Unix root
|
|
255
|
+
for (let i = 1; i < resolvedParts.length; i++) {
|
|
256
|
+
if (resolvedParts[i] === "") continue;
|
|
257
|
+
resolvedAccumulated = path.join(resolvedAccumulated, resolvedParts[i]);
|
|
258
|
+
try {
|
|
259
|
+
const fd = fs.openSync(resolvedAccumulated, O_RDONLY | O_NOFOLLOW);
|
|
260
|
+
fs.closeSync(fd);
|
|
261
|
+
} catch (error) {
|
|
262
|
+
if ((error as NodeJS.ErrnoException).code === "ELOOP") {
|
|
263
|
+
// On macOS, /var → /private/var, /tmp → /private/tmp, /etc → /private/etc
|
|
264
|
+
// are system symlinks managed by the OS. Allow them.
|
|
265
|
+
if (process.platform === "darwin") {
|
|
266
|
+
const resolvedSymlink = resolvedAccumulated;
|
|
267
|
+
const knownDarwinSymlinks = ["/var", "/tmp", "/etc", "/private/var", "/private/tmp", "/private/etc"];
|
|
268
|
+
// Use prefix match to handle subdirectories like /var/folders/...
|
|
269
|
+
if (knownDarwinSymlinks.some((s) => resolvedSymlink === s || resolvedSymlink.startsWith(s + "/"))) continue;
|
|
270
|
+
}
|
|
271
|
+
throw new Error("Refusing to resolve: target path ancestor is a symlink: " + resolvedAccumulated);
|
|
272
|
+
}
|
|
273
|
+
// EPERM on Windows when opening a directory — skip validation
|
|
274
|
+
if ((error as NodeJS.ErrnoException).code === "EPERM" && process.platform === "win32") continue;
|
|
275
|
+
// ENOENT means component doesn't exist — that's OK. Only existing symlinks
|
|
276
|
+
// are a security risk (symlinks to attacker-controlled targets). Non-existent
|
|
277
|
+
// paths can be created by the caller and don't pose a symlink risk.
|
|
278
|
+
if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
|
|
279
|
+
// For the final component (target itself), ENOENT is expected for non-existent targets.
|
|
280
|
+
if (i === resolvedParts.length - 1) continue;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// Open the target with O_NOFOLLOW to catch any symlinks.
|
|
285
|
+
// ENOENT is acceptable for write operations — the file may not exist yet.
|
|
286
|
+
let targetFd: number;
|
|
287
|
+
try {
|
|
288
|
+
targetFd = fs.openSync(resolved, O_RDONLY | O_NOFOLLOW);
|
|
289
|
+
} catch (error) {
|
|
290
|
+
if ((error as NodeJS.ErrnoException).code === "ELOOP")
|
|
291
|
+
throw new Error("Refusing to resolve: target path is a symlink: " + resolved);
|
|
292
|
+
// EPERM on Windows when opening a directory — treat as non-existent
|
|
293
|
+
if ((error as NodeJS.ErrnoException).code === "EPERM" && process.platform === "win32") return resolved;
|
|
294
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") {
|
|
295
|
+
// Target doesn't exist yet — that's OK for write operations.
|
|
296
|
+
// All ancestors have been validated above (no symlinks).
|
|
297
|
+
// The caller will create the file with atomic primitives.
|
|
298
|
+
return resolved;
|
|
299
|
+
}
|
|
300
|
+
throw new Error(`Cannot open ${resolved}: ${error instanceof Error ? error.message : String(error)}`);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
let realTarget: string;
|
|
304
|
+
try {
|
|
305
|
+
// Use regular realpathSync (not .native) to preserve input path form.
|
|
306
|
+
realTarget = fs.realpathSync(resolved);
|
|
307
|
+
} catch (targetError) {
|
|
308
|
+
if ((targetError as NodeJS.ErrnoException).code === "ENOENT") {
|
|
309
|
+
// Target doesn't exist yet — this is OK for write operations.
|
|
310
|
+
// Return the resolved path so the caller can create the file.
|
|
311
|
+
// We already validated all ancestors are not symlinks above.
|
|
312
|
+
return resolved;
|
|
313
|
+
}
|
|
314
|
+
throw new Error(
|
|
315
|
+
`Cannot resolve real path of ${resolved}: ${targetError instanceof Error ? targetError.message : String(targetError)}`,
|
|
316
|
+
);
|
|
317
|
+
} finally {
|
|
318
|
+
fs.closeSync(targetFd);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// Re-validate the ancestor chain of the resolved path to catch any TOCTOU
|
|
322
|
+
// races that occurred between the initial O_NOFOLLOW validation and the
|
|
323
|
+
// realpathSync call. An attacker could have replaced a validated ancestor
|
|
324
|
+
// with a symlink during this window.
|
|
325
|
+
//
|
|
326
|
+
// Skip the final path component (realTarget itself) — we just successfully
|
|
327
|
+
// realpathSync'd it, so it exists. Re-validating it can spuriously fail on
|
|
328
|
+
// Windows where the resolved path uses short-name (8.3) form that
|
|
329
|
+
// openSync cannot reopen, or where the realpathSync result differs in
|
|
330
|
+
// case/separator form from the original.
|
|
331
|
+
//
|
|
332
|
+
// Walk via path.dirname which is portable across all platforms and
|
|
333
|
+
// correctly handles extended-length (\\?\), UNC (\\server\share), and
|
|
334
|
+
// short-name paths on Windows without manual parsing.
|
|
335
|
+
let ancestor = path.dirname(realTarget);
|
|
336
|
+
while (ancestor && ancestor !== path.dirname(ancestor)) {
|
|
337
|
+
try {
|
|
338
|
+
const fd = fs.openSync(ancestor, O_RDONLY | O_NOFOLLOW);
|
|
339
|
+
fs.closeSync(fd);
|
|
340
|
+
} catch (error) {
|
|
341
|
+
const errCode = (error as NodeJS.ErrnoException).code;
|
|
342
|
+
if (errCode === "ELOOP") throw new Error("Refusing to resolve: TOCTOU race detected, path became a symlink: " + ancestor);
|
|
343
|
+
// Windows: EPERM can occur when opening system directories (e.g. C:\)
|
|
344
|
+
// or NTFS internal paths ($Extend/$Deleted). Skip and continue walking.
|
|
345
|
+
if (process.platform === "win32" && errCode === "EPERM") {
|
|
346
|
+
if (ancestor.includes("$Extend") || ancestor.includes("$Deleted")) {
|
|
347
|
+
// NTFS internal path — stop walking, we've reached the filesystem root
|
|
348
|
+
break;
|
|
349
|
+
}
|
|
350
|
+
// System directory — continue walking up
|
|
351
|
+
ancestor = path.dirname(ancestor);
|
|
352
|
+
continue;
|
|
353
|
+
}
|
|
354
|
+
if (errCode !== "ENOENT") throw error;
|
|
355
|
+
// ENOENT on an ancestor of realTarget after realpathSync is concerning
|
|
356
|
+
// — the path existed when we validated it but now doesn't. This could
|
|
357
|
+
// indicate a race or attack. For safety, treat this as an error.
|
|
358
|
+
throw new Error(
|
|
359
|
+
`Cannot validate resolved path: ${ancestor} disappeared after realpathSync: ${error instanceof Error ? error.message : String(error)}`,
|
|
360
|
+
);
|
|
361
|
+
}
|
|
362
|
+
ancestor = path.dirname(ancestor);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// Verify the resolved real path is still within baseDir.
|
|
366
|
+
// On Windows, realpathSync.native may return different short/long-name forms
|
|
367
|
+
// for the same physical directory depending on how the path was opened.
|
|
368
|
+
// Use resolveWindowsCanonical (same as resolveContainedPath) to normalize
|
|
369
|
+
// both paths consistently before comparison.
|
|
370
|
+
if (process.platform === "win32") {
|
|
371
|
+
const normBase = resolveWindowsCanonical(realBase).replace(/\\/g, "/").toLowerCase();
|
|
372
|
+
const normTarget = resolveWindowsCanonical(realTarget).replace(/\\/g, "/").toLowerCase();
|
|
373
|
+
if (!normTarget.startsWith(normBase + "/") && normBase !== normTarget) {
|
|
374
|
+
throw new Error(`Path is outside ${baseDir}: ${targetPath}`);
|
|
375
|
+
}
|
|
376
|
+
} else {
|
|
377
|
+
const relative = path.relative(realBase, realTarget);
|
|
378
|
+
if (relative.startsWith("..") || path.isAbsolute(relative)) throw new Error(`Path is outside ${baseDir}: ${targetPath}`);
|
|
379
|
+
}
|
|
380
|
+
return realTarget;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
export function resolveContainedRelativePath(baseDir: string, relativePath: string, kind = "path"): string {
|
|
384
|
+
if (relativePath.includes("\0")) {
|
|
385
|
+
throw new Error(`Security: path contains null byte: ${kind}`);
|
|
386
|
+
}
|
|
387
|
+
const normalized = relativePath.replace(/\\/g, "/").replace(/^\.\/+/, "");
|
|
388
|
+
// Detect Windows absolute paths (C:\, \\server\share) that path.isAbsolute may miss after normalization
|
|
389
|
+
if (/^[A-Za-z]:/.test(normalized)) throw new Error(`Invalid ${kind}: ${relativePath}`);
|
|
390
|
+
if (!normalized || normalized.split("/").some((segment) => segment === "..") || path.isAbsolute(normalized))
|
|
391
|
+
throw new Error(`Invalid ${kind}: ${relativePath}`);
|
|
392
|
+
return resolveContainedPath(baseDir, path.resolve(baseDir, normalized));
|
|
393
|
+
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
|
|
4
|
+
export interface ScanEntry {
|
|
5
|
+
/** Unique key for this entry (e.g., runId, artifactPath) */
|
|
6
|
+
key: string;
|
|
7
|
+
/** Filesystem path */
|
|
8
|
+
path: string;
|
|
9
|
+
/** Raw content (parsed JSON or text) */
|
|
10
|
+
raw: unknown;
|
|
11
|
+
/** File modification time */
|
|
12
|
+
mtimeMs: number;
|
|
13
|
+
/** File size in bytes */
|
|
14
|
+
sizeBytes: number;
|
|
15
|
+
/** When this entry was loaded */
|
|
16
|
+
loadedAtMs: number;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface ScanCacheOptions {
|
|
20
|
+
/** TTL in ms for cached entries. Default 1000. */
|
|
21
|
+
ttlMs?: number;
|
|
22
|
+
/** Maximum number of entries. Default 100. */
|
|
23
|
+
maxEntries?: number;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface CacheBucket {
|
|
27
|
+
entries: Map<string, ScanEntry>;
|
|
28
|
+
expireAtMs: number;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Shared raw scan-entry cache for runs, artifacts, mailbox, transcripts.
|
|
33
|
+
* Provides deterministic sort order and invalidation on mutation.
|
|
34
|
+
*/
|
|
35
|
+
export class SharedScanCache {
|
|
36
|
+
#buckets = new Map<string, CacheBucket>();
|
|
37
|
+
#ttlMs: number;
|
|
38
|
+
#maxEntries: number;
|
|
39
|
+
#now: () => number;
|
|
40
|
+
|
|
41
|
+
constructor(options: ScanCacheOptions = {}) {
|
|
42
|
+
this.#ttlMs = options.ttlMs ?? 1000;
|
|
43
|
+
this.#maxEntries = options.maxEntries ?? 100;
|
|
44
|
+
this.#now = () => Date.now();
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Get a cached entry by bucket and key. Returns undefined if not cached or expired. */
|
|
48
|
+
get(bucket: string, key: string): ScanEntry | undefined {
|
|
49
|
+
const b = this.#buckets.get(bucket);
|
|
50
|
+
if (!b) return undefined;
|
|
51
|
+
if (this.#now() > b.expireAtMs) {
|
|
52
|
+
this.#buckets.delete(bucket);
|
|
53
|
+
return undefined;
|
|
54
|
+
}
|
|
55
|
+
return b.entries.get(key);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Get all entries in a bucket. Returns empty array if expired or missing. */
|
|
59
|
+
list(bucket: string): ScanEntry[] {
|
|
60
|
+
const b = this.#buckets.get(bucket);
|
|
61
|
+
if (!b) return [];
|
|
62
|
+
if (this.#now() > b.expireAtMs) {
|
|
63
|
+
this.#buckets.delete(bucket);
|
|
64
|
+
return [];
|
|
65
|
+
}
|
|
66
|
+
return [...b.entries.values()].sort((a, b) => a.key.localeCompare(b.key));
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Set an entry in a bucket. */
|
|
70
|
+
set(bucket: string, entry: ScanEntry): void {
|
|
71
|
+
let b = this.#buckets.get(bucket);
|
|
72
|
+
if (!b || this.#now() > b.expireAtMs) {
|
|
73
|
+
b = { entries: new Map(), expireAtMs: this.#now() + this.#ttlMs };
|
|
74
|
+
this.#buckets.set(bucket, b);
|
|
75
|
+
}
|
|
76
|
+
if (b.entries.size >= this.#maxEntries) {
|
|
77
|
+
// Evict oldest entry
|
|
78
|
+
const firstKey = b.entries.keys().next().value;
|
|
79
|
+
if (firstKey !== undefined) b.entries.delete(firstKey);
|
|
80
|
+
}
|
|
81
|
+
b.entries.set(entry.key, entry);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Invalidate a specific key in a bucket. */
|
|
85
|
+
invalidate(bucket: string, key: string): void {
|
|
86
|
+
const b = this.#buckets.get(bucket);
|
|
87
|
+
if (b) b.entries.delete(key);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Invalidate an entire bucket. */
|
|
91
|
+
invalidateBucket(bucket: string): void {
|
|
92
|
+
this.#buckets.delete(bucket);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Invalidate all buckets. */
|
|
96
|
+
clear(): void {
|
|
97
|
+
this.#buckets.clear();
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** Read a file, parse if JSON, and cache the result. */
|
|
101
|
+
readAndCache(bucket: string, key: string, filePath: string, parseJson = true): ScanEntry | undefined {
|
|
102
|
+
try {
|
|
103
|
+
if (!fs.existsSync(filePath)) return undefined;
|
|
104
|
+
const stat = fs.statSync(filePath);
|
|
105
|
+
const cached = this.get(bucket, key);
|
|
106
|
+
if (cached && cached.mtimeMs >= stat.mtimeMs && cached.sizeBytes === stat.size) return cached;
|
|
107
|
+
const content = fs.readFileSync(filePath, "utf-8");
|
|
108
|
+
const raw = parseJson ? JSON.parse(content) : content;
|
|
109
|
+
const entry: ScanEntry = {
|
|
110
|
+
key,
|
|
111
|
+
path: filePath,
|
|
112
|
+
raw,
|
|
113
|
+
mtimeMs: stat.mtimeMs,
|
|
114
|
+
sizeBytes: stat.size,
|
|
115
|
+
loadedAtMs: this.#now(),
|
|
116
|
+
};
|
|
117
|
+
this.set(bucket, entry);
|
|
118
|
+
return entry;
|
|
119
|
+
} catch {
|
|
120
|
+
return undefined;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** Read a directory and cache entries for each file. */
|
|
125
|
+
scanAndCache(bucket: string, dirPath: string, parseJson = true): ScanEntry[] {
|
|
126
|
+
try {
|
|
127
|
+
if (!fs.existsSync(dirPath)) return [];
|
|
128
|
+
const entries = fs.readdirSync(dirPath, { withFileTypes: true });
|
|
129
|
+
const results: ScanEntry[] = [];
|
|
130
|
+
for (const entry of entries) {
|
|
131
|
+
if (!entry.isFile()) continue;
|
|
132
|
+
const filePath = path.join(dirPath, entry.name);
|
|
133
|
+
const cached = this.readAndCache(bucket, entry.name, filePath, parseJson);
|
|
134
|
+
if (cached) results.push(cached);
|
|
135
|
+
}
|
|
136
|
+
return results.sort((a, b) => a.key.localeCompare(b.key));
|
|
137
|
+
} catch {
|
|
138
|
+
return [];
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** Global shared scan cache instance. */
|
|
144
|
+
export const sharedScanCache = new SharedScanCache();
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session ID utilities for pi-crew / pi session alignment.
|
|
3
|
+
*
|
|
4
|
+
* pi's session IDs use the format:
|
|
5
|
+
* ^[A-Za-z0-9](?:[A-Za-z0-9._-]*[A-Za-z0-9])?$
|
|
6
|
+
*
|
|
7
|
+
* This module provides utilities to generate valid pi session IDs
|
|
8
|
+
* that align with pi-crew run IDs for easy cross-referencing.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Validate session ID format per pi's requirements.
|
|
13
|
+
* Format: ^[A-Za-z0-9](?:[A-Za-z0-9._-]*[A-Za-z0-9])?$
|
|
14
|
+
*/
|
|
15
|
+
export function assertValidSessionId(id: string): void {
|
|
16
|
+
if (!id || !/^[A-Za-z0-9](?:[A-Za-z0-9._-]*[A-Za-z0-9])?$/.test(id)) {
|
|
17
|
+
throw new Error(`Invalid session id: must be non-empty, alphanumeric with '-', '_', '.' and start/end with alphanumeric`);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Convert a pi-crew run ID to a valid pi session ID.
|
|
23
|
+
*
|
|
24
|
+
* - Strips non-alphanumeric characters
|
|
25
|
+
* - Lowercases
|
|
26
|
+
* - Prefixes with "crew-"
|
|
27
|
+
* - Truncates to 16 chars for safety
|
|
28
|
+
*
|
|
29
|
+
* @param runId - The pi-crew run ID (e.g., "team_20260528133725_02e05cc5480d0175")
|
|
30
|
+
* @returns Valid pi session ID (e.g., "crew-team20260528133")
|
|
31
|
+
*/
|
|
32
|
+
export function toPiSessionId(runId: string): string {
|
|
33
|
+
// Strip non-alphanumeric, lowercase, prefix with "crew-"
|
|
34
|
+
const sanitized = runId.replace(/[^A-Za-z0-9]/g, "").toLowerCase();
|
|
35
|
+
return `crew-${sanitized.slice(0, 16)}`;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Validate and convert a run ID to a pi session ID.
|
|
40
|
+
* Returns the session ID if valid, or undefined if conversion would produce invalid ID.
|
|
41
|
+
*/
|
|
42
|
+
export function safeToPiSessionId(runId: string): string | undefined {
|
|
43
|
+
try {
|
|
44
|
+
const sessionId = toPiSessionId(runId);
|
|
45
|
+
assertValidSessionId(sessionId);
|
|
46
|
+
return sessionId;
|
|
47
|
+
} catch {
|
|
48
|
+
return undefined;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Extract the current Pi session id from an ExtensionContext.
|
|
54
|
+
*
|
|
55
|
+
* `ExtensionContext` does not declare `sessionId` in its type, but the runtime
|
|
56
|
+
* attaches it as an own property. We read it via `getOwnPropertyDescriptor`
|
|
57
|
+
* to safely bypass any Proxy traps, then validate it as a non-empty string.
|
|
58
|
+
*
|
|
59
|
+
* This is the canonical accessor — every site that filters the SHARED
|
|
60
|
+
* per-project `.crew/state/` tree down to the current session MUST use this,
|
|
61
|
+
* otherwise cross-session state leaks (e.g. compaction-guard resuming another
|
|
62
|
+
* session's runs, ambient-status injecting another session's runs).
|
|
63
|
+
*
|
|
64
|
+
* Returns undefined when the session id is absent or unparseable — callers
|
|
65
|
+
* must decide whether to treat that as "no filter" (back-compat) or "no runs".
|
|
66
|
+
*/
|
|
67
|
+
export function extractSessionId(ctx: unknown): string | undefined {
|
|
68
|
+
if (typeof ctx !== "object" || ctx === null) return undefined;
|
|
69
|
+
let raw: unknown;
|
|
70
|
+
try {
|
|
71
|
+
raw = Object.getOwnPropertyDescriptor(ctx, "sessionId")?.value;
|
|
72
|
+
} catch {
|
|
73
|
+
// Defensive: a hostile Proxy or exotic object may trap descriptor
|
|
74
|
+
// access. Real Pi ExtensionContext objects are plain, so this is
|
|
75
|
+
// only hit by adversarial/degenerate inputs — treat as no session id.
|
|
76
|
+
return undefined;
|
|
77
|
+
}
|
|
78
|
+
if (typeof raw !== "string" || raw.length === 0) return undefined;
|
|
79
|
+
return raw;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Broker-only session id extractor.
|
|
84
|
+
*
|
|
85
|
+
* Pi's `ExtensionContext` does NOT expose a top-level `sessionId` property on
|
|
86
|
+
* its public surface — the id is reachable via `ctx.sessionManager.getSessionId()`.
|
|
87
|
+
* This helper is only called from `installCrewBrokerLifecycleController.setSessionId`
|
|
88
|
+
* (once per session_start), so the extra method invocation is safe here. It is
|
|
89
|
+
* INTENTIONALLY a separate function from `extractSessionId`, which is called on
|
|
90
|
+
* every `context` event (before every LLM call) from `context-status-injection.ts`
|
|
91
|
+
* — extending that hot path with method calls was observed to freeze the TUI
|
|
92
|
+
* (dashboard opens but is unresponsive, footer does not render) during smoke
|
|
93
|
+
* testing, so it stays on the trivial property lookup.
|
|
94
|
+
*
|
|
95
|
+
* Tries the sessionManager path first, then falls back to a direct
|
|
96
|
+
* `ctx.sessionId` for test mock compatibility.
|
|
97
|
+
*/
|
|
98
|
+
export function extractBrokerSessionId(ctx: unknown): string | undefined {
|
|
99
|
+
if (typeof ctx !== "object" || ctx === null) return undefined;
|
|
100
|
+
try {
|
|
101
|
+
const sm = (ctx as { sessionManager?: { getSessionId?: () => unknown } }).sessionManager;
|
|
102
|
+
const viaManager = sm?.getSessionId?.();
|
|
103
|
+
if (typeof viaManager === "string" && viaManager.length > 0) return viaManager;
|
|
104
|
+
const direct = Object.getOwnPropertyDescriptor(ctx, "sessionId")?.value;
|
|
105
|
+
if (typeof direct === "string" && direct.length > 0) return direct;
|
|
106
|
+
return undefined;
|
|
107
|
+
} catch {
|
|
108
|
+
return undefined;
|
|
109
|
+
}
|
|
110
|
+
}
|