pi-crew 0.9.16 → 0.9.18
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 +130 -0
- package/README.md +19 -0
- package/dist/build-meta.json +21784 -0
- package/dist/index.mjs +83169 -0
- package/dist/index.mjs.map +7 -0
- package/docs/A +358 -0
- package/docs/M-A +357 -0
- package/docs/REVIEW-FINDINGS-2026-06 +357 -0
- package/docs/Y +357 -0
- package/docs/a +357 -0
- package/docs/aA +358 -0
- package/docs/archive/README.md +91 -0
- package/docs/migration/atomic-write-v2-migration.md +297 -0
- package/docs/patterns/command-agent-skill.md +1 -1
- package/docs/perf/performance-review-2026-07.md +287 -0
- package/docs/skills/REFERENCE.md +0 -17
- package/docs//303/242mmaAAA/303/242 +357 -0
- package/index.bundle.ts +25 -0
- package/index.ts +95 -4
- package/package.json +15 -4
- package/skills/iterative-audit/SKILL.md +0 -1
- package/skills/widget-rendering/SKILL.md +17 -0
- package/src/adapters/claude-adapter.ts +1 -3
- package/src/adapters/export-util.ts +16 -16
- package/src/adapters/index.ts +5 -5
- package/src/agents/agent-config.ts +20 -14
- package/src/agents/agent-serializer.ts +1 -1
- package/src/agents/discover-agents.ts +83 -39
- package/src/benchmark/benchmark-runner.ts +51 -51
- package/src/benchmark/feedback-loop.ts +1 -1
- package/src/config/config.ts +303 -591
- package/src/config/defaults.ts +28 -2
- package/src/config/drift-detector.ts +11 -14
- package/src/config/markers.ts +201 -203
- package/src/config/resilient-parser.ts +14 -6
- package/src/config/role-tools.ts +3 -3
- package/src/config/suggestions.ts +5 -12
- package/src/config/types.ts +9 -25
- package/src/errors.ts +151 -157
- package/src/extension/action-suggestions.ts +54 -9
- package/src/extension/async-notifier.ts +43 -13
- package/src/extension/autonomous-policy.ts +77 -37
- package/src/extension/command-completions.ts +9 -8
- package/src/extension/context-status-injection.ts +14 -12
- package/src/extension/crew-autocomplete.ts +8 -16
- package/src/extension/crew-cleanup.ts +2 -1
- package/src/extension/crew-shortcuts.ts +9 -6
- package/src/extension/cross-extension-rpc.ts +136 -42
- package/src/extension/help.ts +1 -1
- package/src/extension/import-index.ts +10 -5
- package/src/extension/knowledge-injection.ts +88 -15
- package/src/extension/management.ts +89 -349
- package/src/extension/message-renderers.ts +3 -4
- package/src/extension/notification-router.ts +22 -5
- package/src/extension/notification-sink.ts +6 -3
- package/src/extension/pi-api.ts +17 -8
- package/src/extension/plan-orchestrate.ts +8 -27
- package/src/extension/project-init.ts +21 -6
- package/src/extension/register.ts +329 -975
- package/src/extension/registration/artifact-cleanup.ts +9 -4
- package/src/extension/registration/command-utils.ts +5 -1
- package/src/extension/registration/commands.ts +806 -314
- package/src/extension/registration/compaction-guard.ts +15 -15
- package/src/extension/registration/lifecycle.ts +259 -0
- package/src/extension/registration/observability.ts +293 -0
- package/src/extension/registration/subagent-helpers.ts +29 -7
- package/src/extension/registration/subagent-tools.ts +253 -44
- package/src/extension/registration/team-tool.ts +19 -84
- package/src/extension/registration/ui.ts +168 -0
- package/src/extension/registration/viewers.ts +64 -24
- package/src/extension/result-watcher.ts +18 -7
- package/src/extension/run-bundle-schema.ts +21 -6
- package/src/extension/run-export.ts +15 -6
- package/src/extension/run-import.ts +64 -29
- package/src/extension/run-index.ts +28 -9
- package/src/extension/run-maintenance.ts +32 -12
- package/src/extension/session-summary.ts +1 -1
- package/src/extension/team-manager-command.ts +64 -7
- package/src/extension/team-onboard.ts +149 -150
- package/src/extension/team-recommendation.ts +121 -21
- package/src/extension/team-tool/anchor.ts +31 -64
- package/src/extension/team-tool/api.ts +929 -141
- package/src/extension/team-tool/auto-summarize.ts +14 -26
- package/src/extension/team-tool/cache-control.ts +1 -5
- package/src/extension/team-tool/cancel.ts +138 -38
- package/src/extension/team-tool/chain-dispatch.ts +5 -13
- package/src/extension/team-tool/chain-executor.ts +29 -51
- package/src/extension/team-tool/config-patch.ts +1 -1
- package/src/extension/team-tool/context.ts +40 -20
- package/src/extension/team-tool/destructive-gate.ts +10 -5
- package/src/extension/team-tool/doctor.ts +170 -51
- package/src/extension/team-tool/explain.ts +228 -214
- package/src/extension/team-tool/failure-patterns.ts +3 -9
- package/src/extension/team-tool/goal-wrap.ts +71 -29
- package/src/extension/team-tool/goal.ts +185 -35
- package/src/extension/team-tool/handle-schedule.ts +34 -15
- package/src/extension/team-tool/handle-settings.ts +94 -56
- package/src/extension/team-tool/health-monitor.ts +27 -108
- package/src/extension/team-tool/inspect.ts +42 -9
- package/src/extension/team-tool/intent-policy.ts +4 -1
- package/src/extension/team-tool/lifecycle-actions.ts +244 -48
- package/src/extension/team-tool/orchestrate.ts +7 -18
- package/src/extension/team-tool/parallel-dispatch.ts +51 -29
- package/src/extension/team-tool/plan.ts +29 -5
- package/src/extension/team-tool/respond.ts +49 -16
- package/src/extension/team-tool/run-not-found.ts +3 -8
- package/src/extension/team-tool/run.ts +80 -317
- package/src/extension/team-tool/status.ts +135 -43
- package/src/extension/team-tool/workflow-manage.ts +92 -25
- package/src/extension/team-tool-types.ts +8 -1
- package/src/extension/team-tool.ts +142 -551
- package/src/extension/validate-resources.ts +39 -8
- package/src/hooks/registry.ts +66 -17
- package/src/hooks/types.ts +1 -1
- package/src/i18n.ts +25 -11
- package/src/observability/correlation.ts +17 -3
- package/src/observability/event-bus.ts +51 -56
- package/src/observability/event-to-metric.ts +117 -15
- package/src/observability/exporters/otlp-exporter.ts +56 -17
- package/src/observability/exporters/prometheus-exporter.ts +1 -1
- package/src/observability/metric-registry.ts +16 -4
- package/src/observability/metric-retention.ts +8 -2
- package/src/observability/metric-sink.ts +11 -3
- package/src/observability/metrics-primitives.ts +42 -9
- package/src/plugins/plugin-define.ts +2 -2
- package/src/plugins/plugin-registry.ts +25 -25
- package/src/plugins/plugins/index.ts +1 -1
- package/src/plugins/plugins/nextjs.ts +16 -16
- package/src/plugins/plugins/vite.ts +7 -11
- package/src/plugins/plugins/vitest.ts +6 -11
- package/src/runtime/adaptive-plan.ts +225 -40
- package/src/runtime/agent-control.ts +58 -15
- package/src/runtime/agent-memory.ts +15 -9
- package/src/runtime/agent-observability.ts +10 -2
- package/src/runtime/anchor-manager.ts +27 -25
- package/src/runtime/async-marker.ts +7 -1
- package/src/runtime/async-runner.ts +39 -24
- package/src/runtime/auto-summarize.ts +7 -13
- package/src/runtime/background-runner.ts +189 -251
- package/src/runtime/batch-barrier.ts +18 -16
- package/src/runtime/cancellation-token.ts +16 -6
- package/src/runtime/cancellation.ts +46 -8
- package/src/runtime/capability-inventory.ts +6 -4
- package/src/runtime/chain-parser.ts +44 -11
- package/src/runtime/chain-runner.ts +63 -69
- package/src/runtime/checkpoint.ts +12 -56
- package/src/runtime/child-pi.ts +344 -79
- package/src/runtime/coalesce-tasks.ts +268 -0
- package/src/runtime/code-summary.ts +71 -26
- package/src/runtime/compact-stages/index.ts +15 -5
- package/src/runtime/compact-stages/tail-capture-stage.ts +7 -2
- package/src/runtime/compact-stages/truncation-stage.ts +4 -1
- package/src/runtime/compaction-summary.ts +17 -10
- package/src/runtime/completion-guard.ts +33 -16
- package/src/runtime/crash-classification.ts +28 -7
- package/src/runtime/crash-recovery.ts +177 -37
- package/src/runtime/crew-agent-records.ts +145 -47
- package/src/runtime/crew-hooks.ts +9 -24
- package/src/runtime/cross-extension-rpc.ts +25 -23
- package/src/runtime/custom-tools/irc-tool.ts +43 -15
- package/src/runtime/custom-tools/submit-result-tool.ts +16 -5
- package/src/runtime/delivery-coordinator.ts +34 -7
- package/src/runtime/delta-conflict.ts +9 -21
- package/src/runtime/deterministic-ast.ts +1 -1
- package/src/runtime/diagnostic-export.ts +53 -20
- package/src/runtime/direct-run.ts +12 -2
- package/src/runtime/dwf-state-store.ts +1 -1
- package/src/runtime/dynamic-workflow-context.ts +187 -59
- package/src/runtime/dynamic-workflow-runner.ts +42 -20
- package/src/runtime/effectiveness.ts +16 -8
- package/src/runtime/errors/crew-errors.ts +7 -11
- package/src/runtime/event-stream-bridge.ts +9 -3
- package/src/runtime/foreground-control.ts +54 -14
- package/src/runtime/foreground-watchdog.ts +9 -6
- package/src/runtime/goal-achievement.ts +27 -10
- package/src/runtime/goal-evaluator.ts +54 -19
- package/src/runtime/goal-loop-runner.ts +280 -99
- package/src/runtime/goal-state-store.ts +27 -17
- package/src/runtime/green-contract.ts +11 -2
- package/src/runtime/group-join.ts +64 -31
- package/src/runtime/handoff-manager.ts +37 -42
- package/src/runtime/heartbeat-gradient.ts +10 -2
- package/src/runtime/heartbeat-watcher.ts +32 -6
- package/src/runtime/hidden-handoff.ts +13 -31
- package/src/runtime/important-line-classifier.ts +12 -2
- package/src/runtime/iteration-hooks.ts +25 -15
- package/src/runtime/live-agent-control.ts +55 -7
- package/src/runtime/live-agent-manager.ts +130 -27
- package/src/runtime/live-control-realtime.ts +23 -3
- package/src/runtime/live-irc.ts +4 -1
- package/src/runtime/live-session-health.ts +12 -4
- package/src/runtime/live-session-runtime.ts +379 -117
- package/src/runtime/manifest-cache.ts +28 -12
- package/src/runtime/mcp-proxy.ts +10 -18
- package/src/runtime/metric-parser.ts +1 -5
- package/src/runtime/model-fallback.ts +55 -18
- package/src/runtime/model-resolver.ts +2 -6
- package/src/runtime/model-scope.ts +19 -3
- package/src/runtime/notebook-helpers.ts +60 -62
- package/src/runtime/orphan-worker-registry.ts +37 -26
- package/src/runtime/output-validator.ts +18 -3
- package/src/runtime/overflow-recovery.ts +5 -4
- package/src/runtime/parallel-research.ts +29 -5
- package/src/runtime/parallel-utils.ts +9 -11
- package/src/runtime/path-overlap.ts +150 -0
- package/src/runtime/peer-dep.ts +12 -17
- package/src/runtime/per-write-validator.ts +1 -3
- package/src/runtime/phase-tracker.ts +342 -330
- package/src/runtime/pi-args.ts +26 -8
- package/src/runtime/pi-json-output.ts +1 -1
- package/src/runtime/pi-spawn.ts +30 -19
- package/src/runtime/pipeline-runner.ts +56 -55
- package/src/runtime/plan-templates.ts +5 -6
- package/src/runtime/policy-engine.ts +43 -7
- package/src/runtime/post-checks.ts +12 -4
- package/src/runtime/post-exit-stdio-guard.ts +2 -2
- package/src/runtime/process-lifecycle.ts +20 -10
- package/src/runtime/process-status.ts +16 -5
- package/src/runtime/progress-event-coalescer.ts +2 -1
- package/src/runtime/progress-tracker.ts +103 -103
- package/src/runtime/prose-compressor.ts +9 -11
- package/src/runtime/recovery-recipes.ts +118 -24
- package/src/runtime/replace.ts +25 -10
- package/src/runtime/resilient-edit.ts +10 -25
- package/src/runtime/result-extractor.ts +2 -6
- package/src/runtime/retry-executor.ts +20 -4
- package/src/runtime/retry-runner.ts +27 -51
- package/src/runtime/role-permission.ts +6 -1
- package/src/runtime/run-coalesced-task-group.ts +256 -0
- package/src/runtime/run-drift.ts +14 -15
- package/src/runtime/run-tracker.ts +6 -28
- package/src/runtime/runtime-policy.ts +1 -6
- package/src/runtime/runtime-resolver.ts +80 -15
- package/src/runtime/scheduler.ts +47 -18
- package/src/runtime/semaphore.ts +5 -7
- package/src/runtime/sensitive-paths.ts +2 -1
- package/src/runtime/session-usage.ts +1 -1
- package/src/runtime/settings-store.ts +16 -12
- package/src/runtime/sidechain-output.ts +6 -2
- package/src/runtime/single-agent-compose.ts +1 -4
- package/src/runtime/skill-effectiveness.ts +29 -97
- package/src/runtime/skill-instructions.ts +40 -83
- package/src/runtime/stale-reconciler.ts +73 -197
- package/src/runtime/stream-preview.ts +1 -1
- package/src/runtime/streaming-output.ts +1 -1
- package/src/runtime/subagent-manager.ts +44 -169
- package/src/runtime/subprocess-tool-registry.ts +4 -1
- package/src/runtime/supervisor-contact.ts +10 -5
- package/src/runtime/task-display.ts +19 -5
- package/src/runtime/task-graph-scheduler.ts +95 -21
- package/src/runtime/task-graph.ts +5 -11
- package/src/runtime/task-health.ts +54 -47
- package/src/runtime/task-id.ts +12 -19
- package/src/runtime/task-output-context.ts +265 -81
- package/src/runtime/task-packet.ts +12 -20
- package/src/runtime/task-quality.ts +6 -14
- package/src/runtime/task-runner/context-retrieval.ts +4 -13
- package/src/runtime/task-runner/live-executor.ts +114 -36
- package/src/runtime/task-runner/output-splitter.ts +152 -0
- package/src/runtime/task-runner/progress.ts +50 -12
- package/src/runtime/task-runner/prompt-builder.ts +31 -7
- package/src/runtime/task-runner/prompt-pipeline.ts +31 -7
- package/src/runtime/task-runner/result-utils.ts +3 -1
- package/src/runtime/task-runner/retrieval-orchestrator.ts +310 -0
- package/src/runtime/task-runner/run-projection.ts +27 -8
- package/src/runtime/task-runner/state-helpers.ts +50 -14
- package/src/runtime/task-runner/tail-read.ts +2 -6
- package/src/runtime/task-runner.ts +180 -326
- package/src/runtime/team-runner-artifacts.ts +13 -0
- package/src/runtime/team-runner.ts +456 -974
- package/src/runtime/tool-output-pruner.ts +6 -9
- package/src/runtime/tool-progress.ts +11 -21
- package/src/runtime/verification-gates.ts +33 -24
- package/src/runtime/verification-integrity.ts +2 -7
- package/src/runtime/verification-worktree.ts +62 -12
- package/src/runtime/worker-heartbeat.ts +5 -1
- package/src/runtime/worker-startup.ts +29 -5
- package/src/runtime/workflow-state.ts +17 -9
- package/src/runtime/workspace-lock.ts +30 -35
- package/src/runtime/workspace-tree.ts +20 -32
- package/src/runtime/yield-handler.ts +43 -9
- package/src/runtime/zombie-scanner.ts +5 -4
- package/src/schema/config-schema.ts +266 -172
- package/src/schema/team-tool-schema.ts +29 -74
- package/src/schema/validation-types.ts +25 -17
- package/src/skills/discover-skills.ts +35 -10
- package/src/skills/skill-templates.ts +109 -27
- package/src/skills/validate.ts +26 -8
- package/src/state/active-run-registry.ts +87 -28
- package/src/state/artifact-store.ts +9 -5
- package/src/state/atomic-write-v2.ts +85 -63
- package/src/state/atomic-write.ts +106 -27
- package/src/state/blob-store.ts +47 -20
- package/src/state/contracts.ts +20 -5
- package/src/state/crew-init.ts +5 -22
- package/src/state/decision-ledger.ts +19 -73
- package/src/state/event-log-rotation.ts +41 -15
- package/src/state/event-log.ts +168 -53
- package/src/state/event-reconstructor.ts +11 -1
- package/src/state/gitignore-manager.ts +2 -8
- package/src/state/health-store.ts +57 -57
- package/src/state/hook-instinct-bridge.ts +1 -1
- package/src/state/hook-integrations.ts +1 -1
- package/src/state/instinct-store.ts +17 -5
- package/src/state/jsonl-writer.ts +1 -1
- package/src/state/locks.ts +23 -9
- package/src/state/mailbox.ts +151 -28
- package/src/state/observation-store.ts +20 -14
- package/src/state/run-cache.ts +142 -135
- package/src/state/run-graph.ts +6 -15
- package/src/state/run-metrics.ts +5 -18
- package/src/state/schedule.ts +15 -9
- package/src/state/state-store.ts +137 -47
- package/src/state/task-claims.ts +12 -2
- package/src/state/tiered-eval.ts +52 -43
- package/src/state/types-eval.ts +2 -2
- package/src/state/types.ts +24 -20
- package/src/state/usage.ts +20 -4
- package/src/state/worker-atomic-writer.ts +6 -3
- package/src/subagents/index.ts +2 -2
- package/src/teams/discover-teams.ts +21 -6
- package/src/tools/safe-bash-extension.ts +5 -6
- package/src/tools/safe-bash.ts +10 -11
- package/src/types/new-api-types.ts +6 -10
- package/src/ui/agent-management-overlay.ts +52 -40
- package/src/ui/card-colors.ts +13 -4
- package/src/ui/crew-footer.ts +8 -7
- package/src/ui/crew-select-list.ts +1 -1
- package/src/ui/dashboard-panes/agents-pane.ts +41 -25
- package/src/ui/dashboard-panes/cancellation-pane.ts +1 -1
- package/src/ui/dashboard-panes/capability-pane.ts +32 -15
- package/src/ui/dashboard-panes/health-pane.ts +2 -1
- package/src/ui/dashboard-panes/metrics-pane.ts +4 -1
- package/src/ui/dashboard-panes/progress-pane.ts +12 -9
- package/src/ui/heartbeat-aggregator.ts +15 -4
- package/src/ui/keybinding-map.ts +40 -7
- package/src/ui/live-conversation-overlay.ts +36 -20
- package/src/ui/live-duration.ts +1 -4
- package/src/ui/live-run-sidebar.ts +88 -25
- package/src/ui/loaders.ts +2 -8
- package/src/ui/mascot.ts +20 -36
- package/src/ui/overlays/agent-picker-overlay.ts +11 -3
- package/src/ui/overlays/confirm-overlay.ts +4 -2
- package/src/ui/overlays/help-overlay.ts +25 -14
- package/src/ui/overlays/mailbox-compose-overlay.ts +48 -11
- package/src/ui/overlays/mailbox-compose-preview.ts +20 -5
- package/src/ui/overlays/mailbox-detail-overlay.ts +27 -6
- package/src/ui/pi-ui-compat.ts +7 -7
- package/src/ui/powerbar-publisher.ts +120 -42
- package/src/ui/render-diff.ts +10 -3
- package/src/ui/render-scheduler.ts +12 -4
- package/src/ui/run-action-dispatcher.ts +81 -18
- package/src/ui/run-dashboard.ts +172 -76
- package/src/ui/run-event-bus.ts +59 -37
- package/src/ui/run-snapshot-cache.ts +276 -86
- package/src/ui/settings-overlay.ts +361 -74
- package/src/ui/status-colors.ts +12 -1
- package/src/ui/syntax-highlight.ts +1 -1
- package/src/ui/terminal-status.ts +1 -3
- package/src/ui/theme-adapter.ts +16 -14
- package/src/ui/theme-discovery.ts +13 -2
- package/src/ui/tool-progress-formatter.ts +7 -7
- package/src/ui/tool-render.ts +128 -56
- package/src/ui/tool-renderers/brief-mode.ts +45 -27
- package/src/ui/tool-renderers/index.ts +109 -43
- package/src/ui/transcript-cache.ts +32 -6
- package/src/ui/transcript-entries.ts +25 -23
- package/src/ui/transcript-viewer.ts +78 -29
- package/src/ui/widget/index.ts +99 -40
- package/src/ui/widget/widget-formatters.ts +13 -6
- package/src/ui/widget/widget-model.ts +19 -8
- package/src/ui/widget/widget-renderer.ts +23 -13
- package/src/ui/widget/widget-types.ts +1 -1
- package/src/utils/bm25-search.ts +199 -199
- package/src/utils/conflict-detect.ts +22 -21
- package/src/utils/env-filter.ts +14 -7
- package/src/utils/file-coalescer.ts +5 -1
- package/src/utils/fingerprint.ts +3 -6
- package/src/utils/frontmatter.ts +3 -1
- package/src/utils/fs-watch.ts +2 -6
- package/src/utils/gh-protocol.ts +119 -44
- package/src/utils/git.ts +13 -15
- package/src/utils/guards.ts +2 -5
- package/src/utils/ids.ts +9 -2
- package/src/utils/incremental-reader.ts +14 -3
- package/src/utils/internal-error.ts +2 -1
- package/src/utils/names.ts +12 -3
- package/src/utils/paths.ts +49 -5
- package/src/utils/project-detector.ts +2 -2
- package/src/utils/redaction.ts +29 -19
- package/src/utils/resolve-shell.ts +9 -7
- package/src/utils/run-watcher-registry.ts +19 -31
- package/src/utils/safe-paths.ts +46 -29
- package/src/utils/scan-cache.ts +9 -2
- package/src/utils/session-utils.ts +2 -4
- package/src/utils/sleep.ts +12 -5
- package/src/utils/sse-parser.ts +5 -17
- package/src/utils/visual.ts +52 -32
- package/src/workflows/discover-workflows.ts +41 -109
- package/src/workflows/intermediate-store.ts +5 -21
- package/src/workflows/preflight-validator.ts +6 -33
- package/src/workflows/topology-analyzer.ts +5 -21
- package/src/workflows/workflow-config.ts +7 -6
- package/src/worktree/branch-freshness.ts +66 -8
- package/src/worktree/cleanup.ts +130 -20
- package/src/worktree/worktree-manager.ts +212 -69
- package/skills/artifact-analysis-loop/SKILL.md +0 -303
- package/skills/detection-pipeline-design/SKILL.md +0 -286
- package/skills/hunting-investigation-loop/SKILL.md +0 -402
- package/skills/incident-playbook-construction/SKILL.md +0 -384
- package/skills/security-review/SKILL.md +0 -561
- package/skills/threat-hypothesis-framework/SKILL.md +0 -176
- package/skills/ui-render-performance/SKILL.md +0 -58
- /package/docs/{followup-review-round3-2026-05-12.md → archive/followup-review-round3-2026-05-12.md} +0 -0
- /package/docs/{followup-review-round4-2026-05-13.md → archive/followup-review-round4-2026-05-13.md} +0 -0
- /package/docs/{pi-crew-bugs.md → archive/pi-crew-bugs.md} +0 -0
- /package/docs/{pi-crew-test-final.md → archive/pi-crew-test-final.md} +0 -0
- /package/docs/{pi-crew-test-results.md → archive/pi-crew-test-results.md} +0 -0
- /package/docs/{pi-crew-test-round2.md → archive/pi-crew-test-round2.md} +0 -0
- /package/docs/{pi-crew-test-round4.md → archive/pi-crew-test-round4.md} +0 -0
- /package/docs/{pi-crew-test-round5.md → archive/pi-crew-test-round5.md} +0 -0
- /package/docs/{pi-crew-test-round6.md → archive/pi-crew-test-round6.md} +0 -0
- /package/docs/{pi-crew-v0.5.10-audit-fix-plan.md → archive/pi-crew-v0.5.10-audit-fix-plan.md} +0 -0
- /package/docs/{pi-crew-v0.5.11-audit-fix-plan.md → archive/pi-crew-v0.5.11-audit-fix-plan.md} +0 -0
- /package/docs/{pi-crew-v0.5.12-audit-fix-plan.md → archive/pi-crew-v0.5.12-audit-fix-plan.md} +0 -0
- /package/docs/{pi-crew-v0.5.13-audit-fix-plan.md → archive/pi-crew-v0.5.13-audit-fix-plan.md} +0 -0
- /package/docs/{pi-crew-v0.5.14-audit-fix-plan.md → archive/pi-crew-v0.5.14-audit-fix-plan.md} +0 -0
- /package/docs/{pi-crew-v0.5.16-audit-fix-plan.md → archive/pi-crew-v0.5.16-audit-fix-plan.md} +0 -0
- /package/docs/{pi-crew-v0.5.17-audit-fix-plan.md → archive/pi-crew-v0.5.17-audit-fix-plan.md} +0 -0
- /package/docs/{pi-crew-v0.5.5-audit-fix-plan.md → archive/pi-crew-v0.5.5-audit-fix-plan.md} +0 -0
- /package/docs/{pi-crew-v0.5.9-audit-fix-plan.md → archive/pi-crew-v0.5.9-audit-fix-plan.md} +0 -0
- /package/docs/{pi-mono-opportunities.md → archive/pi-mono-opportunities.md} +0 -0
- /package/docs/{pi-mono-review.md → archive/pi-mono-review.md} +0 -0
- /package/docs/{pi-subagent4-comparison.md → archive/pi-subagent4-comparison.md} +0 -0
- /package/docs/{pi-subagents3-deep-analysis.md → archive/pi-subagents3-deep-analysis.md} +0 -0
|
@@ -3,8 +3,8 @@ import * as fs from "node:fs";
|
|
|
3
3
|
import * as os from "node:os";
|
|
4
4
|
import * as path from "node:path";
|
|
5
5
|
import { logInternalError } from "../utils/internal-error.ts";
|
|
6
|
-
import { isWorkerAtomicWriterEnabled, atomicWriteFileViaWorker } from "./worker-atomic-writer.ts";
|
|
7
6
|
import { sleepSync } from "../utils/sleep.ts";
|
|
7
|
+
import { atomicWriteFileViaWorker, isWorkerAtomicWriterEnabled } from "./worker-atomic-writer.ts";
|
|
8
8
|
|
|
9
9
|
function hashContent(content: string): string {
|
|
10
10
|
return crypto.createHash("sha256").update(content, "utf-8").digest("hex");
|
|
@@ -68,7 +68,9 @@ export function isSymlinkSafePath(filePath: string): boolean {
|
|
|
68
68
|
// Found existing ancestor — join with remaining non-existent parts
|
|
69
69
|
realBase = path.join(resolved, path.relative(walk, baseDir));
|
|
70
70
|
break;
|
|
71
|
-
} catch {
|
|
71
|
+
} catch {
|
|
72
|
+
/* not found yet, keep walking */
|
|
73
|
+
}
|
|
72
74
|
}
|
|
73
75
|
const realDirNorm = realDir.endsWith(path.sep) ? realDir : realDir + path.sep;
|
|
74
76
|
const realBaseNorm = realBase.endsWith(path.sep) ? realBase : realBase + path.sep;
|
|
@@ -84,7 +86,11 @@ export function isSymlinkSafePath(filePath: string): boolean {
|
|
|
84
86
|
if (typeof process.getuid === "function" && realStat.uid !== process.getuid()) {
|
|
85
87
|
const systemTmp = (typeof os.tmpdir === "function" ? os.tmpdir() : "/tmp").toLowerCase();
|
|
86
88
|
let realSystemTmp = systemTmp;
|
|
87
|
-
try {
|
|
89
|
+
try {
|
|
90
|
+
realSystemTmp = fs.realpathSync(systemTmp).toLowerCase();
|
|
91
|
+
} catch {
|
|
92
|
+
/* ok */
|
|
93
|
+
}
|
|
88
94
|
const realDirLower = realDir.toLowerCase();
|
|
89
95
|
const dirNorm = realDirLower.endsWith(path.sep) ? realDirLower : realDirLower + path.sep;
|
|
90
96
|
const tmpNorm = realSystemTmp.endsWith(path.sep) ? realSystemTmp : realSystemTmp + path.sep;
|
|
@@ -95,11 +101,14 @@ export function isSymlinkSafePath(filePath: string): boolean {
|
|
|
95
101
|
// Also accept canonical system temp paths:
|
|
96
102
|
// - /tmp → /private/tmp (macOS)
|
|
97
103
|
// - /var/folders → /private/var/folders (macOS)
|
|
98
|
-
const isSystemTmp =
|
|
99
|
-
|
|
100
|
-
realDirLower === "/
|
|
101
|
-
|
|
102
|
-
|
|
104
|
+
const isSystemTmp =
|
|
105
|
+
process.platform === "darwin" &&
|
|
106
|
+
(realDirLower === "/tmp" ||
|
|
107
|
+
realDirLower === "/private/tmp" ||
|
|
108
|
+
realDirLower === "/var/folders" ||
|
|
109
|
+
realDirLower === "/private/var/folders" ||
|
|
110
|
+
realDirLower.startsWith("/var/folders/") ||
|
|
111
|
+
realDirLower.startsWith("/private/var/folders/"));
|
|
103
112
|
if (!isInsideTmp && !isAncestorOfTmp && !isSystemTmp) {
|
|
104
113
|
return false;
|
|
105
114
|
}
|
|
@@ -124,7 +133,6 @@ export function isSymlinkSafePath(filePath: string): boolean {
|
|
|
124
133
|
currentPath = dir;
|
|
125
134
|
}
|
|
126
135
|
|
|
127
|
-
|
|
128
136
|
// Issue 1 fix: verify baseDir itself is not a symlink.
|
|
129
137
|
// The while loop above walks ancestors but stops when currentPath reaches
|
|
130
138
|
// root (currentPath === path.dirname(currentPath)). If baseDir is a symlink
|
|
@@ -152,18 +160,20 @@ export function isSymlinkSafePath(filePath: string): boolean {
|
|
|
152
160
|
}
|
|
153
161
|
}
|
|
154
162
|
|
|
155
|
-
|
|
156
|
-
|
|
157
163
|
function sleep(ms: number): Promise<void> {
|
|
158
164
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
159
165
|
}
|
|
160
166
|
|
|
161
167
|
function isRetryableRenameError(error: unknown): boolean {
|
|
162
|
-
return Boolean(
|
|
168
|
+
return Boolean(
|
|
169
|
+
error && typeof error === "object" && "code" in error && RETRYABLE_RENAME_CODES.has(String((error as NodeJS.ErrnoException).code)),
|
|
170
|
+
);
|
|
163
171
|
}
|
|
164
172
|
|
|
165
173
|
function isRetryableLinkError(error: unknown): boolean {
|
|
166
|
-
return Boolean(
|
|
174
|
+
return Boolean(
|
|
175
|
+
error && typeof error === "object" && "code" in error && RETRYABLE_LINK_CODES.has(String((error as NodeJS.ErrnoException).code)),
|
|
176
|
+
);
|
|
167
177
|
}
|
|
168
178
|
|
|
169
179
|
/**
|
|
@@ -181,7 +191,11 @@ function renameWithLinkSync(tempPath: string, filePath: string, retries = 8): vo
|
|
|
181
191
|
// path aliases are mixed (e.g. RUNNER~1 vs runneradmin). Use
|
|
182
192
|
// renameSync which handles this correctly via MoveFileEx.
|
|
183
193
|
if (process.platform === "win32") {
|
|
184
|
-
try {
|
|
194
|
+
try {
|
|
195
|
+
fs.unlinkSync(filePath);
|
|
196
|
+
} catch {
|
|
197
|
+
/* destination may not exist */
|
|
198
|
+
}
|
|
185
199
|
try {
|
|
186
200
|
fs.renameSync(tempPath, filePath);
|
|
187
201
|
return;
|
|
@@ -191,7 +205,11 @@ function renameWithLinkSync(tempPath: string, filePath: string, retries = 8): vo
|
|
|
191
205
|
}
|
|
192
206
|
} else {
|
|
193
207
|
// First try to unlink any existing file at destination (hard links don't follow symlinks)
|
|
194
|
-
try {
|
|
208
|
+
try {
|
|
209
|
+
fs.unlinkSync(filePath);
|
|
210
|
+
} catch {
|
|
211
|
+
/* destination may not exist */
|
|
212
|
+
}
|
|
195
213
|
// Create hard link - does NOT follow symlinks at filePath
|
|
196
214
|
fs.linkSync(tempPath, filePath);
|
|
197
215
|
// Successfully linked - now unlink the temp file
|
|
@@ -214,7 +232,11 @@ async function renameWithLinkAsync(tempPath: string, filePath: string, retries =
|
|
|
214
232
|
for (let attempt = 0; attempt <= retries; attempt++) {
|
|
215
233
|
try {
|
|
216
234
|
if (process.platform === "win32") {
|
|
217
|
-
try {
|
|
235
|
+
try {
|
|
236
|
+
await fs.promises.unlink(filePath);
|
|
237
|
+
} catch {
|
|
238
|
+
/* destination may not exist */
|
|
239
|
+
}
|
|
218
240
|
try {
|
|
219
241
|
await fs.promises.rename(tempPath, filePath);
|
|
220
242
|
return;
|
|
@@ -223,7 +245,11 @@ async function renameWithLinkAsync(tempPath: string, filePath: string, retries =
|
|
|
223
245
|
if (!isRetryableLinkError(renameError) || attempt === retries) break;
|
|
224
246
|
}
|
|
225
247
|
} else {
|
|
226
|
-
try {
|
|
248
|
+
try {
|
|
249
|
+
await fs.promises.unlink(filePath);
|
|
250
|
+
} catch {
|
|
251
|
+
/* destination may not exist */
|
|
252
|
+
}
|
|
227
253
|
await fs.promises.link(tempPath, filePath);
|
|
228
254
|
await fs.promises.unlink(tempPath);
|
|
229
255
|
return;
|
|
@@ -239,7 +265,12 @@ async function renameWithLinkAsync(tempPath: string, filePath: string, retries =
|
|
|
239
265
|
throw lastError;
|
|
240
266
|
}
|
|
241
267
|
|
|
242
|
-
export function renameWithRetry(
|
|
268
|
+
export function renameWithRetry(
|
|
269
|
+
tempPath: string,
|
|
270
|
+
filePath: string,
|
|
271
|
+
retries = 8,
|
|
272
|
+
rename: (oldPath: string, newPath: string) => void = fs.renameSync,
|
|
273
|
+
): void {
|
|
243
274
|
let lastError: unknown;
|
|
244
275
|
for (let attempt = 0; attempt <= retries; attempt++) {
|
|
245
276
|
try {
|
|
@@ -262,7 +293,12 @@ export function renameWithRetry(tempPath: string, filePath: string, retries = 8,
|
|
|
262
293
|
/** Test alias for renameWithRetry. */
|
|
263
294
|
export const __test__renameWithRetry = renameWithRetry;
|
|
264
295
|
|
|
265
|
-
export async function renameWithRetryAsync(
|
|
296
|
+
export async function renameWithRetryAsync(
|
|
297
|
+
tempPath: string,
|
|
298
|
+
filePath: string,
|
|
299
|
+
retries = 8,
|
|
300
|
+
rename: (oldPath: string, newPath: string) => Promise<void> = (source, destination) => fs.promises.rename(source, destination),
|
|
301
|
+
): Promise<void> {
|
|
266
302
|
let lastError: unknown;
|
|
267
303
|
for (let attempt = 0; attempt <= retries; attempt++) {
|
|
268
304
|
try {
|
|
@@ -303,7 +339,11 @@ export function atomicWriteFile(filePath: string, content: string, expectedHash?
|
|
|
303
339
|
const r = fs.realpathSync.native(p);
|
|
304
340
|
return r.startsWith("\\\\?\\") ? r.slice(4) : r;
|
|
305
341
|
} catch {
|
|
306
|
-
try {
|
|
342
|
+
try {
|
|
343
|
+
return fs.realpathSync(p);
|
|
344
|
+
} catch {
|
|
345
|
+
return p;
|
|
346
|
+
}
|
|
307
347
|
}
|
|
308
348
|
};
|
|
309
349
|
const dirPath = path.dirname(filePath);
|
|
@@ -312,7 +352,7 @@ export function atomicWriteFile(filePath: string, content: string, expectedHash?
|
|
|
312
352
|
} catch (error) {
|
|
313
353
|
if (process.platform === "win32" && (error as NodeJS.ErrnoException).code === "EPERM") {
|
|
314
354
|
// mkdir hit a short/long-name alias wall — retry with the canonical
|
|
315
|
-
|
|
355
|
+
// form. The write itself still targets the original filePath below.
|
|
316
356
|
const realDir = canonicalize(dirPath);
|
|
317
357
|
if (realDir !== dirPath) fs.mkdirSync(realDir, { recursive: true });
|
|
318
358
|
} else {
|
|
@@ -332,6 +372,16 @@ export function atomicWriteFile(filePath: string, content: string, expectedHash?
|
|
|
332
372
|
throw new Error(`Refusing to write: opened path is not a regular file: ${tempPath}`);
|
|
333
373
|
}
|
|
334
374
|
fs.writeSync(fd, content, undefined, "utf-8");
|
|
375
|
+
// FIX (2026-07-01 mailbox-replay CI flake investigation): fsync the data
|
|
376
|
+
// before close. Without this, writeSync puts content in page cache but
|
|
377
|
+
// the data may not be flushed to disk before the subsequent rename.
|
|
378
|
+
// On shared CI runners (Ubuntu, high I/O pressure), a subsequent
|
|
379
|
+
// readFileSync could see the OLD content if the page cache evicted
|
|
380
|
+
// between write and read. fsyncSync forces the data to disk so the
|
|
381
|
+
// rename + post-rename read always sees the new content. Cost: ~1ms
|
|
382
|
+
// per write (acceptable for the small delivery.json / manifest.json
|
|
383
|
+
// files this function writes; large state-store uses the async path).
|
|
384
|
+
fs.fsyncSync(fd);
|
|
335
385
|
fs.closeSync(fd);
|
|
336
386
|
try {
|
|
337
387
|
// Issue 1 fix: re-check symlink safety immediately before rename.
|
|
@@ -346,6 +396,23 @@ export function atomicWriteFile(filePath: string, content: string, expectedHash?
|
|
|
346
396
|
}
|
|
347
397
|
// Issue 1 fix: use link+unlink instead of rename to avoid following symlinks
|
|
348
398
|
renameWithLinkSync(tempPath, filePath);
|
|
399
|
+
// FIX (2026-07-01 mailbox-replay CI flake): fsync the parent directory
|
|
400
|
+
// after rename so the directory entry update is durable. On most Linux
|
|
401
|
+
// filesystems, rename is journaled but the journal flush isn't always
|
|
402
|
+
// synchronous; without this, a subsequent open() after rename can
|
|
403
|
+
// race and see ENOENT or stale content on high-IO CI runners.
|
|
404
|
+
// We skip on Windows where directory fsync semantics differ (and
|
|
405
|
+
// where the Windows-specific rename path in renameWithLinkSync already
|
|
406
|
+
// uses MoveFileEx which is fully durable).
|
|
407
|
+
if (process.platform !== "win32") {
|
|
408
|
+
try {
|
|
409
|
+
const dirFd = fs.openSync(path.dirname(filePath), "r");
|
|
410
|
+
fs.fsyncSync(dirFd);
|
|
411
|
+
fs.closeSync(dirFd);
|
|
412
|
+
} catch {
|
|
413
|
+
/* best-effort — not all filesystems support directory fsync */
|
|
414
|
+
}
|
|
415
|
+
}
|
|
349
416
|
} catch (renameError) {
|
|
350
417
|
// Issue 4 fix: use finally block to guarantee temp file cleanup.
|
|
351
418
|
// Between the initial isSymlinkSafePath check and rename attempt,
|
|
@@ -374,12 +441,15 @@ export function atomicWriteFile(filePath: string, content: string, expectedHash?
|
|
|
374
441
|
// Issue 4 fix: always clean up temp file, regardless of success or error path.
|
|
375
442
|
// This ensures no orphaned temp files remain when errors occur at any point.
|
|
376
443
|
if (fd !== undefined) {
|
|
377
|
-
try {
|
|
444
|
+
try {
|
|
445
|
+
fs.rmSync(tempPath, { force: true });
|
|
446
|
+
} catch {
|
|
447
|
+
/* best-effort */
|
|
448
|
+
}
|
|
378
449
|
}
|
|
379
450
|
}
|
|
380
451
|
}
|
|
381
452
|
|
|
382
|
-
|
|
383
453
|
export async function atomicWriteFileAsync(filePath: string, content: string): Promise<void> {
|
|
384
454
|
// Phase 1.5 (RFC 15): when the worker-thread atomic writer is enabled
|
|
385
455
|
// (PI_CREW_WORKER_ATOMIC_WRITER=1), dispatch to a dedicated worker thread
|
|
@@ -412,7 +482,11 @@ export async function atomicWriteFileAsync(filePath: string, content: string): P
|
|
|
412
482
|
// The post-rename lstat check only runs on rename failure, so we must
|
|
413
483
|
// check BEFORE the rename to catch this TOCTOU race.
|
|
414
484
|
if (!isSymlinkSafePath(filePath)) {
|
|
415
|
-
try {
|
|
485
|
+
try {
|
|
486
|
+
await fs.promises.rm(tempPath, { force: true });
|
|
487
|
+
} catch {
|
|
488
|
+
/* best-effort */
|
|
489
|
+
}
|
|
416
490
|
throw new Error(`Refusing to rename: target became a symlink or inside untrusted directory: ${filePath}`);
|
|
417
491
|
}
|
|
418
492
|
// Issue 1 fix: use link+unlink instead of rename to avoid following symlinks
|
|
@@ -445,7 +519,6 @@ export async function atomicWriteFileAsync(filePath: string, content: string): P
|
|
|
445
519
|
}
|
|
446
520
|
}
|
|
447
521
|
|
|
448
|
-
|
|
449
522
|
export function atomicWriteJson<T>(filePath: string, value: T): void {
|
|
450
523
|
atomicWriteFile(filePath, `${JSON.stringify(value, null, 2)}\n`);
|
|
451
524
|
}
|
|
@@ -500,7 +573,13 @@ export function atomicWriteJsonCoalesced<T>(filePath: string, value: T, coalesce
|
|
|
500
573
|
timer.unref();
|
|
501
574
|
// Issue 2 fix: increment generation for each new entry
|
|
502
575
|
const generation = ++writeGeneration;
|
|
503
|
-
pendingAtomicWrites.set(filePath, {
|
|
576
|
+
pendingAtomicWrites.set(filePath, {
|
|
577
|
+
content,
|
|
578
|
+
timer,
|
|
579
|
+
coalesceMs,
|
|
580
|
+
retryCount: 0,
|
|
581
|
+
generation,
|
|
582
|
+
});
|
|
504
583
|
}
|
|
505
584
|
|
|
506
585
|
function flushOnePendingAtomicWrite(filePath: string): void {
|
|
@@ -536,7 +615,7 @@ function flushOnePendingAtomicWrite(filePath: string): void {
|
|
|
536
615
|
throw error;
|
|
537
616
|
}
|
|
538
617
|
// Exponential backoff: base delay * 2^(retryCount-1), capped at 30 seconds
|
|
539
|
-
const backoffMs = Math.min(30000, current.coalesceMs *
|
|
618
|
+
const backoffMs = Math.min(30000, current.coalesceMs * 2 ** (current.retryCount - 1));
|
|
540
619
|
const timer = setTimeout(() => flushOnePendingAtomicWrite(filePath), backoffMs);
|
|
541
620
|
timer.unref();
|
|
542
621
|
current.timer = timer;
|
package/src/state/blob-store.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
1
2
|
import * as fs from "node:fs";
|
|
2
3
|
import * as path from "node:path";
|
|
3
|
-
import { createHash, randomUUID } from "node:crypto";
|
|
4
4
|
import { resolveRealContainedPath } from "../utils/safe-paths.ts";
|
|
5
|
+
import { sleepSync } from "../utils/sleep.ts";
|
|
5
6
|
import { atomicWriteFile, renameWithRetry } from "./atomic-write.ts";
|
|
6
7
|
import { withFileLockSync } from "./locks.ts";
|
|
7
|
-
import { sleepSync } from "../utils/sleep.ts";
|
|
8
8
|
|
|
9
9
|
const SHA256_HEX = /^[a-f0-9]{64}$/i;
|
|
10
10
|
|
|
@@ -32,11 +32,19 @@ function atomicWriteBuffer(filePath: string, content: Buffer): void {
|
|
|
32
32
|
fs.writeSync(fd, content, 0, content.length);
|
|
33
33
|
renameWithRetry(tempPath, filePath);
|
|
34
34
|
} catch (error) {
|
|
35
|
-
try {
|
|
35
|
+
try {
|
|
36
|
+
fs.rmSync(tempPath, { force: true });
|
|
37
|
+
} catch {
|
|
38
|
+
/* best-effort */
|
|
39
|
+
}
|
|
36
40
|
throw error;
|
|
37
41
|
} finally {
|
|
38
42
|
// Always close fd; closeSync is safe to call even if fd was already closed
|
|
39
|
-
try {
|
|
43
|
+
try {
|
|
44
|
+
fs.closeSync(fd);
|
|
45
|
+
} catch {
|
|
46
|
+
/* best-effort */
|
|
47
|
+
}
|
|
40
48
|
}
|
|
41
49
|
}
|
|
42
50
|
|
|
@@ -75,7 +83,9 @@ export interface BlobWriteResult {
|
|
|
75
83
|
}
|
|
76
84
|
|
|
77
85
|
function sha256Of(content: string | Buffer): string {
|
|
78
|
-
return createHash("sha256")
|
|
86
|
+
return createHash("sha256")
|
|
87
|
+
.update(typeof content === "string" ? content : content)
|
|
88
|
+
.digest("hex");
|
|
79
89
|
}
|
|
80
90
|
|
|
81
91
|
/**
|
|
@@ -85,16 +95,19 @@ function sha256Of(content: string | Buffer): string {
|
|
|
85
95
|
* partial writes on crash. The deduplication check is now advisory (the atomic
|
|
86
96
|
* write handles concurrent writes correctly via O_EXCL temp file pattern).
|
|
87
97
|
*/
|
|
88
|
-
export function writeBlob(
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
+
export function writeBlob(
|
|
99
|
+
artifactsRoot: string,
|
|
100
|
+
input: {
|
|
101
|
+
content: string | Buffer;
|
|
102
|
+
runId: string;
|
|
103
|
+
taskId?: string;
|
|
104
|
+
mime?: string;
|
|
105
|
+
producer: string;
|
|
106
|
+
originalPath: string;
|
|
107
|
+
redacted?: boolean;
|
|
108
|
+
retention?: BlobMetadata["retention"];
|
|
109
|
+
},
|
|
110
|
+
): BlobWriteResult {
|
|
98
111
|
const content = input.content;
|
|
99
112
|
const hash = sha256Of(content);
|
|
100
113
|
const algorithm = SHA256_PREFIX;
|
|
@@ -165,11 +178,15 @@ export function writeBlob(artifactsRoot: string, input: {
|
|
|
165
178
|
// taskId, the conflict check passes and both writes succeed — the
|
|
166
179
|
// final taskId reflects whichever write completed last.
|
|
167
180
|
// Callers should not rely on taskId for correctness-critical decisions.
|
|
168
|
-
if (
|
|
181
|
+
if (
|
|
182
|
+
existingMeta.mime !== metadata.mime ||
|
|
169
183
|
existingMeta.retention !== metadata.retention ||
|
|
170
184
|
existingMeta.producer !== metadata.producer ||
|
|
171
|
-
existingMeta.originalPath !== metadata.originalPath
|
|
172
|
-
|
|
185
|
+
existingMeta.originalPath !== metadata.originalPath
|
|
186
|
+
) {
|
|
187
|
+
throw new Error(
|
|
188
|
+
`Concurrent metadata write conflict for blob ${hash}: different metadata values detected. Existing: ${JSON.stringify(existingMeta)}, New: ${JSON.stringify(metadata)}`,
|
|
189
|
+
);
|
|
173
190
|
}
|
|
174
191
|
} catch (err) {
|
|
175
192
|
if (err instanceof Error && err.message.includes("ENOENT")) {
|
|
@@ -203,7 +220,11 @@ export function writeBlob(artifactsRoot: string, input: {
|
|
|
203
220
|
// file from a failed content write) — which is the only unambiguously-safe
|
|
204
221
|
// case to clean up here.
|
|
205
222
|
if (!blobContentWritten) {
|
|
206
|
-
try {
|
|
223
|
+
try {
|
|
224
|
+
fs.rmSync(blobPath, { force: true });
|
|
225
|
+
} catch {
|
|
226
|
+
/* best-effort */
|
|
227
|
+
}
|
|
207
228
|
}
|
|
208
229
|
throw error;
|
|
209
230
|
}
|
|
@@ -211,7 +232,13 @@ export function writeBlob(artifactsRoot: string, input: {
|
|
|
211
232
|
// Issue 2 fix: resolve paths before writes and return cached values
|
|
212
233
|
const resolvedBlobPath = resolveRealContainedPath(artifactsRoot, blobPath);
|
|
213
234
|
const resolvedMetadataPath = resolveRealContainedPath(artifactsRoot, metadataPath);
|
|
214
|
-
return {
|
|
235
|
+
return {
|
|
236
|
+
hash,
|
|
237
|
+
algorithm,
|
|
238
|
+
blobPath: resolvedBlobPath,
|
|
239
|
+
metadataPath: resolvedMetadataPath,
|
|
240
|
+
sizeBytes: metadata.sizeBytes,
|
|
241
|
+
};
|
|
215
242
|
}
|
|
216
243
|
|
|
217
244
|
/**
|
package/src/state/contracts.ts
CHANGED
|
@@ -1,11 +1,26 @@
|
|
|
1
1
|
export const TEAM_RUN_STATUSES = ["queued", "planning", "running", "blocked", "completed", "failed", "cancelled"] as const;
|
|
2
|
-
export type TeamRunStatus = typeof TEAM_RUN_STATUSES[number];
|
|
2
|
+
export type TeamRunStatus = (typeof TEAM_RUN_STATUSES)[number];
|
|
3
3
|
|
|
4
|
-
export const TEAM_TASK_STATUSES = [
|
|
5
|
-
|
|
4
|
+
export const TEAM_TASK_STATUSES = [
|
|
5
|
+
"queued",
|
|
6
|
+
"running",
|
|
7
|
+
"waiting",
|
|
8
|
+
"completed",
|
|
9
|
+
"failed",
|
|
10
|
+
"cancelled",
|
|
11
|
+
"skipped",
|
|
12
|
+
"needs_attention",
|
|
13
|
+
] as const;
|
|
14
|
+
export type TeamTaskStatus = (typeof TEAM_TASK_STATUSES)[number];
|
|
6
15
|
|
|
7
16
|
export const TEAM_TERMINAL_RUN_STATUSES: ReadonlySet<TeamRunStatus> = new Set(["blocked", "completed", "failed", "cancelled"]);
|
|
8
|
-
export const TEAM_TERMINAL_TASK_STATUSES: ReadonlySet<TeamTaskStatus> = new Set([
|
|
17
|
+
export const TEAM_TERMINAL_TASK_STATUSES: ReadonlySet<TeamTaskStatus> = new Set([
|
|
18
|
+
"completed",
|
|
19
|
+
"failed",
|
|
20
|
+
"cancelled",
|
|
21
|
+
"skipped",
|
|
22
|
+
"needs_attention",
|
|
23
|
+
]);
|
|
9
24
|
|
|
10
25
|
export const TEAM_RUN_STATUS_TRANSITIONS: Readonly<Record<TeamRunStatus, readonly TeamRunStatus[]>> = {
|
|
11
26
|
queued: ["planning", "running", "cancelled", "failed"],
|
|
@@ -93,7 +108,7 @@ const TEAM_EVENT_TYPES = [
|
|
|
93
108
|
"dwf.failed",
|
|
94
109
|
"dwf.log",
|
|
95
110
|
] as const;
|
|
96
|
-
export type TeamEventType = typeof TEAM_EVENT_TYPES[number];
|
|
111
|
+
export type TeamEventType = (typeof TEAM_EVENT_TYPES)[number];
|
|
97
112
|
|
|
98
113
|
export const TEAM_WAKEABLE_EVENT_TYPES: ReadonlySet<TeamEventType> = new Set([
|
|
99
114
|
"run.blocked",
|
package/src/state/crew-init.ts
CHANGED
|
@@ -89,10 +89,7 @@ function parseRoot(start: string): string {
|
|
|
89
89
|
const rest = start.slice(2);
|
|
90
90
|
const firstSep = Math.max(rest.indexOf("\\"), rest.indexOf("/"));
|
|
91
91
|
if (firstSep === -1) return start;
|
|
92
|
-
const secondSep = Math.max(
|
|
93
|
-
rest.indexOf("\\", firstSep + 1),
|
|
94
|
-
rest.indexOf("/", firstSep + 1),
|
|
95
|
-
);
|
|
92
|
+
const secondSep = Math.max(rest.indexOf("\\", firstSep + 1), rest.indexOf("/", firstSep + 1));
|
|
96
93
|
if (secondSep === -1) return start;
|
|
97
94
|
// secondSep is an index into `rest`; add 2 to map back to `start`.
|
|
98
95
|
return start.slice(0, 2 + secondSep);
|
|
@@ -136,17 +133,11 @@ function safeJoin(...parts: string[]): string {
|
|
|
136
133
|
}
|
|
137
134
|
// Strip the leading separator(s) from the first part before joining, so
|
|
138
135
|
// the collapse regex doesn't re-collapse them.
|
|
139
|
-
const firstPartStripped =
|
|
140
|
-
sep === "\\"
|
|
141
|
-
? firstPart.replace(/^\\{1,2}/, "")
|
|
142
|
-
: firstPart.replace(/^\/+/, "");
|
|
136
|
+
const firstPartStripped = sep === "\\" ? firstPart.replace(/^\\{1,2}/, "") : firstPart.replace(/^\/+/, "");
|
|
143
137
|
const rest = filtered.slice(1);
|
|
144
138
|
const joined = [firstPartStripped, ...rest].filter(Boolean).join(sep);
|
|
145
139
|
// Collapse internal runs of the separator.
|
|
146
|
-
const collapsed = joined.replace(
|
|
147
|
-
new RegExp(`${sep === "\\" ? "\\\\" : "/"}{2,}`, "g"),
|
|
148
|
-
sep,
|
|
149
|
-
);
|
|
140
|
+
const collapsed = joined.replace(new RegExp(`${sep === "\\" ? "\\\\" : "/"}{2,}`, "g"), sep);
|
|
150
141
|
return leading + collapsed;
|
|
151
142
|
}
|
|
152
143
|
|
|
@@ -171,17 +162,9 @@ function safeResolve(p: string, pathDep?: typeof path): string {
|
|
|
171
162
|
return p;
|
|
172
163
|
}
|
|
173
164
|
|
|
174
|
-
function findProjectRoot(
|
|
175
|
-
start: string,
|
|
176
|
-
pathDep?: typeof path,
|
|
177
|
-
): string | undefined {
|
|
165
|
+
function findProjectRoot(start: string, pathDep?: typeof path): string | undefined {
|
|
178
166
|
const dirMarkers = [".git", ".hg", ".svn"];
|
|
179
|
-
const fileMarkers = [
|
|
180
|
-
"package.json",
|
|
181
|
-
"pyproject.toml",
|
|
182
|
-
"Cargo.toml",
|
|
183
|
-
"go.mod",
|
|
184
|
-
];
|
|
167
|
+
const fileMarkers = ["package.json", "pyproject.toml", "Cargo.toml", "go.mod"];
|
|
185
168
|
// Use `parseRoot` (inlined above) to avoid `path.parse` for the critical
|
|
186
169
|
// termination root — fixes the jiti namespace race in issue #28.
|
|
187
170
|
const root = parseRoot(start);
|
|
@@ -30,24 +30,15 @@ export interface RolloutEntry {
|
|
|
30
30
|
* Uses projectCrewRoot() to honour the `.pi/teams/` fallback for `.pi`-based
|
|
31
31
|
* projects (see issue #29).
|
|
32
32
|
*/
|
|
33
|
-
function getLedgerPath(
|
|
34
|
-
|
|
35
|
-
stateRoot?: string,
|
|
36
|
-
cwd?: string,
|
|
37
|
-
): string {
|
|
38
|
-
const base =
|
|
39
|
-
stateRoot ??
|
|
40
|
-
join(projectCrewRoot(cwd ?? process.cwd()), "state", "runs", runId);
|
|
33
|
+
function getLedgerPath(runId: string, stateRoot?: string, cwd?: string): string {
|
|
34
|
+
const base = stateRoot ?? join(projectCrewRoot(cwd ?? process.cwd()), "state", "runs", runId);
|
|
41
35
|
return `${base}/decision-ledger.jsonl`;
|
|
42
36
|
}
|
|
43
37
|
|
|
44
38
|
/**
|
|
45
39
|
* Compute coherence marks based on existing ledger entries.
|
|
46
40
|
*/
|
|
47
|
-
function computeCoherence(
|
|
48
|
-
entry: RolloutEntry,
|
|
49
|
-
ledger: RolloutEntry[],
|
|
50
|
-
): CoherenceMark {
|
|
41
|
+
function computeCoherence(entry: RolloutEntry, ledger: RolloutEntry[]): CoherenceMark {
|
|
51
42
|
if (ledger.length === 0) {
|
|
52
43
|
return {
|
|
53
44
|
matchesPrior: false,
|
|
@@ -59,22 +50,15 @@ function computeCoherence(
|
|
|
59
50
|
|
|
60
51
|
const previousEntry = ledger[ledger.length - 1];
|
|
61
52
|
const matchesPrior: boolean =
|
|
62
|
-
entry.decisionMark === previousEntry.decisionMark ||
|
|
63
|
-
Boolean(
|
|
64
|
-
entry.priorWinner &&
|
|
65
|
-
entry.topCandidates.includes(entry.priorWinner),
|
|
66
|
-
);
|
|
53
|
+
entry.decisionMark === previousEntry.decisionMark || Boolean(entry.priorWinner && entry.topCandidates.includes(entry.priorWinner));
|
|
67
54
|
|
|
68
55
|
// Check last 10 entries for recursive pattern
|
|
69
56
|
const recentEntries = ledger.slice(-10);
|
|
70
57
|
const recentDecisions = recentEntries.map((e) => e.decisionMark);
|
|
71
58
|
const currentDecision = entry.decisionMark;
|
|
72
59
|
|
|
73
|
-
const recursiveMatches = recentDecisions.filter(
|
|
74
|
-
|
|
75
|
-
).length;
|
|
76
|
-
const matchesRecursive =
|
|
77
|
-
recursiveMatches >= Math.ceil(recentDecisions.length / 2); // At least half match
|
|
60
|
+
const recursiveMatches = recentDecisions.filter((d) => d === currentDecision).length;
|
|
61
|
+
const matchesRecursive = recursiveMatches >= Math.ceil(recentDecisions.length / 2); // At least half match
|
|
78
62
|
|
|
79
63
|
const promotionAllowed = matchesPrior || matchesRecursive;
|
|
80
64
|
|
|
@@ -146,10 +130,7 @@ export function appendEntry(runId: string, entry: RolloutEntry): RolloutEntry {
|
|
|
146
130
|
// Append to JSONL file using atomic write to prevent corruption
|
|
147
131
|
// Use the already-loaded ledger content (no double-read)
|
|
148
132
|
const line = JSON.stringify(entryWithCoherence) + "\n";
|
|
149
|
-
const existingContent =
|
|
150
|
-
ledger.length > 0
|
|
151
|
-
? ledger.map((e) => JSON.stringify(e)).join("\n") + "\n"
|
|
152
|
-
: "";
|
|
133
|
+
const existingContent = ledger.length > 0 ? ledger.map((e) => JSON.stringify(e)).join("\n") + "\n" : "";
|
|
153
134
|
atomicWriteFile(ledgerPath, existingContent + line);
|
|
154
135
|
});
|
|
155
136
|
|
|
@@ -201,15 +182,7 @@ export function summarizeLedger(runId: string): string {
|
|
|
201
182
|
return "# Decision Ledger Summary\n\n*No entries recorded yet.*";
|
|
202
183
|
}
|
|
203
184
|
|
|
204
|
-
const lines: string[] = [
|
|
205
|
-
"# Decision Ledger Summary",
|
|
206
|
-
"",
|
|
207
|
-
`Run ID: ${runId}`,
|
|
208
|
-
`Total Entries: ${ledger.length}`,
|
|
209
|
-
"",
|
|
210
|
-
"## Entries",
|
|
211
|
-
"",
|
|
212
|
-
];
|
|
185
|
+
const lines: string[] = ["# Decision Ledger Summary", "", `Run ID: ${runId}`, `Total Entries: ${ledger.length}`, "", "## Entries", ""];
|
|
213
186
|
|
|
214
187
|
for (let i = 0; i < ledger.length; i++) {
|
|
215
188
|
const entry = ledger[i];
|
|
@@ -224,20 +197,12 @@ export function summarizeLedger(runId: string): string {
|
|
|
224
197
|
lines.push(`- **Prior Winner**: ${entry.priorWinner}`);
|
|
225
198
|
}
|
|
226
199
|
|
|
227
|
-
lines.push(
|
|
228
|
-
`- **Top Candidates**: ${entry.topCandidates.join(", ") || "(none)"}`,
|
|
229
|
-
);
|
|
200
|
+
lines.push(`- **Top Candidates**: ${entry.topCandidates.join(", ") || "(none)"}`);
|
|
230
201
|
lines.push("");
|
|
231
202
|
lines.push("#### Coherence");
|
|
232
|
-
lines.push(
|
|
233
|
-
|
|
234
|
-
);
|
|
235
|
-
lines.push(
|
|
236
|
-
`- **Matches Recursive**: ${entry.coherenceMark.matchesRecursive ? "✓" : "✗"}`,
|
|
237
|
-
);
|
|
238
|
-
lines.push(
|
|
239
|
-
`- **Promotion Allowed**: ${entry.coherenceMark.promotionAllowed ? "✓" : "✗"}`,
|
|
240
|
-
);
|
|
203
|
+
lines.push(`- **Matches Prior**: ${entry.coherenceMark.matchesPrior ? "✓" : "✗"}`);
|
|
204
|
+
lines.push(`- **Matches Recursive**: ${entry.coherenceMark.matchesRecursive ? "✓" : "✗"}`);
|
|
205
|
+
lines.push(`- **Promotion Allowed**: ${entry.coherenceMark.promotionAllowed ? "✓" : "✗"}`);
|
|
241
206
|
lines.push(`- **Reason**: ${entry.coherenceMark.reason}`);
|
|
242
207
|
lines.push("");
|
|
243
208
|
}
|
|
@@ -259,12 +224,8 @@ export function summarizeLedger(runId: string): string {
|
|
|
259
224
|
lines.push(`| Decay | ${decayCount} |`);
|
|
260
225
|
lines.push("");
|
|
261
226
|
|
|
262
|
-
const promotedCount = ledger.filter(
|
|
263
|
-
|
|
264
|
-
).length;
|
|
265
|
-
lines.push(
|
|
266
|
-
`**Promotion Rate**: ${promotedCount}/${ledger.length} (${((promotedCount / ledger.length) * 100).toFixed(1)}%)`,
|
|
267
|
-
);
|
|
227
|
+
const promotedCount = ledger.filter((e) => e.coherenceMark.promotionAllowed).length;
|
|
228
|
+
lines.push(`**Promotion Rate**: ${promotedCount}/${ledger.length} (${((promotedCount / ledger.length) * 100).toFixed(1)}%)`);
|
|
268
229
|
|
|
269
230
|
return lines.join("\n");
|
|
270
231
|
}
|
|
@@ -282,10 +243,7 @@ export function summarizeLedger(runId: string): string {
|
|
|
282
243
|
* FIX: Wrap read+write in file lock to prevent concurrent promotion/decay
|
|
283
244
|
* calls from losing entries.
|
|
284
245
|
*/
|
|
285
|
-
export function promoteCandidate(
|
|
286
|
-
runId: string,
|
|
287
|
-
candidate: string,
|
|
288
|
-
): RolloutEntry {
|
|
246
|
+
export function promoteCandidate(runId: string, candidate: string): RolloutEntry {
|
|
289
247
|
assertSafePathId("runId", runId);
|
|
290
248
|
assertSafePathId("candidate", candidate);
|
|
291
249
|
|
|
@@ -314,10 +272,7 @@ export function promoteCandidate(
|
|
|
314
272
|
};
|
|
315
273
|
|
|
316
274
|
// Compute coherence (empty ledger = no matches)
|
|
317
|
-
const coherenceMark = computeCoherence(
|
|
318
|
-
entryWithoutCoherence as RolloutEntry,
|
|
319
|
-
ledger,
|
|
320
|
-
);
|
|
275
|
+
const coherenceMark = computeCoherence(entryWithoutCoherence as RolloutEntry, ledger);
|
|
321
276
|
|
|
322
277
|
// Manual promotion always allows further promotion
|
|
323
278
|
coherenceMark.promotionAllowed = true;
|
|
@@ -330,10 +285,7 @@ export function promoteCandidate(
|
|
|
330
285
|
ledger.push(entry);
|
|
331
286
|
|
|
332
287
|
// Rewrite entire ledger atomically to preserve all entries
|
|
333
|
-
atomicWriteFile(
|
|
334
|
-
ledgerPath,
|
|
335
|
-
ledger.map((e) => JSON.stringify(e)).join("\n") + "\n",
|
|
336
|
-
);
|
|
288
|
+
atomicWriteFile(ledgerPath, ledger.map((e) => JSON.stringify(e)).join("\n") + "\n");
|
|
337
289
|
});
|
|
338
290
|
|
|
339
291
|
return entry!;
|
|
@@ -373,10 +325,7 @@ export function decayCandidate(runId: string, candidate: string): RolloutEntry {
|
|
|
373
325
|
};
|
|
374
326
|
|
|
375
327
|
// Compute coherence (empty ledger = no matches)
|
|
376
|
-
const coherenceMark = computeCoherence(
|
|
377
|
-
entryWithoutCoherence as RolloutEntry,
|
|
378
|
-
ledger,
|
|
379
|
-
);
|
|
328
|
+
const coherenceMark = computeCoherence(entryWithoutCoherence as RolloutEntry, ledger);
|
|
380
329
|
|
|
381
330
|
// Manual decay never allows promotion
|
|
382
331
|
coherenceMark.promotionAllowed = false;
|
|
@@ -389,10 +338,7 @@ export function decayCandidate(runId: string, candidate: string): RolloutEntry {
|
|
|
389
338
|
ledger.push(entry);
|
|
390
339
|
|
|
391
340
|
// Rewrite entire ledger to preserve all entries
|
|
392
|
-
atomicWriteFile(
|
|
393
|
-
ledgerPath,
|
|
394
|
-
ledger.map((e) => JSON.stringify(e)).join("\n") + "\n",
|
|
395
|
-
);
|
|
341
|
+
atomicWriteFile(ledgerPath, ledger.map((e) => JSON.stringify(e)).join("\n") + "\n");
|
|
396
342
|
});
|
|
397
343
|
|
|
398
344
|
return entry!;
|