pi-crew 0.9.50 → 0.9.52
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 +150 -0
- package/README.md +216 -20
- package/dist/build-meta.json +2222 -1761
- package/dist/index.mjs +18636 -17603
- package/docs/actions-reference.md +431 -1
- package/package.json +19 -9
- package/schema.json +41 -0
- package/scripts/check-bundle-size.mjs +38 -0
- package/scripts/test-runner.mjs +11 -1
- package/skills/distill-software/SKILL.md +24 -1
- package/dist/index.mjs.map +0 -7
- package/docs/FEATURE_INTAKE.md +0 -126
- package/docs/HARNESS.md +0 -87
- package/docs/HARNESS_BACKLOG.md +0 -89
- package/docs/PROJECT_REVIEW.md +0 -270
- package/docs/PROJECT_REVIEW_FIXES.md +0 -342
- package/docs/PROJECT_REVIEW_ROUND4.md +0 -156
- package/docs/PROJECT_REVIEW_ROUND5.md +0 -86
- package/docs/REVIEW-FINDINGS-2026-06 +0 -357
- package/docs/REVIEW-FINDINGS-2026-07-CORE.md +0 -139
- package/docs/REVIEW-FINDINGS-2026-07.md +0 -125
- package/docs/TEST_MATRIX.md +0 -51
- package/docs/archive/README.md +0 -91
- package/docs/archive/followup-review-round3-2026-05-12.md +0 -342
- package/docs/archive/followup-review-round4-2026-05-13.md +0 -107
- package/docs/archive/pi-crew-bugs.md +0 -961
- package/docs/archive/pi-crew-test-final.md +0 -120
- package/docs/archive/pi-crew-test-results.md +0 -260
- package/docs/archive/pi-crew-test-round2.md +0 -136
- package/docs/archive/pi-crew-test-round4.md +0 -100
- package/docs/archive/pi-crew-test-round5.md +0 -70
- package/docs/archive/pi-crew-test-round6.md +0 -110
- package/docs/archive/pi-crew-v0.5.10-audit-fix-plan.md +0 -60
- package/docs/archive/pi-crew-v0.5.11-audit-fix-plan.md +0 -92
- package/docs/archive/pi-crew-v0.5.12-audit-fix-plan.md +0 -76
- package/docs/archive/pi-crew-v0.5.13-audit-fix-plan.md +0 -75
- package/docs/archive/pi-crew-v0.5.14-audit-fix-plan.md +0 -75
- package/docs/archive/pi-crew-v0.5.16-audit-fix-plan.md +0 -35
- package/docs/archive/pi-crew-v0.5.17-audit-fix-plan.md +0 -80
- package/docs/archive/pi-crew-v0.5.5-audit-fix-plan.md +0 -133
- package/docs/archive/pi-crew-v0.5.9-audit-fix-plan.md +0 -88
- package/docs/archive/pi-mono-opportunities.md +0 -969
- package/docs/archive/pi-mono-review.md +0 -291
- package/docs/archive/pi-subagent4-comparison.md +0 -261
- package/docs/archive/pi-subagents3-deep-analysis.md +0 -508
- package/docs/bugs/bug-021-notification-badge-counter-misleading.md +0 -293
- package/docs/bugs/bug-023-chain-windows-path-resolution.md +0 -106
- package/docs/bugs/bug-quota-display-truncation.md +0 -223
- package/docs/bugs/cross-session-notification-leakage.md +0 -82
- package/docs/code-review-2026-05-11.md +0 -592
- package/docs/coding-agent-optimization.md +0 -268
- package/docs/comparison-pi-subagents-vs-pi-crew.md +0 -303
- package/docs/decisions/0001-durable-state.md +0 -41
- package/docs/decisions/0002-child-process-for-async.md +0 -42
- package/docs/decisions/0003-depth-guard.md +0 -36
- package/docs/decisions/0004-execfile-over-exec.md +0 -34
- package/docs/decisions/0005-no-parameter-properties.md +0 -49
- package/docs/decisions/0006-publish-bundled-esm.md +0 -63
- package/docs/decisions/0007-active-run-binary-index.md +0 -54
- package/docs/decisions/0008-child-pi-warm-pool.md +0 -61
- package/docs/decisions/2026-07-21-broker-phase4-default-on.md +0 -77
- package/docs/decisions/2026-07-21-broker-windows-perms.md +0 -91
- package/docs/decisions/2026-07-22-broker-phase4-gated-on.md +0 -99
- package/docs/decisions/2026-07-24-oidc-trusted-publishing.md +0 -112
- package/docs/decisions/README.md +0 -26
- package/docs/deep-review-report.md +0 -384
- package/docs/distillation/cybersecurity-patterns.md +0 -294
- package/docs/feature-analysis-subagent4.md +0 -305
- package/docs/fix-plan-disabletools-exit-null.md +0 -219
- package/docs/fixes/BATCH_A_H1_H2.md +0 -86
- package/docs/fixes/bug-006-foreground-cancel-concurrent.md +0 -78
- package/docs/fixes/bug-007-async-notifier-stale-ctx.md +0 -112
- package/docs/fixes/bug-008-child-process-silent-timeout.md +0 -100
- package/docs/fixes/bug-009-executor-yield-limit-needs-attention.md +0 -75
- package/docs/fixes/bug-010-child-process-api-key-filtered.md +0 -109
- package/docs/fixes/bug-011-spawn-pi-enoent.md +0 -92
- package/docs/fixes/bug-012-essential-env-stripped.md +0 -89
- package/docs/fixes/bug-013-background-runner-death.md +0 -84
- package/docs/fixes/bug-014-infinite-retry-loop-needs-attention.md +0 -82
- package/docs/fixes/bug-015-background-runner-sigterm.md +0 -65
- package/docs/fixes/bug-017-background-runner-session-shutdown.md +0 -66
- package/docs/fixes/bug-017-background-runner-sigkill-double-fork.md +0 -28
- package/docs/fixes/bug-018-child-pi-worker-stdin-hang.md +0 -61
- package/docs/fixes/bug-019-phantom-runs-temp-workspace.md +0 -52
- package/docs/fixes/bug-020-infinite-retry-loop-needs-attention.md +0 -47
- package/docs/fixes/v0.9.10/locks-fix-verify.md +0 -3
- package/docs/fixes/v0.9.10/smoke-test.md +0 -12
- package/docs/followup-plan-2026-05-12.md +0 -463
- package/docs/followup-review-2026-05-12.md +0 -297
- package/docs/goals.md +0 -59
- package/docs/implementation-plan-top3.md +0 -333
- package/docs/issue-29-analysis.md +0 -189
- package/docs/live-mailbox-runtime.md +0 -36
- package/docs/migration/atomic-write-v2-migration.md +0 -299
- package/docs/next-upgrade-roadmap.md +0 -808
- package/docs/oh-my-pi-research.md +0 -509
- package/docs/optimization-plan-2026-07.md +0 -366
- package/docs/optimization-plan.md +0 -644
- package/docs/patterns/command-agent-skill.md +0 -71
- package/docs/perf/baseline-2026-05.md +0 -113
- package/docs/perf/final-report-2026-05.md +0 -206
- package/docs/perf/optimization-plan-2026-07-verified.md +0 -396
- package/docs/perf/performance-audit-report-2026-07.md +0 -948
- package/docs/perf/performance-review-2026-07.md +0 -287
- package/docs/perf/sprint-1-report.md +0 -71
- package/docs/perf/sprint-2-report.md +0 -81
- package/docs/perf/sprint-2.5-report.md +0 -53
- package/docs/perf/sprint-3-report.md +0 -36
- package/docs/perf/sprint-4-report.md +0 -47
- package/docs/perf/sprint-5-report.md +0 -51
- package/docs/perf/sprint-6-report.md +0 -94
- package/docs/perf/sprint-7-report.md +0 -74
- package/docs/perf/upgrade-plan-2026-05.md +0 -147
- package/docs/performance-optimization-execution-plan.md +0 -520
- package/docs/phase4-triage.md +0 -58
- package/docs/pi-crew-investigation-report.md +0 -411
- package/docs/product/README.md +0 -31
- package/docs/product/platform.md +0 -27
- package/docs/product/runtime-safety.md +0 -37
- package/docs/product/team-run.md +0 -39
- package/docs/product/team-tool.md +0 -37
- package/docs/runtime-analysis-child-vs-live.md +0 -171
- package/docs/runtime-migration-in-process-analysis.md +0 -250
- package/docs/skills/REFERENCE.md +0 -138
- package/docs/stories/README.md +0 -32
- package/docs/stories/US-DEPS-major-upgrade.md +0 -62
- package/docs/stories/backlog.md +0 -36
- package/docs/superpowers/plans/2026-06-09-fallow-patterns-adoption.md +0 -1270
- package/docs/templates/decision.md +0 -27
- package/docs/templates/story.md +0 -44
- package/docs/templates/validation-report.md +0 -32
- package/docs/ui-optimization-plan.md +0 -447
- package/src/adapters/claude-adapter.ts +0 -23
- package/src/adapters/codex-adapter.ts +0 -21
- package/src/adapters/cursor-adapter.ts +0 -17
- package/src/adapters/export-util.ts +0 -143
- package/src/adapters/index.ts +0 -15
- package/src/adapters/registry.ts +0 -18
- package/src/adapters/types.ts +0 -23
- package/src/agents/agent-config.ts +0 -194
- package/src/agents/agent-search.ts +0 -98
- package/src/agents/agent-serializer.ts +0 -38
- package/src/agents/discover-agents.ts +0 -623
- package/src/benchmark/benchmark-runner.ts +0 -313
- package/src/benchmark/feedback-loop.ts +0 -73
- package/src/config/config.ts +0 -1269
- package/src/config/defaults.ts +0 -193
- package/src/config/drift-detector.ts +0 -290
- package/src/config/markers.ts +0 -330
- package/src/config/resilient-parser.ts +0 -117
- package/src/config/role-tools.ts +0 -118
- package/src/config/suggestions.ts +0 -75
- package/src/config/types.ts +0 -278
- package/src/errors.ts +0 -194
- package/src/extension/action-suggestions.ts +0 -117
- package/src/extension/async-notifier.ts +0 -179
- package/src/extension/autonomous-policy.ts +0 -209
- package/src/extension/command-completions.ts +0 -120
- package/src/extension/context-status-injection.ts +0 -185
- package/src/extension/crew-autocomplete.ts +0 -131
- package/src/extension/crew-cleanup.ts +0 -168
- package/src/extension/crew-input-router.ts +0 -93
- package/src/extension/crew-shortcuts.ts +0 -87
- package/src/extension/crew-vibes/cat-frames.ts +0 -18
- package/src/extension/crew-vibes/config.ts +0 -195
- package/src/extension/crew-vibes/figures.ts +0 -101
- package/src/extension/crew-vibes/font-detect.ts +0 -71
- package/src/extension/crew-vibes/footer.ts +0 -292
- package/src/extension/crew-vibes/index.ts +0 -432
- package/src/extension/crew-vibes/provider-usage.ts +0 -396
- package/src/extension/crew-vibes/render.ts +0 -206
- package/src/extension/crew-vibes/speed.ts +0 -286
- package/src/extension/cross-extension-rpc.ts +0 -350
- package/src/extension/help.ts +0 -62
- package/src/extension/import-index.ts +0 -74
- package/src/extension/knowledge-injection.ts +0 -402
- package/src/extension/management.ts +0 -571
- package/src/extension/message-renderers.ts +0 -108
- package/src/extension/notification-router.ts +0 -152
- package/src/extension/notification-sink.ts +0 -54
- package/src/extension/pi-api.ts +0 -56
- package/src/extension/plan-orchestrate.ts +0 -302
- package/src/extension/project-init.ts +0 -170
- package/src/extension/register.ts +0 -113
- package/src/extension/registration/artifact-cleanup.ts +0 -20
- package/src/extension/registration/command-registration.ts +0 -58
- package/src/extension/registration/command-utils.ts +0 -58
- package/src/extension/registration/commands.ts +0 -1228
- package/src/extension/registration/compaction-guard.ts +0 -333
- package/src/extension/registration/context-builder.ts +0 -135
- package/src/extension/registration/crash-recovery-cache.ts +0 -54
- package/src/extension/registration/foreground-run-controller.ts +0 -238
- package/src/extension/registration/hook-registration.ts +0 -119
- package/src/extension/registration/lazy-configurers.ts +0 -124
- package/src/extension/registration/lifecycle-handlers.ts +0 -923
- package/src/extension/registration/lifecycle.ts +0 -259
- package/src/extension/registration/observability.ts +0 -322
- package/src/extension/registration/registration-types.ts +0 -158
- package/src/extension/registration/runtime-cleanup.ts +0 -230
- package/src/extension/registration/subagent-helpers.ts +0 -125
- package/src/extension/registration/subagent-manager-setup.ts +0 -330
- package/src/extension/registration/subagent-tools.ts +0 -456
- package/src/extension/registration/team-tool.ts +0 -220
- package/src/extension/registration/tool-registration.ts +0 -50
- package/src/extension/registration/ui.ts +0 -172
- package/src/extension/registration/viewers.ts +0 -121
- package/src/extension/registration/wire-cross-extension.ts +0 -37
- package/src/extension/result-watcher.ts +0 -139
- package/src/extension/rpc-hmac.ts +0 -256
- package/src/extension/run-bundle-schema.ts +0 -104
- package/src/extension/run-export.ts +0 -97
- package/src/extension/run-import.ts +0 -168
- package/src/extension/run-index.ts +0 -131
- package/src/extension/run-maintenance.ts +0 -190
- package/src/extension/session-summary.ts +0 -18
- package/src/extension/team-manager-command.ts +0 -153
- package/src/extension/team-onboard.ts +0 -177
- package/src/extension/team-recommendation.ts +0 -288
- package/src/extension/team-tool/anchor.ts +0 -172
- package/src/extension/team-tool/api.ts +0 -1244
- package/src/extension/team-tool/auto-summarize.ts +0 -142
- package/src/extension/team-tool/cache-control.ts +0 -19
- package/src/extension/team-tool/cancel.ts +0 -337
- package/src/extension/team-tool/chain-dispatch.ts +0 -95
- package/src/extension/team-tool/chain-executor.ts +0 -379
- package/src/extension/team-tool/config-patch.ts +0 -50
- package/src/extension/team-tool/context.ts +0 -149
- package/src/extension/team-tool/destructive-gate.ts +0 -52
- package/src/extension/team-tool/doctor.ts +0 -431
- package/src/extension/team-tool/explain.ts +0 -282
- package/src/extension/team-tool/failure-patterns.ts +0 -118
- package/src/extension/team-tool/goal-wrap.ts +0 -330
- package/src/extension/team-tool/goal.ts +0 -556
- package/src/extension/team-tool/handle-schedule.ts +0 -332
- package/src/extension/team-tool/handle-settings.ts +0 -520
- package/src/extension/team-tool/health-monitor.ts +0 -481
- package/src/extension/team-tool/inspect.ts +0 -88
- package/src/extension/team-tool/intent-policy.ts +0 -45
- package/src/extension/team-tool/lifecycle-actions.ts +0 -615
- package/src/extension/team-tool/orchestrate.ts +0 -98
- package/src/extension/team-tool/parallel-dispatch.ts +0 -183
- package/src/extension/team-tool/plan.ts +0 -50
- package/src/extension/team-tool/respond.ts +0 -150
- package/src/extension/team-tool/run-not-found.ts +0 -49
- package/src/extension/team-tool/run.ts +0 -1035
- package/src/extension/team-tool/status.ts +0 -260
- package/src/extension/team-tool/workflow-manage.ts +0 -261
- package/src/extension/team-tool-types.ts +0 -26
- package/src/extension/team-tool.ts +0 -1116
- package/src/extension/tool-result.ts +0 -16
- package/src/extension/validate-resources.ts +0 -108
- package/src/hooks/registry.ts +0 -200
- package/src/hooks/types.ts +0 -69
- package/src/i18n.ts +0 -212
- package/src/observability/correlation.ts +0 -50
- package/src/observability/event-bus.ts +0 -86
- package/src/observability/event-to-metric.ts +0 -170
- package/src/observability/exporters/adapter.ts +0 -27
- package/src/observability/exporters/otlp-exporter.ts +0 -356
- package/src/observability/exporters/prometheus-exporter.ts +0 -54
- package/src/observability/metric-registry.ts +0 -100
- package/src/observability/metric-retention.ts +0 -64
- package/src/observability/metric-sink.ts +0 -99
- package/src/observability/metrics-primitives.ts +0 -219
- package/src/plugins/plugin-define.ts +0 -6
- package/src/plugins/plugin-registry.ts +0 -32
- package/src/plugins/plugins/index.ts +0 -3
- package/src/plugins/plugins/nextjs.ts +0 -19
- package/src/plugins/plugins/vite.ts +0 -10
- package/src/plugins/plugins/vitest.ts +0 -9
- package/src/prompt/prompt-runtime.ts +0 -380
- package/src/runtime/adaptive-plan.ts +0 -563
- package/src/runtime/agent-control.ts +0 -215
- package/src/runtime/agent-memory.ts +0 -81
- package/src/runtime/agent-observability.ts +0 -122
- package/src/runtime/anchor-manager.ts +0 -475
- package/src/runtime/async-marker.ts +0 -32
- package/src/runtime/async-runner.ts +0 -381
- package/src/runtime/attention-events.ts +0 -28
- package/src/runtime/auto-summarize.ts +0 -344
- package/src/runtime/background-runner.ts +0 -837
- package/src/runtime/batch-barrier.ts +0 -147
- package/src/runtime/broker-issuer.ts +0 -37
- package/src/runtime/cancellation-token.ts +0 -99
- package/src/runtime/cancellation.ts +0 -99
- package/src/runtime/capability-inventory.ts +0 -137
- package/src/runtime/chain-parser.ts +0 -237
- package/src/runtime/chain-runner.ts +0 -606
- package/src/runtime/checkpoint.ts +0 -291
- package/src/runtime/child-pi-constants.ts +0 -42
- package/src/runtime/child-pi-kill.ts +0 -180
- package/src/runtime/child-pi-pool.ts +0 -68
- package/src/runtime/child-pi-spawn.ts +0 -287
- package/src/runtime/child-pi-steering.ts +0 -128
- package/src/runtime/child-pi-streams.ts +0 -296
- package/src/runtime/child-pi-transcript.ts +0 -169
- package/src/runtime/child-pi.ts +0 -1105
- package/src/runtime/coalesce-tasks.ts +0 -268
- package/src/runtime/code-summary.ts +0 -292
- package/src/runtime/command-trace.ts +0 -105
- package/src/runtime/compact-pipeline.ts +0 -56
- package/src/runtime/compact-stages/ansi-strip-stage.ts +0 -25
- package/src/runtime/compact-stages/blank-collapse-stage.ts +0 -31
- package/src/runtime/compact-stages/deduplicate-stage.ts +0 -34
- package/src/runtime/compact-stages/head-snap-stage.ts +0 -57
- package/src/runtime/compact-stages/index.ts +0 -23
- package/src/runtime/compact-stages/tail-capture-stage.ts +0 -77
- package/src/runtime/compact-stages/truncation-stage.ts +0 -74
- package/src/runtime/compaction-summary.ts +0 -278
- package/src/runtime/completion-guard.ts +0 -207
- package/src/runtime/concurrency.ts +0 -58
- package/src/runtime/crash-classification.ts +0 -229
- package/src/runtime/crash-recovery.ts +0 -577
- package/src/runtime/crew-agent-records.ts +0 -593
- package/src/runtime/crew-agent-runtime.ts +0 -62
- package/src/runtime/crew-broker-child.ts +0 -88
- package/src/runtime/crew-broker-client.ts +0 -673
- package/src/runtime/crew-broker-tokens.ts +0 -84
- package/src/runtime/crew-broker.ts +0 -1276
- package/src/runtime/crew-hooks.ts +0 -227
- package/src/runtime/cross-extension-rpc.ts +0 -143
- package/src/runtime/custom-tools/irc-tool.ts +0 -282
- package/src/runtime/custom-tools/submit-result-tool.ts +0 -102
- package/src/runtime/deadletter.ts +0 -47
- package/src/runtime/delivery-coordinator.ts +0 -211
- package/src/runtime/delta-conflict.ts +0 -348
- package/src/runtime/deterministic-ast.ts +0 -161
- package/src/runtime/diagnostic-export.ts +0 -171
- package/src/runtime/direct-run.ts +0 -45
- package/src/runtime/dwf-state-store.ts +0 -102
- package/src/runtime/dynamic-workflow-context.ts +0 -1016
- package/src/runtime/dynamic-workflow-runner.ts +0 -310
- package/src/runtime/effectiveness.ts +0 -90
- package/src/runtime/errors/crew-errors.ts +0 -162
- package/src/runtime/event-stream-bridge.ts +0 -98
- package/src/runtime/foreground-control.ts +0 -193
- package/src/runtime/foreground-watchdog.ts +0 -132
- package/src/runtime/global-worker-cap.ts +0 -96
- package/src/runtime/goal-achievement.ts +0 -148
- package/src/runtime/goal-evaluator.ts +0 -361
- package/src/runtime/goal-loop-runner.ts +0 -828
- package/src/runtime/goal-state-store.ts +0 -219
- package/src/runtime/green-contract.ts +0 -55
- package/src/runtime/group-join.ts +0 -267
- package/src/runtime/handoff-manager.ts +0 -598
- package/src/runtime/heartbeat-gradient.ts +0 -36
- package/src/runtime/heartbeat-watcher.ts +0 -239
- package/src/runtime/hidden-handoff.ts +0 -407
- package/src/runtime/important-line-classifier.ts +0 -140
- package/src/runtime/intercom-bridge.ts +0 -187
- package/src/runtime/iteration-hooks.ts +0 -305
- package/src/runtime/live-agent-control.ts +0 -136
- package/src/runtime/live-agent-manager.ts +0 -704
- package/src/runtime/live-control-realtime.ts +0 -56
- package/src/runtime/live-extension-bridge.ts +0 -148
- package/src/runtime/live-irc.ts +0 -97
- package/src/runtime/live-session-health.ts +0 -108
- package/src/runtime/live-session-runtime.ts +0 -1139
- package/src/runtime/loop-gates.ts +0 -128
- package/src/runtime/manifest-cache.ts +0 -375
- package/src/runtime/mcp-proxy.ts +0 -105
- package/src/runtime/metric-parser.ts +0 -36
- package/src/runtime/model-fallback.ts +0 -409
- package/src/runtime/model-resolver.ts +0 -126
- package/src/runtime/model-scope.ts +0 -158
- package/src/runtime/orphan-worker-registry.ts +0 -465
- package/src/runtime/output-validator.ts +0 -202
- package/src/runtime/overflow-recovery.ts +0 -206
- package/src/runtime/parallel-research.ts +0 -68
- package/src/runtime/parallel-utils.ts +0 -160
- package/src/runtime/parent-guard.ts +0 -134
- package/src/runtime/path-overlap.ts +0 -150
- package/src/runtime/peer-dep.ts +0 -292
- package/src/runtime/per-write-validator.ts +0 -181
- package/src/runtime/phase-progress.ts +0 -217
- package/src/runtime/phase-tracker.ts +0 -385
- package/src/runtime/pi-args.ts +0 -655
- package/src/runtime/pi-json-output.ts +0 -170
- package/src/runtime/pi-spawn.ts +0 -273
- package/src/runtime/pipeline-runner.ts +0 -523
- package/src/runtime/plan-templates.ts +0 -201
- package/src/runtime/policy-engine.ts +0 -115
- package/src/runtime/post-checks.ts +0 -142
- package/src/runtime/post-exit-stdio-guard.ts +0 -104
- package/src/runtime/process-lifecycle.ts +0 -491
- package/src/runtime/process-status.ts +0 -154
- package/src/runtime/progress-event-coalescer.ts +0 -44
- package/src/runtime/progress-tracker.ts +0 -124
- package/src/runtime/prose-compressor.ts +0 -162
- package/src/runtime/recovery-recipes.ts +0 -203
- package/src/runtime/replace.ts +0 -570
- package/src/runtime/resilient-edit.ts +0 -153
- package/src/runtime/result-extractor.ts +0 -213
- package/src/runtime/retry-executor.ts +0 -100
- package/src/runtime/retry-runner.ts +0 -336
- package/src/runtime/role-permission.ts +0 -59
- package/src/runtime/run-coalesced-task-group.ts +0 -308
- package/src/runtime/run-drift.ts +0 -219
- package/src/runtime/run-tracker.ts +0 -116
- package/src/runtime/runtime-policy.ts +0 -29
- package/src/runtime/runtime-resolver.ts +0 -172
- package/src/runtime/runtime-warmup.ts +0 -160
- package/src/runtime/scheduler.ts +0 -353
- package/src/runtime/semaphore.ts +0 -141
- package/src/runtime/sensitive-paths.ts +0 -93
- package/src/runtime/session-resources.ts +0 -25
- package/src/runtime/session-snapshot.ts +0 -59
- package/src/runtime/session-usage.ts +0 -79
- package/src/runtime/settings-store.ts +0 -155
- package/src/runtime/sidechain-output.ts +0 -35
- package/src/runtime/single-agent-compose.ts +0 -84
- package/src/runtime/skill-effectiveness.ts +0 -524
- package/src/runtime/skill-instructions.ts +0 -391
- package/src/runtime/stale-reconciler.ts +0 -680
- package/src/runtime/stream-preview.ts +0 -184
- package/src/runtime/streaming-output.ts +0 -50
- package/src/runtime/subagent-manager.ts +0 -561
- package/src/runtime/subprocess-tool-registry.ts +0 -70
- package/src/runtime/supervisor-contact.ts +0 -64
- package/src/runtime/task-display.ts +0 -52
- package/src/runtime/task-graph-scheduler.ts +0 -227
- package/src/runtime/task-graph.ts +0 -290
- package/src/runtime/task-health.ts +0 -83
- package/src/runtime/task-id.ts +0 -161
- package/src/runtime/task-output-context.ts +0 -602
- package/src/runtime/task-packet.ts +0 -268
- package/src/runtime/task-quality.ts +0 -199
- package/src/runtime/task-runner/capabilities.ts +0 -78
- package/src/runtime/task-runner/context-retrieval.ts +0 -163
- package/src/runtime/task-runner/live-executor.ts +0 -227
- package/src/runtime/task-runner/output-splitter.ts +0 -152
- package/src/runtime/task-runner/progress.ts +0 -157
- package/src/runtime/task-runner/prompt-builder.ts +0 -278
- package/src/runtime/task-runner/prompt-pipeline.ts +0 -88
- package/src/runtime/task-runner/result-utils.ts +0 -16
- package/src/runtime/task-runner/retrieval-orchestrator.ts +0 -310
- package/src/runtime/task-runner/run-projection.ts +0 -125
- package/src/runtime/task-runner/state-helpers.ts +0 -176
- package/src/runtime/task-runner/tail-read.ts +0 -34
- package/src/runtime/task-runner.ts +0 -1420
- package/src/runtime/team-runner-artifacts.ts +0 -13
- package/src/runtime/team-runner.ts +0 -1993
- package/src/runtime/tool-output-pruner.ts +0 -333
- package/src/runtime/tool-progress.ts +0 -278
- package/src/runtime/usage-tracker.ts +0 -73
- package/src/runtime/verification-gates.ts +0 -571
- package/src/runtime/verification-integrity.ts +0 -105
- package/src/runtime/verification-worktree.ts +0 -186
- package/src/runtime/worker-heartbeat.ts +0 -25
- package/src/runtime/worker-startup.ts +0 -83
- package/src/runtime/workflow-state.ts +0 -195
- package/src/runtime/workspace-lock.ts +0 -443
- package/src/runtime/workspace-tree.ts +0 -312
- package/src/runtime/yield-handler.ts +0 -224
- package/src/runtime/zombie-scanner.ts +0 -298
- package/src/schema/config-schema.ts +0 -327
- package/src/schema/team-tool-schema.ts +0 -415
- package/src/schema/validation-types.ts +0 -159
- package/src/skills/discover-skills.ts +0 -197
- package/src/skills/skill-templates.ts +0 -456
- package/src/skills/validate.ts +0 -285
- package/src/state/active-run-registry.ts +0 -439
- package/src/state/artifact-store.ts +0 -176
- package/src/state/atomic-write.ts +0 -903
- package/src/state/blob-store.ts +0 -308
- package/src/state/contracts.ts +0 -158
- package/src/state/crew-init.ts +0 -269
- package/src/state/decision-ledger.ts +0 -345
- package/src/state/event-log-rotation.ts +0 -342
- package/src/state/event-log.ts +0 -1311
- package/src/state/event-reconstructor.ts +0 -229
- package/src/state/gitignore-manager.ts +0 -46
- package/src/state/health-store.ts +0 -79
- package/src/state/hook-instinct-bridge.ts +0 -94
- package/src/state/hook-integrations.ts +0 -51
- package/src/state/instinct-store.ts +0 -275
- package/src/state/jsonl-writer.ts +0 -107
- package/src/state/locks.ts +0 -562
- package/src/state/mailbox.ts +0 -916
- package/src/state/observation-store.ts +0 -176
- package/src/state/run-cache.ts +0 -193
- package/src/state/run-graph.ts +0 -183
- package/src/state/run-metrics.ts +0 -165
- package/src/state/schedule.ts +0 -166
- package/src/state/session-state-map.ts +0 -51
- package/src/state/state-store.ts +0 -958
- package/src/state/task-claims.ts +0 -61
- package/src/state/tiered-eval.ts +0 -480
- package/src/state/types-eval.ts +0 -58
- package/src/state/types.ts +0 -440
- package/src/state/usage.ts +0 -138
- package/src/state/worker-atomic-writer.ts +0 -194
- package/src/subagents/async-entry.ts +0 -1
- package/src/subagents/index.ts +0 -3
- package/src/subagents/live/control.ts +0 -1
- package/src/subagents/live/manager.ts +0 -1
- package/src/subagents/live/realtime.ts +0 -1
- package/src/subagents/live/session-runtime.ts +0 -1
- package/src/subagents/manager.ts +0 -1
- package/src/subagents/spawn.ts +0 -1
- package/src/teams/discover-teams.ts +0 -187
- package/src/teams/team-config.ts +0 -27
- package/src/teams/team-serializer.ts +0 -38
- package/src/tools/safe-bash-extension.ts +0 -54
- package/src/tools/safe-bash.ts +0 -505
- package/src/types/diff.d.ts +0 -18
- package/src/types/new-api-types.ts +0 -35
- package/src/ui/agent-management-overlay.ts +0 -160
- package/src/ui/card-colors.ts +0 -139
- package/src/ui/crew-footer.ts +0 -102
- package/src/ui/crew-select-list.ts +0 -111
- package/src/ui/dashboard-panes/agents-pane.ts +0 -164
- package/src/ui/dashboard-panes/cancellation-pane.ts +0 -66
- package/src/ui/dashboard-panes/capability-pane.ts +0 -77
- package/src/ui/dashboard-panes/health-pane.ts +0 -31
- package/src/ui/dashboard-panes/mailbox-pane.ts +0 -35
- package/src/ui/dashboard-panes/metrics-pane.ts +0 -37
- package/src/ui/dashboard-panes/progress-pane.ts +0 -38
- package/src/ui/dashboard-panes/transcript-pane.ts +0 -10
- package/src/ui/deploy-bundled-themes.ts +0 -71
- package/src/ui/dwf-phase-display.ts +0 -151
- package/src/ui/dynamic-border.ts +0 -35
- package/src/ui/heartbeat-aggregator.ts +0 -74
- package/src/ui/key-utils.ts +0 -42
- package/src/ui/keybinding-map.ts +0 -248
- package/src/ui/layout-primitives.ts +0 -106
- package/src/ui/live-conversation-overlay.ts +0 -183
- package/src/ui/live-duration.ts +0 -55
- package/src/ui/live-run-sidebar.ts +0 -301
- package/src/ui/loaders.ts +0 -176
- package/src/ui/mascot.ts +0 -436
- package/src/ui/overlays/agent-picker-overlay.ts +0 -65
- package/src/ui/overlays/confirm-overlay.ts +0 -60
- package/src/ui/overlays/help-overlay.ts +0 -177
- package/src/ui/overlays/mailbox-compose-overlay.ts +0 -181
- package/src/ui/overlays/mailbox-compose-preview.ts +0 -78
- package/src/ui/overlays/mailbox-detail-overlay.ts +0 -154
- package/src/ui/pi-ui-compat.ts +0 -68
- package/src/ui/powerbar-publisher.ts +0 -459
- package/src/ui/render-coalescer.ts +0 -101
- package/src/ui/render-diff.ts +0 -160
- package/src/ui/render-scheduler.ts +0 -224
- package/src/ui/run-action-dispatcher.ts +0 -182
- package/src/ui/run-dashboard.ts +0 -913
- package/src/ui/run-event-bus.ts +0 -381
- package/src/ui/run-snapshot-cache.ts +0 -1057
- package/src/ui/settings-overlay.ts +0 -1067
- package/src/ui/snapshot-types.ts +0 -90
- package/src/ui/spinner.ts +0 -17
- package/src/ui/status-colors.ts +0 -143
- package/src/ui/syntax-highlight.ts +0 -136
- package/src/ui/terminal-status.ts +0 -269
- package/src/ui/theme-adapter.ts +0 -271
- package/src/ui/theme-discovery.ts +0 -199
- package/src/ui/tool-progress-formatter.ts +0 -93
- package/src/ui/tool-render.ts +0 -453
- package/src/ui/tool-renderers/brief-mode.ts +0 -225
- package/src/ui/tool-renderers/index.ts +0 -726
- package/src/ui/transcript-cache.ts +0 -128
- package/src/ui/transcript-entries.ts +0 -256
- package/src/ui/transcript-viewer.ts +0 -424
- package/src/ui/widget/index.ts +0 -357
- package/src/ui/widget/widget-formatters.ts +0 -182
- package/src/ui/widget/widget-model.ts +0 -110
- package/src/ui/widget/widget-renderer.ts +0 -182
- package/src/ui/widget/widget-types.ts +0 -36
- package/src/utils/bm25-search.ts +0 -235
- package/src/utils/completion-dedupe.ts +0 -63
- package/src/utils/conflict-detect.ts +0 -668
- package/src/utils/env-allowlist.ts +0 -30
- package/src/utils/env-filter.ts +0 -155
- package/src/utils/file-coalescer.ts +0 -98
- package/src/utils/fingerprint.ts +0 -180
- package/src/utils/frontmatter.ts +0 -70
- package/src/utils/fs-watch.ts +0 -37
- package/src/utils/gh-protocol.ts +0 -556
- package/src/utils/git.ts +0 -260
- package/src/utils/guards.ts +0 -107
- package/src/utils/ids.ts +0 -24
- package/src/utils/incremental-reader.ts +0 -220
- package/src/utils/internal-error.ts +0 -10
- package/src/utils/names.ts +0 -36
- package/src/utils/ndjson.ts +0 -115
- package/src/utils/paths.ts +0 -227
- package/src/utils/project-detector.ts +0 -160
- package/src/utils/redaction.ts +0 -347
- package/src/utils/resolve-shell.ts +0 -36
- package/src/utils/run-watcher-registry.ts +0 -152
- package/src/utils/safe-paths.ts +0 -383
- package/src/utils/scan-cache.ts +0 -144
- package/src/utils/session-utils.ts +0 -110
- package/src/utils/sleep.ts +0 -54
- package/src/utils/socket-path.ts +0 -127
- package/src/utils/sse-parser.ts +0 -129
- package/src/utils/task-name-generator.ts +0 -337
- package/src/utils/timings.ts +0 -33
- package/src/utils/token-counter.ts +0 -200
- package/src/utils/visual.ts +0 -207
- package/src/workflows/cost-estimator.ts +0 -34
- package/src/workflows/discover-workflows.ts +0 -290
- package/src/workflows/intermediate-store.ts +0 -166
- package/src/workflows/preflight-validator.ts +0 -168
- package/src/workflows/topology-analyzer.ts +0 -203
- package/src/workflows/validate-workflow.ts +0 -50
- package/src/workflows/workflow-config.ts +0 -70
- package/src/workflows/workflow-serializer.ts +0 -32
- package/src/worktree/branch-freshness.ts +0 -112
- package/src/worktree/cleanup.ts +0 -341
- package/src/worktree/worktree-manager.ts +0 -1200
package/src/state/locks.ts
DELETED
|
@@ -1,562 +0,0 @@
|
|
|
1
|
-
import { AsyncLocalStorage } from "node:async_hooks";
|
|
2
|
-
import { randomUUID, timingSafeEqual } from "node:crypto";
|
|
3
|
-
import * as fs from "node:fs";
|
|
4
|
-
import * as path from "node:path";
|
|
5
|
-
import { DEFAULT_LOCKS } from "../config/defaults.ts";
|
|
6
|
-
import { logInternalError } from "../utils/internal-error.ts";
|
|
7
|
-
import { sleepSync } from "../utils/sleep.ts";
|
|
8
|
-
import { isSymlinkSafePath } from "./atomic-write.ts";
|
|
9
|
-
import type { TeamRunManifest } from "./types.ts";
|
|
10
|
-
|
|
11
|
-
export interface RunLockOptions {
|
|
12
|
-
staleMs?: number;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
const DEFAULT_STALE_MS = DEFAULT_LOCKS.staleMs;
|
|
16
|
-
|
|
17
|
-
function lockPath(manifest: TeamRunManifest): string {
|
|
18
|
-
return path.join(manifest.stateRoot, "run.lock");
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
function parseCreatedAtFromLock(raw: string): number | undefined {
|
|
22
|
-
try {
|
|
23
|
-
const payload = JSON.parse(raw) as unknown;
|
|
24
|
-
if (!payload || typeof payload !== "object" || Array.isArray(payload)) return undefined;
|
|
25
|
-
const candidate = payload as { createdAt?: unknown };
|
|
26
|
-
if (typeof candidate.createdAt !== "string") return undefined;
|
|
27
|
-
const parsed = Date.parse(candidate.createdAt);
|
|
28
|
-
return Number.isNaN(parsed) ? undefined : parsed;
|
|
29
|
-
} catch {
|
|
30
|
-
return undefined;
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
function isLockStale(filePath: string, staleMs: number): boolean {
|
|
35
|
-
try {
|
|
36
|
-
const stat = fs.statSync(filePath);
|
|
37
|
-
let createdAt = parseCreatedAtFromLock(fs.readFileSync(filePath, "utf-8"));
|
|
38
|
-
if (createdAt === undefined) createdAt = stat.mtimeMs;
|
|
39
|
-
return Date.now() - createdAt > staleMs;
|
|
40
|
-
} catch {
|
|
41
|
-
return false;
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
function isLockHolderAlive(filePath: string): boolean {
|
|
46
|
-
try {
|
|
47
|
-
const raw = fs.readFileSync(filePath, "utf-8");
|
|
48
|
-
const parsed = JSON.parse(raw) as { pid?: unknown };
|
|
49
|
-
const pid = typeof parsed.pid === "number" ? parsed.pid : undefined;
|
|
50
|
-
if (pid === undefined) return true; // Unknown holder — assume alive to be safe
|
|
51
|
-
try {
|
|
52
|
-
process.kill(pid, 0);
|
|
53
|
-
return true; // Signal 0 succeeded — process is alive
|
|
54
|
-
} catch (error) {
|
|
55
|
-
const code = (error as NodeJS.ErrnoException).code;
|
|
56
|
-
// EPERM: process exists but we don't have permission to signal it.
|
|
57
|
-
// Since we cannot verify liveness, treat the holder as potentially
|
|
58
|
-
// stale so the lock can be stolen rather than blocking indefinitely.
|
|
59
|
-
// This is an acceptable trade-off: EPERM requires elevated privileges,
|
|
60
|
-
// and blocking indefinitely would be worse. The risk is low.
|
|
61
|
-
// Other errors (ESRCH — process doesn't exist) also mean holder is dead.
|
|
62
|
-
return false;
|
|
63
|
-
}
|
|
64
|
-
} catch {
|
|
65
|
-
return true; // Can't read — assume alive to be safe
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
/**
|
|
70
|
-
* Round 26 (BUG 1): read the lock file ONCE and evaluate staleness + holder
|
|
71
|
-
* liveness from that single snapshot.
|
|
72
|
-
*
|
|
73
|
-
* Previously `acquireLockWithRetry` called `isLockStale()` and
|
|
74
|
-
* `isLockHolderAlive()` separately, each performing its own `readFileSync`.
|
|
75
|
-
* Between those two reads the lock could transition stale→fresh (old holder
|
|
76
|
-
* released, new holder acquired): isLockStale saw the OLD createdAt → stale,
|
|
77
|
-
* isLockHolderAlive saw the NEW pid → alive, yielding `!stale && alive` =
|
|
78
|
-
* false → we forcibly rm the NEW holder's freshly-acquired lock and take it
|
|
79
|
-
* ourselves → BOTH in the critical section. Reading once closes the window.
|
|
80
|
-
*
|
|
81
|
-
* Returns `{ canSteal: true }` if the lock is stale OR the holder is dead
|
|
82
|
-
* (safe to forcibly remove); `{ canSteal: false }` if it is fresh AND held by
|
|
83
|
-
* a live process (must keep waiting).
|
|
84
|
-
*
|
|
85
|
-
* ## EPERM Handling (Accepted Risk)
|
|
86
|
-
*
|
|
87
|
-
* When `process.kill(pid, 0)` returns EPERM, it means the holder process
|
|
88
|
-
* EXISTS but we lack permission to signal it. We treat this as "not alive"
|
|
89
|
-
* (stealable) because:
|
|
90
|
-
*
|
|
91
|
-
* 1. **Blocking indefinitely is worse** — on shared systems, a holder running
|
|
92
|
-
* under a different user/permission context would block us forever.
|
|
93
|
-
* 2. **EPERM requires elevated privileges** — on single-user workstations
|
|
94
|
-
* (the typical pi-crew environment), EPERM is rare.
|
|
95
|
-
* 3. **Defense in depth** — the lock staleness check provides a secondary
|
|
96
|
-
* safeguard; we only steal if the lock is also stale.
|
|
97
|
-
*
|
|
98
|
-
* **Trade-off:** On multi-user systems, this could theoretically allow two
|
|
99
|
-
* processes in the critical section simultaneously if: (a) holder is alive
|
|
100
|
-
* under different permissions, AND (b) our steal attempt races with holder's
|
|
101
|
-
* release. The practical risk is low because:
|
|
102
|
-
* - Lock holders typically complete quickly
|
|
103
|
-
* - Stale timeout provides a safety window
|
|
104
|
-
* - Critical sections are designed to tolerate rare races
|
|
105
|
-
*
|
|
106
|
-
* See also: SECURITY-ISSUES.md SEC-008 for documented acceptance.
|
|
107
|
-
*/
|
|
108
|
-
function readLockSnapshot(filePath: string, staleMs: number, options?: { treatOwnPidAsStealable?: boolean }): { canSteal: boolean } {
|
|
109
|
-
const treatOwnPidAsStealable = options?.treatOwnPidAsStealable === true;
|
|
110
|
-
let stat: fs.Stats | undefined;
|
|
111
|
-
let raw: string | undefined;
|
|
112
|
-
try {
|
|
113
|
-
stat = fs.statSync(filePath);
|
|
114
|
-
raw = fs.readFileSync(filePath, "utf-8");
|
|
115
|
-
} catch (error) {
|
|
116
|
-
// FIX (CI flake): when the lock file vanishes between writeLockFile's
|
|
117
|
-
// EEXIST and our read here, the holder has released — return canSteal:true
|
|
118
|
-
// so the acquire loop retries the create instead of throwing a false
|
|
119
|
-
// "locked" error.
|
|
120
|
-
const code = (error as NodeJS.ErrnoException).code;
|
|
121
|
-
if (code === "ENOENT") {
|
|
122
|
-
return { canSteal: true };
|
|
123
|
-
}
|
|
124
|
-
// Transient I/O error — be conservative (don't steal, retry the read on
|
|
125
|
-
// next attempt).
|
|
126
|
-
return { canSteal: false };
|
|
127
|
-
}
|
|
128
|
-
// Staleness from a single snapshot.
|
|
129
|
-
let createdAt = parseCreatedAtFromLock(raw);
|
|
130
|
-
if (createdAt === undefined) createdAt = stat.mtimeMs;
|
|
131
|
-
const isStale = Date.now() - createdAt > staleMs;
|
|
132
|
-
let holderPid: number | undefined;
|
|
133
|
-
let isAlive = true;
|
|
134
|
-
try {
|
|
135
|
-
const parsed = JSON.parse(raw) as { pid?: unknown };
|
|
136
|
-
holderPid = typeof parsed.pid === "number" ? parsed.pid : undefined;
|
|
137
|
-
} catch {
|
|
138
|
-
/* malformed payload — keep isAlive=true */
|
|
139
|
-
}
|
|
140
|
-
if (holderPid !== undefined) {
|
|
141
|
-
try {
|
|
142
|
-
process.kill(holderPid, 0);
|
|
143
|
-
isAlive = true;
|
|
144
|
-
} catch (error) {
|
|
145
|
-
const code = (error as NodeJS.ErrnoException).code;
|
|
146
|
-
// EPERM/ESRCH → treat as not-alive (stealable), see isLockHolderAlive.
|
|
147
|
-
isAlive = false;
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
// Steal if stale OR holder dead. Optionally steal if holder is our own pid
|
|
151
|
-
// (used by withRunLock* which is single-process — a fresh lock with our pid
|
|
152
|
-
// between acquisitions is just a leftover from a previous call that
|
|
153
|
-
// releaseOwnLock didn't get to delete yet; safe to steal).
|
|
154
|
-
const isOurOwnHolder = holderPid === process.pid;
|
|
155
|
-
return {
|
|
156
|
-
canSteal: isStale || !isAlive || (treatOwnPidAsStealable && isOurOwnHolder),
|
|
157
|
-
};
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
/**
|
|
161
|
-
* Lock file kinds. Discriminator written to the lock file payload so that:
|
|
162
|
-
* - Debugging tools (e.g. a future `pi-crew locks` command) can identify
|
|
163
|
-
* what a lock is protecting.
|
|
164
|
-
* - Cross-kind ambiguity is prevented if two locks somehow resolve to the
|
|
165
|
-
* same path (defense in depth).
|
|
166
|
-
* - Forward compat: new lock types can be added without changing the
|
|
167
|
-
* on-disk format (the `kind` field is the only discriminator).
|
|
168
|
-
*/
|
|
169
|
-
export type LockKind = "run" | "file";
|
|
170
|
-
|
|
171
|
-
function writeLockFile(filePath: string, token: string, kind: LockKind = "file"): void {
|
|
172
|
-
// Reject a pre-existing symlink at the lock path before O_CREAT|O_EXCL.
|
|
173
|
-
// O_EXCL fails with EEXIST if the path exists (including as a symlink),
|
|
174
|
-
// but the failure mode is confusing — an explicit lstatSync gives a
|
|
175
|
-
// clean, distinguishable error instead.
|
|
176
|
-
try {
|
|
177
|
-
const stat = fs.lstatSync(filePath);
|
|
178
|
-
if (stat.isSymbolicLink()) {
|
|
179
|
-
throw new Error(`Refusing to create lock file over symlink: ${filePath}`);
|
|
180
|
-
}
|
|
181
|
-
} catch (error) {
|
|
182
|
-
const code = (error as NodeJS.ErrnoException).code;
|
|
183
|
-
// ENOENT means the path doesn't exist yet — that's fine, proceed.
|
|
184
|
-
// Anything else (EACCES, EPERM, etc.) should surface immediately.
|
|
185
|
-
if (code !== "ENOENT") throw error;
|
|
186
|
-
}
|
|
187
|
-
// Ensure parent directory exists (may have been cleaned up by a concurrent process)
|
|
188
|
-
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
189
|
-
const fd = fs.openSync(filePath, fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_EXCL, 0o600);
|
|
190
|
-
try {
|
|
191
|
-
fs.writeSync(
|
|
192
|
-
fd,
|
|
193
|
-
JSON.stringify({
|
|
194
|
-
kind,
|
|
195
|
-
pid: process.pid,
|
|
196
|
-
createdAt: new Date().toISOString(),
|
|
197
|
-
token,
|
|
198
|
-
}),
|
|
199
|
-
);
|
|
200
|
-
} finally {
|
|
201
|
-
fs.closeSync(fd);
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
/**
|
|
206
|
-
* Read the token stored in a lock file. Returns undefined if the file
|
|
207
|
-
* cannot be read or parsed.
|
|
208
|
-
*/
|
|
209
|
-
function readLockToken(filePath: string): string | undefined {
|
|
210
|
-
try {
|
|
211
|
-
// Refuse to read a symlink — prevents reading a target an attacker placed
|
|
212
|
-
const stat = fs.lstatSync(filePath);
|
|
213
|
-
if (stat.isSymbolicLink()) return undefined;
|
|
214
|
-
const raw = fs.readFileSync(filePath, "utf-8");
|
|
215
|
-
const parsed = JSON.parse(raw) as { token?: unknown };
|
|
216
|
-
return typeof parsed.token === "string" ? parsed.token : undefined;
|
|
217
|
-
} catch {
|
|
218
|
-
return undefined;
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
/**
|
|
223
|
-
* Release a lock file, but ONLY if the stored token matches. This prevents
|
|
224
|
-
* the "losing contender wipes winner's lock" race that occurs when:
|
|
225
|
-
* 1. Process A acquires lock with token T_A
|
|
226
|
-
* 2. Process B times out waiting, steals the lock (overwriting with T_B)
|
|
227
|
-
* 3. Process A finishes, tries to release — would otherwise rm Process B's lock
|
|
228
|
-
*
|
|
229
|
-
* With token matching, A's release is a no-op for B's lock.
|
|
230
|
-
*/
|
|
231
|
-
function timingSafeTokenMatch(a: string, b: string): boolean {
|
|
232
|
-
// Early length check prevents timing side-channel that leaks length info.
|
|
233
|
-
// Without this, timingSafeEqual compares the full padded length, revealing
|
|
234
|
-
// that the strings have different lengths via the zero-padding.
|
|
235
|
-
if (a.length !== b.length) return false;
|
|
236
|
-
const bufA = Buffer.from(a);
|
|
237
|
-
const bufB = Buffer.from(b);
|
|
238
|
-
return timingSafeEqual(bufA, bufB);
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
/**
|
|
242
|
-
* Release the lock we (this process) just acquired. Unlike releaseLock, this
|
|
243
|
-
* unconditionally removes the file because we know we created it in the same
|
|
244
|
-
* withRunLock* call — token matching is only needed for multi-process scenarios
|
|
245
|
-
* (where a different holder's token must not be deleted). Within the same process
|
|
246
|
-
* the new acquire uses a fresh randomUUID token, so token matching would falsely
|
|
247
|
-
* fail and leak the file across releases.
|
|
248
|
-
*
|
|
249
|
-
* Symlink guard is preserved: if a symlink appeared since our writeLockFile, we
|
|
250
|
-
* don't rm it (defense against attacker-planted symlinks).
|
|
251
|
-
*/
|
|
252
|
-
export function releaseOwnLock(filePath: string, _token: string): void {
|
|
253
|
-
try {
|
|
254
|
-
const stat = fs.lstatSync(filePath);
|
|
255
|
-
if (stat.isSymbolicLink()) return;
|
|
256
|
-
} catch {
|
|
257
|
-
/* ENOENT is fine */
|
|
258
|
-
}
|
|
259
|
-
try {
|
|
260
|
-
fs.rmSync(filePath, { force: true });
|
|
261
|
-
} catch (error) {
|
|
262
|
-
const code = (error as NodeJS.ErrnoException).code;
|
|
263
|
-
if (code !== "ENOENT") {
|
|
264
|
-
logInternalError("lock-release-own", error, filePath, "warn");
|
|
265
|
-
}
|
|
266
|
-
}
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
function releaseLock(filePath: string, token: string): void {
|
|
270
|
-
// FIX: Do not delete a symlink — it may have been planted by an attacker
|
|
271
|
-
// after our lock was released. A legitimate lock file should never be a
|
|
272
|
-
// symlink since writeLockFile uses O_CREAT|O_EXCL which fails on symlinks.
|
|
273
|
-
let isSymlink = false;
|
|
274
|
-
try {
|
|
275
|
-
isSymlink = fs.lstatSync(filePath).isSymbolicLink();
|
|
276
|
-
} catch {
|
|
277
|
-
/* file doesn't exist — that's fine, we'll handle ENOENT below */
|
|
278
|
-
}
|
|
279
|
-
if (isSymlink) return;
|
|
280
|
-
|
|
281
|
-
const stored = readLockToken(filePath);
|
|
282
|
-
if (stored === undefined || timingSafeTokenMatch(stored, token)) {
|
|
283
|
-
try {
|
|
284
|
-
fs.rmSync(filePath, { force: true });
|
|
285
|
-
} catch (error) {
|
|
286
|
-
// FIX: Only ignore ENOENT (lock already gone). Other errors (EACCES,
|
|
287
|
-
// EPERM, EBUSY) indicate a real problem and should be surfaced.
|
|
288
|
-
const code = (error as NodeJS.ErrnoException).code;
|
|
289
|
-
if (code !== "ENOENT") throw error;
|
|
290
|
-
// Lock already gone — that's fine, we wanted to release it anyway.
|
|
291
|
-
}
|
|
292
|
-
}
|
|
293
|
-
// If the stored token does not match, our lock has been stolen
|
|
294
|
-
// (probably stale and overtaken). Do not touch it — the new holder owns it.
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
function acquireLockWithRetry(filePath: string, staleMs: number, kind: LockKind = "file"): string {
|
|
298
|
-
let attempt = 0;
|
|
299
|
-
const deadline = Date.now() + staleMs * 2;
|
|
300
|
-
while (true) {
|
|
301
|
-
const token = randomUUID();
|
|
302
|
-
try {
|
|
303
|
-
writeLockFile(filePath, token, kind);
|
|
304
|
-
return token;
|
|
305
|
-
} catch (error) {
|
|
306
|
-
const code = (error as NodeJS.ErrnoException).code;
|
|
307
|
-
if (code !== "EEXIST") throw error;
|
|
308
|
-
if (Date.now() > deadline) {
|
|
309
|
-
throw new Error(`Run '${path.basename(filePath)}' is locked by another operation.`);
|
|
310
|
-
}
|
|
311
|
-
// Round 26 (BUG 1): single-snapshot read closes the TOCTOU window between
|
|
312
|
-
// separate stale + alive reads (which could race stale→fresh).
|
|
313
|
-
// treatOwnPidAsStealable=false (this is the sync file-lock path used
|
|
314
|
-
// by withFileLockSync — never steal a fresh lock even if it has our pid).
|
|
315
|
-
const { canSteal } = readLockSnapshot(filePath, staleMs, { treatOwnPidAsStealable: false });
|
|
316
|
-
if (!canSteal) {
|
|
317
|
-
throw new Error(`Run '${path.basename(filePath)}' is locked by another operation.`);
|
|
318
|
-
}
|
|
319
|
-
// Stale or dead holder — forcibly remove the lock.
|
|
320
|
-
try {
|
|
321
|
-
fs.rmSync(filePath, { force: true });
|
|
322
|
-
} catch {
|
|
323
|
-
/* race — let loop retry */
|
|
324
|
-
}
|
|
325
|
-
sleepSync(Math.min(250, 25 * 2 ** attempt));
|
|
326
|
-
attempt++;
|
|
327
|
-
}
|
|
328
|
-
}
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
function sleep(ms: number): Promise<void> {
|
|
332
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
async function acquireLockWithRetryAsync(filePath: string, staleMs: number, kind: LockKind = "file"): Promise<string> {
|
|
336
|
-
let attempt = 0;
|
|
337
|
-
const deadline = Date.now() + staleMs * 2;
|
|
338
|
-
while (true) {
|
|
339
|
-
const token = randomUUID();
|
|
340
|
-
try {
|
|
341
|
-
writeLockFile(filePath, token, kind);
|
|
342
|
-
return token;
|
|
343
|
-
} catch (error) {
|
|
344
|
-
const code = (error as NodeJS.ErrnoException).code;
|
|
345
|
-
if (code !== "EEXIST") throw error;
|
|
346
|
-
if (Date.now() > deadline) {
|
|
347
|
-
throw new Error(`Run '${path.basename(filePath)}' is locked by another operation.`);
|
|
348
|
-
}
|
|
349
|
-
// Round 26 (BUG 1): single-snapshot read (see sync variant).
|
|
350
|
-
// FIX (CI flake): treatOwnPidAsStealable=true for the run-lock async path
|
|
351
|
-
// (withRunLock). Between acquisitions in parallel-research scaffold mode,
|
|
352
|
-
// the previous releaseOwnLock leaves a microsecond window where the
|
|
353
|
-
// file still exists with our own pid. Stealing it avoids spurious 'locked'
|
|
354
|
-
// errors. The sync file-lock path (withFileLockSync) above uses false to
|
|
355
|
-
// preserve the multi-process safety guarantee.
|
|
356
|
-
const { canSteal } = readLockSnapshot(filePath, staleMs, { treatOwnPidAsStealable: true });
|
|
357
|
-
if (!canSteal) {
|
|
358
|
-
throw new Error(`Run '${path.basename(filePath)}' is locked by another operation.`);
|
|
359
|
-
}
|
|
360
|
-
// Stale or dead holder — forcibly remove the lock.
|
|
361
|
-
try {
|
|
362
|
-
fs.rmSync(filePath, { force: true });
|
|
363
|
-
} catch {
|
|
364
|
-
/* race — let loop retry */
|
|
365
|
-
}
|
|
366
|
-
const delay = Math.min(250, 25 * 2 ** attempt);
|
|
367
|
-
await sleep(delay);
|
|
368
|
-
attempt++;
|
|
369
|
-
}
|
|
370
|
-
}
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
/**
|
|
374
|
-
* General-purpose file lock for arbitrary file paths.
|
|
375
|
-
* Uses the same O_EXCL atomic create strategy as run locks.
|
|
376
|
-
*/
|
|
377
|
-
export function withFileLockSync<T>(filePath: string, fn: () => T, options: RunLockOptions = {}): T {
|
|
378
|
-
// FIX: Use a separate .lock sidecar so the lock file doesn't collide with
|
|
379
|
-
// the file being protected. Previously withFileLockSync used the file path
|
|
380
|
-
// itself as the lock, which meant any operation on the same file (read,
|
|
381
|
-
// append, or even the lock acquisition itself) would race with the lock.
|
|
382
|
-
const lockFile = `${filePath}.lock`;
|
|
383
|
-
const staleMs = options.staleMs ?? DEFAULT_STALE_MS;
|
|
384
|
-
// FIX (Round 29): re-entrance guard — mirrors withRunLockSync below.
|
|
385
|
-
// When the same call stack already holds the file lock (e.g.
|
|
386
|
-
// registerWorker -> cleanupOrphanWorkers -> readRegistry), the second
|
|
387
|
-
// acquisition would otherwise read its own freshly-written lock file
|
|
388
|
-
// (same pid, fresh createdAt), fail the steal check, and deadlock for
|
|
389
|
-
// the full staleMs window. Strace-confirmed in
|
|
390
|
-
// .github/issues/pre-existing-2026-06-10/04-orphan-worker-registry-tests.md:75-86.
|
|
391
|
-
const existingToken = fileLockHeldByUs.get(lockFile);
|
|
392
|
-
if (existingToken) {
|
|
393
|
-
return fn();
|
|
394
|
-
}
|
|
395
|
-
// FIX: Validate the parent directory is not a symlink BEFORE calling mkdirSync.
|
|
396
|
-
// Between mkdir and lock acquisition, an attacker could plant a symlink.
|
|
397
|
-
if (!isSymlinkSafePath(path.dirname(lockFile))) throw new Error("Refusing: parent of lock directory is a symlink");
|
|
398
|
-
fs.mkdirSync(path.dirname(lockFile), { recursive: true });
|
|
399
|
-
// Round 26 (BUG 2): REMOVED the pre-acquisition target-file-existence check.
|
|
400
|
-
// It was racy — between statSync(target) and acquire, a concurrent process
|
|
401
|
-
// could acquire the lock to CREATE the target, and we'd delete its active
|
|
402
|
-
// lock. It was also actively wrong for callers that pass a path already
|
|
403
|
-
// ending in `.lock` (config.ts: the checked "target" never exists, so the
|
|
404
|
-
// cleanup ALWAYS fired, deleting a fresh concurrent holder's lock). Genuine
|
|
405
|
-
// orphan locks (crashed holder) are reclaimed by acquireLockWithRetry's
|
|
406
|
-
// staleMs-based steal logic after at most `staleMs`.
|
|
407
|
-
// FIX (TOCTOU): Re-validate symlink safety before each lock acquisition
|
|
408
|
-
// attempt. Between our initial check and the acquisition (and between
|
|
409
|
-
// acquireLockWithRetry's internal retries), an attacker could plant a
|
|
410
|
-
// symlink. We must re-check on each iteration to catch TOCTOU races.
|
|
411
|
-
let token = "";
|
|
412
|
-
let attempt = 0;
|
|
413
|
-
const deadline = Date.now() + staleMs * 2;
|
|
414
|
-
while (Date.now() <= deadline) {
|
|
415
|
-
if (!isSymlinkSafePath(path.dirname(lockFile))) throw new Error("Refusing: parent of lock directory is a symlink");
|
|
416
|
-
try {
|
|
417
|
-
token = acquireLockWithRetry(lockFile, staleMs, "file");
|
|
418
|
-
break;
|
|
419
|
-
} catch {
|
|
420
|
-
sleepSync(Math.min(250, 25 * 2 ** attempt));
|
|
421
|
-
attempt++;
|
|
422
|
-
}
|
|
423
|
-
}
|
|
424
|
-
if (token === "") throw new Error(`Run '${path.basename(lockFile)}' is locked by another operation.`);
|
|
425
|
-
fileLockHeldByUs.set(lockFile, token);
|
|
426
|
-
try {
|
|
427
|
-
return fn();
|
|
428
|
-
} finally {
|
|
429
|
-
// Token-guarded release: don't rm the lock if it has been stolen.
|
|
430
|
-
fileLockHeldByUs.delete(lockFile);
|
|
431
|
-
releaseLock(lockFile, token);
|
|
432
|
-
}
|
|
433
|
-
}
|
|
434
|
-
|
|
435
|
-
// H-1: track re-entrant run-lock acquisitions PER ASYNC CONTEXT (not process-global).
|
|
436
|
-
// Previously a module-global Map caused a bug: when withRunLock (async) yielded at
|
|
437
|
-
// `await fn()`, a concurrent withRunLockSync for the same run — fired from a
|
|
438
|
-
// different async context (e.g. a child stdout event handler) — saw the holder's
|
|
439
|
-
// token and bypassed the file lock entirely, allowing two writers in the critical
|
|
440
|
-
// section simultaneously. AsyncLocalStorage scopes the "held" set to the current
|
|
441
|
-
// async context, so a call from a different async context no longer bypasses and
|
|
442
|
-
// properly serializes against the on-disk lock. True nested calls in the SAME
|
|
443
|
-
// async context (e.g. handleResume -> executeTeamRun -> executeTeamRunCore) still
|
|
444
|
-
// bypass via lockCtx.getStore(), avoiding the deadlock the bypass was designed for.
|
|
445
|
-
const lockCtx = new AsyncLocalStorage<Set<string>>();
|
|
446
|
-
// Round 29: parallel map for withFileLockSync re-entrance. See the comment
|
|
447
|
-
// at the top of withFileLockSync for the full deadlock mechanism.
|
|
448
|
-
const fileLockHeldByUs = new Map<string, string>(); // lockFile -> token
|
|
449
|
-
|
|
450
|
-
// --- Async file lock (non-blocking alternative to withFileLockSync) ---
|
|
451
|
-
// Uses a promise-chain pattern to serialize per-path access without blocking
|
|
452
|
-
// the Node.js event loop. Unlike withFileLockSync (which uses O_EXCL +
|
|
453
|
-
// sleepSync for cross-process safety), this is **in-process only** —
|
|
454
|
-
// sufficient for single-process mailbox writes (team-runner is single-process).
|
|
455
|
-
// Mirrors the structure of withEventLogLockAsync in event-log.ts.
|
|
456
|
-
const fileAsyncLocks = new Map<string, Promise<unknown>>();
|
|
457
|
-
|
|
458
|
-
// FIND-02 follow-up (P3): re-entrance guard for the async file lock, mirroring
|
|
459
|
-
// the lockCtx pattern used by withRunLock. A future caller doing same-path
|
|
460
|
-
// nested withFileLockAsync(path, ...) inside another withFileLockAsync(path,
|
|
461
|
-
// ...) in the SAME async context would otherwise deadlock (the nested call
|
|
462
|
-
// chains after the outer's still-pending promise). AsyncLocalStorage scopes
|
|
463
|
-
// the held set to the current async context so cross-context callers still
|
|
464
|
-
// serialize via the promise chain (Phase 1 mailbox path is unaffected).
|
|
465
|
-
const fileAsyncLockCtx = new AsyncLocalStorage<Set<string>>();
|
|
466
|
-
|
|
467
|
-
export async function withFileLockAsync<T>(filePath: string, fn: () => Promise<T>): Promise<T> {
|
|
468
|
-
// Re-entrant within the same async context — run fn() directly (no chaining).
|
|
469
|
-
// Same semantics as the sync guard in withFileLockSync (fileLockHeldByUs)
|
|
470
|
-
// and the async guard in withRunLock (lockCtx).
|
|
471
|
-
if (fileAsyncLockCtx.getStore()?.has(filePath)) {
|
|
472
|
-
return await fn();
|
|
473
|
-
}
|
|
474
|
-
// Merge with the parent context's held set so nested DIFFERENT-path locks
|
|
475
|
-
// also bypass correctly (prevents cross-path deadlock, matching withRunLock).
|
|
476
|
-
const prevHeld = fileAsyncLockCtx.getStore() ?? new Set<string>();
|
|
477
|
-
const held = new Set(prevHeld);
|
|
478
|
-
held.add(filePath);
|
|
479
|
-
const prev = fileAsyncLocks.get(filePath) ?? Promise.resolve();
|
|
480
|
-
// Chain fn after the previous holder. `next` may reject (propagating to the
|
|
481
|
-
// caller), but `stored` never rejects so subsequent waiters aren't blocked.
|
|
482
|
-
// fn() is wrapped in fileAsyncLockCtx.run so nested same-context calls see
|
|
483
|
-
// `held` and bypass the promise chain (re-entrant), while cross-context
|
|
484
|
-
// callers chain normally via `prev`.
|
|
485
|
-
const next = prev.then(() => fileAsyncLockCtx.run(held, () => fn()));
|
|
486
|
-
const stored = next.then(
|
|
487
|
-
() => undefined,
|
|
488
|
-
() => undefined,
|
|
489
|
-
);
|
|
490
|
-
fileAsyncLocks.set(filePath, stored);
|
|
491
|
-
try {
|
|
492
|
-
return await next;
|
|
493
|
-
} finally {
|
|
494
|
-
// Compare-and-delete: only remove our entry if it still points at `stored`.
|
|
495
|
-
// With 3+ overlapping callers, an earlier caller's finally would otherwise
|
|
496
|
-
// delete a later caller's promise, breaking mutual exclusion.
|
|
497
|
-
if (fileAsyncLocks.get(filePath) === stored) {
|
|
498
|
-
fileAsyncLocks.delete(filePath);
|
|
499
|
-
}
|
|
500
|
-
}
|
|
501
|
-
}
|
|
502
|
-
|
|
503
|
-
export function withRunLockSync<T>(manifest: TeamRunManifest, fn: () => T, options: RunLockOptions = {}): T {
|
|
504
|
-
const filePath = lockPath(manifest);
|
|
505
|
-
const staleMs = options.staleMs ?? DEFAULT_STALE_MS;
|
|
506
|
-
// H-1: re-entrance check is scoped to the current async context, not process-global.
|
|
507
|
-
// A call from a DIFFERENT async context (e.g. a child-stdout event handler that
|
|
508
|
-
// runs while an async holder is awaiting) sees no held set and properly serializes.
|
|
509
|
-
if (lockCtx.getStore()?.has(filePath)) {
|
|
510
|
-
// Re-entrant within the same async context — already hold this lock.
|
|
511
|
-
return fn();
|
|
512
|
-
}
|
|
513
|
-
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
514
|
-
const token = acquireLockWithRetry(filePath, staleMs, "run");
|
|
515
|
-
const prevHeld = lockCtx.getStore() ?? new Set<string>();
|
|
516
|
-
const newHeld = new Set(prevHeld);
|
|
517
|
-
newHeld.add(filePath);
|
|
518
|
-
// Enter a new async context (or extend the current one) carrying the held set.
|
|
519
|
-
// On exit (sync return OR async resume after an awaited child), the context is
|
|
520
|
-
// restored — so the held set only contains locks acquired within fn()'s subtree.
|
|
521
|
-
return lockCtx.run(newHeld, () => {
|
|
522
|
-
try {
|
|
523
|
-
return fn();
|
|
524
|
-
} finally {
|
|
525
|
-
// FIX (CI flake): releaseLock uses token matching to prevent the
|
|
526
|
-
// "losing contender wipes winner's lock" race in multi-process scenarios.
|
|
527
|
-
// But within withRunLockSync/withRunLock (same process), the new acquire
|
|
528
|
-
// uses a FRESH random token — the previous stored token doesn't match the
|
|
529
|
-
// current token — and the lock file is never removed. Repeat acquisitions
|
|
530
|
-
// then keep failing with EEXIST because the file lingers.
|
|
531
|
-
// Since withRunLock* is always single-process, unconditionally delete
|
|
532
|
-
// (symlink guard is still applied for safety).
|
|
533
|
-
releaseOwnLock(filePath, token);
|
|
534
|
-
}
|
|
535
|
-
});
|
|
536
|
-
}
|
|
537
|
-
|
|
538
|
-
export async function withRunLock<T>(manifest: TeamRunManifest, fn: () => Promise<T>, options: RunLockOptions = {}): Promise<T> {
|
|
539
|
-
const filePath = lockPath(manifest);
|
|
540
|
-
const staleMs = options.staleMs ?? DEFAULT_STALE_MS;
|
|
541
|
-
if (lockCtx.getStore()?.has(filePath)) {
|
|
542
|
-
// Re-entrant within the same async context.
|
|
543
|
-
return await fn();
|
|
544
|
-
}
|
|
545
|
-
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
546
|
-
const token = await acquireLockWithRetryAsync(filePath, staleMs, "run");
|
|
547
|
-
const prevHeld = lockCtx.getStore() ?? new Set<string>();
|
|
548
|
-
const newHeld = new Set(prevHeld);
|
|
549
|
-
newHeld.add(filePath);
|
|
550
|
-
// AsyncLocalStorage propagates across await boundaries, so any code that
|
|
551
|
-
// resumes after an await inside fn() (including microtasks, timers, and
|
|
552
|
-
// awaited child Pi results) still sees newHeld.
|
|
553
|
-
return await lockCtx.run(newHeld, async () => {
|
|
554
|
-
try {
|
|
555
|
-
return await fn();
|
|
556
|
-
} finally {
|
|
557
|
-
// FIX (CI flake): see withRunLockSync above — use releaseOwnLock for
|
|
558
|
-
// unconditional deletion of the lock file we created (same process).
|
|
559
|
-
releaseOwnLock(filePath, token);
|
|
560
|
-
}
|
|
561
|
-
});
|
|
562
|
-
}
|