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
|
@@ -1,439 +0,0 @@
|
|
|
1
|
-
import * as crypto from "node:crypto";
|
|
2
|
-
import * as fs from "node:fs";
|
|
3
|
-
import * as path from "node:path";
|
|
4
|
-
import { DEFAULT_CACHE, DEFAULT_PATHS } from "../config/defaults.ts";
|
|
5
|
-
import { userCrewRoot } from "../utils/paths.ts";
|
|
6
|
-
import { isSafePathId } from "../utils/safe-paths.ts";
|
|
7
|
-
import { sharedScanCache } from "../utils/scan-cache.ts";
|
|
8
|
-
import { sleepSync } from "../utils/sleep.ts";
|
|
9
|
-
import { atomicWriteJson, isSymlinkSafePath, renameWithRetry } from "./atomic-write.ts";
|
|
10
|
-
import type { TeamRunManifest } from "./types.ts";
|
|
11
|
-
|
|
12
|
-
/** Magic bytes prefix for binary registry to prevent deserialization of hostile files. */
|
|
13
|
-
const BINARY_MAGIC = Buffer.from("PICREW2BIN", "utf-8");
|
|
14
|
-
|
|
15
|
-
/** Binary format version for forward compatibility. */
|
|
16
|
-
const BINARY_VERSION = 1;
|
|
17
|
-
|
|
18
|
-
export interface ActiveRunRegistryEntry {
|
|
19
|
-
runId: string;
|
|
20
|
-
cwd: string;
|
|
21
|
-
stateRoot: string;
|
|
22
|
-
manifestPath: string;
|
|
23
|
-
updatedAt: string;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
function registryPath(): string {
|
|
27
|
-
return path.join(userCrewRoot(), DEFAULT_PATHS.state.runsSubdir, "active-run-index.json");
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
/** 2.4 — binary mirror of the JSON registry, written alongside for fast reads. */
|
|
31
|
-
function registryBinaryPath(): string {
|
|
32
|
-
return path.join(userCrewRoot(), DEFAULT_PATHS.state.runsSubdir, "active-run-index.bin");
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
function registryLockPath(): string {
|
|
36
|
-
return `${registryPath()}.lock`;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
function lockCreatedAt(raw: string): number | undefined {
|
|
40
|
-
try {
|
|
41
|
-
const parsed = JSON.parse(raw) as { createdAt?: unknown };
|
|
42
|
-
if (typeof parsed.createdAt !== "string") return undefined;
|
|
43
|
-
const time = Date.parse(parsed.createdAt);
|
|
44
|
-
return Number.isNaN(time) ? undefined : time;
|
|
45
|
-
} catch {
|
|
46
|
-
return undefined;
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
function removeStaleRegistryLock(lockPath: string, staleMs: number): boolean {
|
|
51
|
-
try {
|
|
52
|
-
const stat = fs.statSync(lockPath);
|
|
53
|
-
const createdAt = lockCreatedAt(fs.readFileSync(lockPath, "utf-8")) ?? stat.mtimeMs;
|
|
54
|
-
if (Date.now() - createdAt <= staleMs) return false;
|
|
55
|
-
fs.rmSync(lockPath, { force: true });
|
|
56
|
-
return true;
|
|
57
|
-
} catch {
|
|
58
|
-
return false;
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
function withRegistryLock<T>(fn: () => T): T {
|
|
63
|
-
const filePath = registryLockPath();
|
|
64
|
-
const staleMs = 30_000;
|
|
65
|
-
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
66
|
-
let attempt = 0;
|
|
67
|
-
// FIX Issue 3: Reduced timeout from staleMs*2 (60s) to 10s max for responsive shutdown.
|
|
68
|
-
const deadline = Date.now() + 10_000;
|
|
69
|
-
while (true) {
|
|
70
|
-
try {
|
|
71
|
-
const fd = fs.openSync(filePath, fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_EXCL, 0o644);
|
|
72
|
-
try {
|
|
73
|
-
fs.writeSync(
|
|
74
|
-
fd,
|
|
75
|
-
JSON.stringify({
|
|
76
|
-
pid: process.pid,
|
|
77
|
-
createdAt: new Date().toISOString(),
|
|
78
|
-
}),
|
|
79
|
-
);
|
|
80
|
-
} finally {
|
|
81
|
-
fs.closeSync(fd);
|
|
82
|
-
}
|
|
83
|
-
break;
|
|
84
|
-
} catch (error) {
|
|
85
|
-
const code = (error as NodeJS.ErrnoException).code;
|
|
86
|
-
if (code !== "EEXIST") throw error;
|
|
87
|
-
if (!removeStaleRegistryLock(filePath, staleMs) && Date.now() > deadline)
|
|
88
|
-
throw new Error("Active-run registry is locked by another operation.");
|
|
89
|
-
sleepSync(Math.min(250, 25 * 2 ** attempt));
|
|
90
|
-
attempt += 1;
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
try {
|
|
94
|
-
return fn();
|
|
95
|
-
} finally {
|
|
96
|
-
try {
|
|
97
|
-
fs.rmSync(filePath, { force: true });
|
|
98
|
-
} catch {
|
|
99
|
-
// Best-effort cleanup.
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
function normalizeEntry(value: unknown): ActiveRunRegistryEntry | undefined {
|
|
105
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
|
|
106
|
-
const record = value as Record<string, unknown>;
|
|
107
|
-
const runId = typeof record.runId === "string" ? record.runId : undefined;
|
|
108
|
-
const cwd = typeof record.cwd === "string" ? record.cwd : undefined;
|
|
109
|
-
const stateRoot = typeof record.stateRoot === "string" ? record.stateRoot : undefined;
|
|
110
|
-
const manifestPath = typeof record.manifestPath === "string" ? record.manifestPath : undefined;
|
|
111
|
-
const updatedAt = typeof record.updatedAt === "string" ? record.updatedAt : undefined;
|
|
112
|
-
if (!runId || !isSafePathId(runId) || !cwd || !stateRoot || !manifestPath || !updatedAt) return undefined;
|
|
113
|
-
if (path.basename(stateRoot) !== runId) return undefined;
|
|
114
|
-
if (path.resolve(manifestPath) !== path.resolve(path.join(stateRoot, DEFAULT_PATHS.state.manifestFile))) return undefined;
|
|
115
|
-
return { runId, cwd, stateRoot, manifestPath, updatedAt };
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
export function readActiveRunRegistry(maxEntries = DEFAULT_CACHE.manifestMaxEntries): ActiveRunRegistryEntry[] {
|
|
119
|
-
let parsed: unknown;
|
|
120
|
-
// 2.4 — prefer the binary mirror (JSON after header for schema-safe deserialization).
|
|
121
|
-
// Fall back to JSON when the binary is missing or corrupt; this lets a 2-release
|
|
122
|
-
// migration co-exist with old readers.
|
|
123
|
-
try {
|
|
124
|
-
const buf = fs.readFileSync(registryBinaryPath());
|
|
125
|
-
// Security: verify magic bytes before parsing to prevent hostile files
|
|
126
|
-
if (buf.length < BINARY_MAGIC.length || !buf.slice(0, BINARY_MAGIC.length).equals(BINARY_MAGIC)) {
|
|
127
|
-
throw new Error("Invalid binary registry: missing magic bytes");
|
|
128
|
-
}
|
|
129
|
-
// FIX Issue 1: Verify version field for forward compatibility.
|
|
130
|
-
const versionOffset = BINARY_MAGIC.length;
|
|
131
|
-
const version = buf.readUInt32BE(versionOffset);
|
|
132
|
-
if (version !== BINARY_VERSION) {
|
|
133
|
-
throw new Error(`Unsupported binary registry version: ${version}`);
|
|
134
|
-
}
|
|
135
|
-
// FIX Issue 1: Use JSON.parse instead of v8 deserialize for schema-safe parsing.
|
|
136
|
-
// The magic+version header provides integrity at the file level.
|
|
137
|
-
const jsonSlice = buf.slice(versionOffset + 4);
|
|
138
|
-
parsed = JSON.parse(jsonSlice.toString("utf-8"));
|
|
139
|
-
} catch {
|
|
140
|
-
try {
|
|
141
|
-
parsed = JSON.parse(fs.readFileSync(registryPath(), "utf-8"));
|
|
142
|
-
} catch {
|
|
143
|
-
return [];
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
const entries = Array.isArray(parsed)
|
|
147
|
-
? parsed.map(normalizeEntry).filter((entry): entry is ActiveRunRegistryEntry => entry !== undefined)
|
|
148
|
-
: [];
|
|
149
|
-
const byId = new Map<string, ActiveRunRegistryEntry>();
|
|
150
|
-
for (const entry of entries.sort((a, b) => (b.updatedAt ?? "").localeCompare(a.updatedAt ?? ""))) {
|
|
151
|
-
if (!byId.has(entry.runId)) byId.set(entry.runId, entry);
|
|
152
|
-
}
|
|
153
|
-
return [...byId.values()].slice(0, Math.max(0, maxEntries));
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
/**
|
|
157
|
-
* FIX Issues 1 & 2: Atomic binary write using O_CREAT|O_EXCL|O_NOFOLLOW pattern.
|
|
158
|
-
* Writes to temp file first, then renames. Includes version field for forward compatibility.
|
|
159
|
-
*/
|
|
160
|
-
function atomicWriteBinary(filePath: string, entries: ActiveRunRegistryEntry[]): void {
|
|
161
|
-
if (!isSymlinkSafePath(filePath))
|
|
162
|
-
throw new Error(`Refusing to write binary registry: target is a symlink or inside untrusted directory: ${filePath}`);
|
|
163
|
-
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
164
|
-
const tempPath = `${filePath}.${crypto.randomUUID()}.tmp`;
|
|
165
|
-
const O_NOFOLLOW = typeof fs.constants.O_NOFOLLOW === "number" ? fs.constants.O_NOFOLLOW : 0;
|
|
166
|
-
const fd = fs.openSync(tempPath, fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_EXCL | O_NOFOLLOW, 0o600);
|
|
167
|
-
try {
|
|
168
|
-
// Post-open verification: on Windows O_NOFOLLOW is 0, so verify FD is a regular file
|
|
169
|
-
const openedStat = fs.fstatSync(fd);
|
|
170
|
-
if (!openedStat.isFile()) {
|
|
171
|
-
fs.closeSync(fd);
|
|
172
|
-
throw new Error(`Refusing to write binary registry: opened path is not a regular file: ${tempPath}`);
|
|
173
|
-
}
|
|
174
|
-
// Write: magic (10 bytes) + version (4 bytes) + JSON data
|
|
175
|
-
const header = Buffer.allocUnsafe(14);
|
|
176
|
-
BINARY_MAGIC.copy(header, 0);
|
|
177
|
-
header.writeUInt32BE(BINARY_VERSION, BINARY_MAGIC.length);
|
|
178
|
-
// FIX Issue 1: Use JSON.stringify instead of v8 serialize for schema-safe output.
|
|
179
|
-
const jsonStr = JSON.stringify(entries);
|
|
180
|
-
const jsonBuf = Buffer.from(jsonStr, "utf-8");
|
|
181
|
-
fs.writeSync(fd, Buffer.concat([header, jsonBuf]));
|
|
182
|
-
fs.closeSync(fd);
|
|
183
|
-
renameWithRetry(tempPath, filePath);
|
|
184
|
-
} catch (error) {
|
|
185
|
-
try {
|
|
186
|
-
fs.rmSync(tempPath, { force: true });
|
|
187
|
-
} catch {
|
|
188
|
-
/* best-effort */
|
|
189
|
-
}
|
|
190
|
-
throw error;
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
function writeEntries(entries: ActiveRunRegistryEntry[], skipTerminalRunIds?: Set<string>): void {
|
|
195
|
-
const max = DEFAULT_CACHE.manifestMaxEntries;
|
|
196
|
-
// FIX Issue 3: Gracefully prune oldest entries instead of throwing when cap is exceeded.
|
|
197
|
-
if (entries.length > max) {
|
|
198
|
-
const sorted = [...entries].sort((a, b) => (a.updatedAt ?? "").localeCompare(b.updatedAt ?? ""));
|
|
199
|
-
entries = sorted.slice(entries.length - max);
|
|
200
|
-
}
|
|
201
|
-
// FIX Issue 1: Skip entries that transitioned to terminal since filterAliveEntries was called.
|
|
202
|
-
// This closes the TOCTOU race between status check in filterAliveEntries and write.
|
|
203
|
-
if (skipTerminalRunIds?.size) {
|
|
204
|
-
entries = entries.filter((e) => !skipTerminalRunIds.has(e.runId));
|
|
205
|
-
}
|
|
206
|
-
fs.mkdirSync(path.dirname(registryPath()), { recursive: true });
|
|
207
|
-
// FIX Issues 1 & 2: Write both to temp files first, then rename both atomically.
|
|
208
|
-
// If either rename fails, neither file is updated — registry stays consistent.
|
|
209
|
-
const tempJson = `${registryPath()}.${crypto.randomUUID()}.tmp`;
|
|
210
|
-
const tempBin = `${registryBinaryPath()}.${crypto.randomUUID()}.tmp`;
|
|
211
|
-
let jsonRenamed = false;
|
|
212
|
-
let binRenamed = false;
|
|
213
|
-
try {
|
|
214
|
-
// Write JSON to temp first
|
|
215
|
-
atomicWriteJson(tempJson, entries);
|
|
216
|
-
// Write binary to temp (atomic pattern)
|
|
217
|
-
atomicWriteBinary(tempBin, entries);
|
|
218
|
-
// Both written successfully — rename both atomically
|
|
219
|
-
renameWithRetry(tempJson, registryPath());
|
|
220
|
-
jsonRenamed = true;
|
|
221
|
-
renameWithRetry(tempBin, registryBinaryPath());
|
|
222
|
-
binRenamed = true;
|
|
223
|
-
} catch (error) {
|
|
224
|
-
// FIX Issue 5: Recovery when one rename succeeded — try to rename the other from its temp file
|
|
225
|
-
// instead of deleting both. This preserves whichever registry was successfully written.
|
|
226
|
-
if (jsonRenamed && !binRenamed) {
|
|
227
|
-
// JSON succeeded, binary failed — try to recover binary from temp
|
|
228
|
-
try {
|
|
229
|
-
renameWithRetry(tempBin, registryBinaryPath());
|
|
230
|
-
binRenamed = true;
|
|
231
|
-
} catch {
|
|
232
|
-
/* recovery failed */
|
|
233
|
-
}
|
|
234
|
-
} else if (binRenamed && !jsonRenamed) {
|
|
235
|
-
// Binary succeeded, JSON failed — try to recover JSON from temp
|
|
236
|
-
try {
|
|
237
|
-
renameWithRetry(tempJson, registryPath());
|
|
238
|
-
jsonRenamed = true;
|
|
239
|
-
} catch {
|
|
240
|
-
/* recovery failed */
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
// FIX Issue 2: If recovery failed, do NOT delete the final files — they may contain valid
|
|
244
|
-
// data from a prior write cycle. Leave temp files for manual recovery and throw error.
|
|
245
|
-
if (!binRenamed || !jsonRenamed) {
|
|
246
|
-
// Attempt to clean up temp files only (not final files)
|
|
247
|
-
try {
|
|
248
|
-
fs.rmSync(tempJson, { force: true });
|
|
249
|
-
} catch {
|
|
250
|
-
/* best-effort */
|
|
251
|
-
}
|
|
252
|
-
try {
|
|
253
|
-
fs.rmSync(tempBin, { force: true });
|
|
254
|
-
} catch {
|
|
255
|
-
/* best-effort */
|
|
256
|
-
}
|
|
257
|
-
throw error;
|
|
258
|
-
}
|
|
259
|
-
// FIX Issue 1: Ensure temp file cleanup executes even when recovery rename fails.
|
|
260
|
-
// If recovery throws, the error propagates before reaching the cleanup below.
|
|
261
|
-
// Wrapping in nested try-catch guarantees cleanup runs regardless of recovery outcome.
|
|
262
|
-
try {
|
|
263
|
-
fs.rmSync(tempJson, { force: true });
|
|
264
|
-
} catch {
|
|
265
|
-
/* best-effort */
|
|
266
|
-
}
|
|
267
|
-
try {
|
|
268
|
-
fs.rmSync(tempBin, { force: true });
|
|
269
|
-
} catch {
|
|
270
|
-
/* best-effort */
|
|
271
|
-
}
|
|
272
|
-
throw error;
|
|
273
|
-
}
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
const TERMINAL_STATUSES = new Set(["completed", "failed", "cancelled", "blocked"]);
|
|
277
|
-
|
|
278
|
-
/**
|
|
279
|
-
* FIX Issue 1: Filter out entries that are no longer active: terminal status, missing manifest,
|
|
280
|
-
* or symlink-unsafe paths. This prevents unbounded growth and guards against symlink attacks.
|
|
281
|
-
*
|
|
282
|
-
* WARNING: This function reads manifest files WITHOUT holding the registry lock (TOCTOU race).
|
|
283
|
-
* Status filtering is best-effort at read time. The terminal runIds set is returned so callers
|
|
284
|
-
* can pass them to writeEntries to skip entries that transitioned to terminal since filtering.
|
|
285
|
-
*/
|
|
286
|
-
function filterAliveEntries(entries: ActiveRunRegistryEntry[]): {
|
|
287
|
-
alive: ActiveRunRegistryEntry[];
|
|
288
|
-
terminalRunIds: Set<string>;
|
|
289
|
-
} {
|
|
290
|
-
const terminalRunIds = new Set<string>();
|
|
291
|
-
const alive = entries.filter((entry) => {
|
|
292
|
-
try {
|
|
293
|
-
if (!fs.existsSync(entry.cwd)) return false;
|
|
294
|
-
if (!fs.existsSync(entry.manifestPath)) return false;
|
|
295
|
-
// FIX Issue 2: Guard against symlink attacks on stateRoot and manifestPath
|
|
296
|
-
if (!isSymlinkSafePath(entry.stateRoot)) return false;
|
|
297
|
-
if (!isSymlinkSafePath(entry.manifestPath)) return false;
|
|
298
|
-
} catch {
|
|
299
|
-
return false;
|
|
300
|
-
}
|
|
301
|
-
try {
|
|
302
|
-
const raw = JSON.parse(fs.readFileSync(entry.manifestPath, "utf-8")) as {
|
|
303
|
-
status?: string;
|
|
304
|
-
async?: { pid?: number };
|
|
305
|
-
updatedAt?: string;
|
|
306
|
-
};
|
|
307
|
-
if (TERMINAL_STATUSES.has(raw.status ?? "")) {
|
|
308
|
-
terminalRunIds.add(entry.runId);
|
|
309
|
-
return false;
|
|
310
|
-
}
|
|
311
|
-
// FIX Issue 2: Robust PID liveness check - only ESRCH means process is dead.
|
|
312
|
-
// EPERM means process exists but we can't signal it (different security context).
|
|
313
|
-
// Also check manifest age to guard against PID reuse.
|
|
314
|
-
if (raw.async?.pid) {
|
|
315
|
-
try {
|
|
316
|
-
process.kill(raw.async.pid, 0);
|
|
317
|
-
} catch (err) {
|
|
318
|
-
// Only treat "process does not exist" as dead (ESRCH/ENOENT).
|
|
319
|
-
// EPERM means process is alive but protected; treat as alive.
|
|
320
|
-
const code = (err as NodeJS.ErrnoException).code;
|
|
321
|
-
if (code !== "ESRCH" && code !== "ENOENT") return false;
|
|
322
|
-
return false;
|
|
323
|
-
}
|
|
324
|
-
// FIX Issue 2: Async runs older than 30 min are stale even if PID is alive.
|
|
325
|
-
// This guards against PID reuse after the original process exits.
|
|
326
|
-
const updatedAt = typeof raw.updatedAt === "string" ? Date.parse(raw.updatedAt) : NaN;
|
|
327
|
-
if (Number.isFinite(updatedAt) && Date.now() - updatedAt > 30 * 60 * 1000) return false;
|
|
328
|
-
}
|
|
329
|
-
// 2.19 — Stale non-async run: live-session/scaffold runs older than 30 min
|
|
330
|
-
// Without this, test runs that crash/leak would stay in the registry forever.
|
|
331
|
-
if (!raw.async) {
|
|
332
|
-
const updatedAt = typeof raw.updatedAt === "string" ? Date.parse(raw.updatedAt) : NaN;
|
|
333
|
-
if (Number.isFinite(updatedAt) && Date.now() - updatedAt > 30 * 60 * 1000) return false;
|
|
334
|
-
}
|
|
335
|
-
} catch {
|
|
336
|
-
return false;
|
|
337
|
-
}
|
|
338
|
-
return true;
|
|
339
|
-
});
|
|
340
|
-
return { alive, terminalRunIds };
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
export function registerActiveRun(manifest: TeamRunManifest): void {
|
|
344
|
-
const entry: ActiveRunRegistryEntry = {
|
|
345
|
-
runId: manifest.runId,
|
|
346
|
-
cwd: manifest.cwd,
|
|
347
|
-
stateRoot: manifest.stateRoot,
|
|
348
|
-
manifestPath: path.join(manifest.stateRoot, DEFAULT_PATHS.state.manifestFile),
|
|
349
|
-
updatedAt: manifest.updatedAt,
|
|
350
|
-
};
|
|
351
|
-
withRegistryLock(() => {
|
|
352
|
-
const existing = readActiveRunRegistry().filter((item) => item.runId !== manifest.runId);
|
|
353
|
-
// Inline cleanup: remove terminal-status and stale entries before writing.
|
|
354
|
-
// This prevents unbounded growth between sessions.
|
|
355
|
-
const { alive, terminalRunIds } = filterAliveEntries(existing);
|
|
356
|
-
// FIX Issue 4: Also filter the new entry being registered through filterAliveEntries
|
|
357
|
-
// to exclude it if it is terminal (e.g. completed between call and write).
|
|
358
|
-
const { alive: filteredAlive } = filterAliveEntries([entry]);
|
|
359
|
-
const filteredEntry = filteredAlive.length > 0 ? entry : null;
|
|
360
|
-
if (filteredEntry === null) {
|
|
361
|
-
throw new Error(`Cannot register run ${manifest.runId}: entry is terminal or paths are not symlink-safe`);
|
|
362
|
-
}
|
|
363
|
-
// FIX Issue 1: Pass terminal runIds from filterAliveEntries to writeEntries.
|
|
364
|
-
// writeEntries will skip these entries, closing the TOCTOU race between status check
|
|
365
|
-
// and write without re-reading manifest inside the lock.
|
|
366
|
-
const allTerminalRunIds = new Set(terminalRunIds);
|
|
367
|
-
if (filteredAlive.length === 0) allTerminalRunIds.add(entry.runId);
|
|
368
|
-
writeEntries([filteredEntry, ...alive], allTerminalRunIds);
|
|
369
|
-
});
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
export function unregisterActiveRun(runId: string): void {
|
|
373
|
-
if (!isSafePathId(runId)) {
|
|
374
|
-
console.warn(`unregisterActiveRun: invalid runId ignored: ${runId}`);
|
|
375
|
-
return;
|
|
376
|
-
}
|
|
377
|
-
withRegistryLock(() => {
|
|
378
|
-
// FIX Issue 1: Filter alive entries and pass terminal runIds to writeEntries,
|
|
379
|
-
// closing the TOCTOU race between status check and write (same pattern as registerActiveRun).
|
|
380
|
-
const { alive, terminalRunIds } = filterAliveEntries(readActiveRunRegistry().filter((entry) => entry.runId !== runId));
|
|
381
|
-
writeEntries(alive, terminalRunIds);
|
|
382
|
-
});
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
/**
|
|
386
|
-
* Status filtering is best-effort at read time — entries returned by this function may have
|
|
387
|
-
* transitioned to terminal status between the manifest read and the filter check.
|
|
388
|
-
* Callers must re-check status when opening individual run manifests.
|
|
389
|
-
*/
|
|
390
|
-
export function activeRunEntries(): ActiveRunRegistryEntry[] {
|
|
391
|
-
const entries: ActiveRunRegistryEntry[] = [];
|
|
392
|
-
for (const entry of readActiveRunRegistry()) {
|
|
393
|
-
try {
|
|
394
|
-
// Skip entries whose CWD no longer exists (temp test dirs, deleted projects)
|
|
395
|
-
if (!fs.existsSync(entry.cwd)) continue;
|
|
396
|
-
// FIX Issue 4: sharedScanCache.readAndCache does stat checks internally.
|
|
397
|
-
// Only check stateRoot/manifestPath existence here if cache miss (cached.raw is undefined).
|
|
398
|
-
const cached = sharedScanCache.readAndCache("active-manifests", entry.runId, entry.manifestPath);
|
|
399
|
-
if (cached?.raw === undefined && (!fs.existsSync(entry.stateRoot) || !fs.existsSync(entry.manifestPath))) continue;
|
|
400
|
-
// FIX Issue 4: Check full ancestor chain for symlinks, not just immediate stateRoot.
|
|
401
|
-
if (!isSymlinkSafePath(entry.stateRoot)) continue;
|
|
402
|
-
// FIX Issue 4: Use cached manifest content directly when available.
|
|
403
|
-
// sharedScanCache.readAndCache already read the file; avoid redundant re-read.
|
|
404
|
-
const manifest = (cached?.raw !== undefined ? cached.raw : JSON.parse(fs.readFileSync(entry.manifestPath, "utf-8"))) as {
|
|
405
|
-
status?: unknown;
|
|
406
|
-
updatedAt?: string;
|
|
407
|
-
async?: { pid?: number };
|
|
408
|
-
};
|
|
409
|
-
if (
|
|
410
|
-
manifest.status !== "queued" &&
|
|
411
|
-
manifest.status !== "planning" &&
|
|
412
|
-
manifest.status !== "running" &&
|
|
413
|
-
manifest.status !== "blocked"
|
|
414
|
-
)
|
|
415
|
-
continue;
|
|
416
|
-
// PID liveness check: async runs with dead PID are stale — don't surface them
|
|
417
|
-
if (manifest.async?.pid) {
|
|
418
|
-
try {
|
|
419
|
-
process.kill(manifest.async.pid, 0);
|
|
420
|
-
} catch {
|
|
421
|
-
continue;
|
|
422
|
-
}
|
|
423
|
-
}
|
|
424
|
-
// Stale non-async run: live-session/scaffold runs older than 30 min
|
|
425
|
-
if (!manifest.async) {
|
|
426
|
-
const updatedAt = typeof manifest.updatedAt === "string" ? Date.parse(manifest.updatedAt) : NaN;
|
|
427
|
-
if (Number.isFinite(updatedAt) && Date.now() - updatedAt > 30 * 60 * 1000) continue;
|
|
428
|
-
}
|
|
429
|
-
entries.push(entry);
|
|
430
|
-
} catch {
|
|
431
|
-
// Ignore stale entries; callers filter active status from manifests.
|
|
432
|
-
}
|
|
433
|
-
}
|
|
434
|
-
return entries;
|
|
435
|
-
}
|
|
436
|
-
|
|
437
|
-
export function activeRunRoots(): string[] {
|
|
438
|
-
return [...new Set(activeRunEntries().map((entry) => path.dirname(entry.stateRoot)))];
|
|
439
|
-
}
|
|
@@ -1,176 +0,0 @@
|
|
|
1
|
-
import { createHash } from "node:crypto";
|
|
2
|
-
import * as fs from "node:fs";
|
|
3
|
-
import * as path from "node:path";
|
|
4
|
-
import { redactSecretString, redactSecrets } from "../utils/redaction.ts";
|
|
5
|
-
import { resolveRealContainedPath } from "../utils/safe-paths.ts";
|
|
6
|
-
import { atomicWriteFile } from "./atomic-write.ts";
|
|
7
|
-
import type { ArtifactDescriptor } from "./types.ts";
|
|
8
|
-
|
|
9
|
-
function hashContent(content: string): string {
|
|
10
|
-
return createHash("sha256").update(content).digest("hex");
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
/** Public alias used by writeProgress's content-skip cache. */
|
|
14
|
-
export function hashArtifactContent(content: string): string {
|
|
15
|
-
return hashContent(content);
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export const CLEANUP_MARKER_FILE = ".last-cleanup";
|
|
19
|
-
|
|
20
|
-
export interface ArtifactWriteOptions {
|
|
21
|
-
kind: ArtifactDescriptor["kind"];
|
|
22
|
-
relativePath: string;
|
|
23
|
-
content: string;
|
|
24
|
-
producer: string;
|
|
25
|
-
retention?: ArtifactDescriptor["retention"];
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
export interface ArtifactCleanupOptions {
|
|
29
|
-
maxAgeDays: number;
|
|
30
|
-
maxAgeMs?: number;
|
|
31
|
-
markerFile?: string;
|
|
32
|
-
scanGraceMs?: number;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
function parseAgeDays(value: unknown): number | undefined {
|
|
36
|
-
if (typeof value !== "number" || !Number.isFinite(value) || value < 0) return undefined;
|
|
37
|
-
return Math.floor(value);
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
function nowMs(): number {
|
|
41
|
-
return Date.now();
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
function readMarkerMtime(artifactsRoot: string, markerFile: string): number | undefined {
|
|
45
|
-
try {
|
|
46
|
-
return fs.statSync(path.join(artifactsRoot, markerFile)).mtimeMs;
|
|
47
|
-
} catch {
|
|
48
|
-
return undefined;
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
function shouldCleanup(artifactsRoot: string, markerFile: string, scanGraceMs: number): boolean {
|
|
53
|
-
const marker = readMarkerMtime(artifactsRoot, markerFile);
|
|
54
|
-
if (marker === undefined) return true;
|
|
55
|
-
return nowMs() - marker >= scanGraceMs;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
export function writeCleanupMarker(artifactsRoot: string, markerFile: string): void {
|
|
59
|
-
fs.mkdirSync(artifactsRoot, { recursive: true });
|
|
60
|
-
atomicWriteFile(path.join(artifactsRoot, markerFile), String(nowMs()));
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
export function cleanupOldArtifacts(artifactsRoot: string, options: ArtifactCleanupOptions): void {
|
|
64
|
-
if (!fs.existsSync(artifactsRoot)) return;
|
|
65
|
-
const maxAgeDays = parseAgeDays(options.maxAgeDays);
|
|
66
|
-
if (maxAgeDays === undefined) return;
|
|
67
|
-
const markerFile = options.markerFile ?? CLEANUP_MARKER_FILE;
|
|
68
|
-
const scanGraceMs = options.scanGraceMs ?? 24 * 60 * 60 * 1000;
|
|
69
|
-
if (!shouldCleanup(artifactsRoot, markerFile, scanGraceMs)) return;
|
|
70
|
-
const maxAgeMs = options.maxAgeMs ?? maxAgeDays * 24 * 60 * 60 * 1000;
|
|
71
|
-
const cutoff = nowMs() - maxAgeMs;
|
|
72
|
-
let didCleanup = false;
|
|
73
|
-
try {
|
|
74
|
-
// FIX: Use { withFileTypes: true } to get Dirent objects (with isDirectory/isFile
|
|
75
|
-
// info), avoiding the need for a separate statSync per entry just to check the
|
|
76
|
-
// type. We still need statSync for mtime, but only on entries that passed the
|
|
77
|
-
// marker-file and symlink filters.
|
|
78
|
-
const entries = fs.readdirSync(artifactsRoot, { withFileTypes: true });
|
|
79
|
-
for (const entry of entries) {
|
|
80
|
-
if (entry.name === markerFile) continue;
|
|
81
|
-
if (entry.isSymbolicLink()) continue; // skip symlinks — could escape artifactsRoot
|
|
82
|
-
const target = path.join(artifactsRoot, entry.name);
|
|
83
|
-
try {
|
|
84
|
-
const stat = fs.statSync(target);
|
|
85
|
-
if (stat.mtimeMs >= cutoff) continue;
|
|
86
|
-
// Use Dirent info instead of stat.isDirectory() to save a stat call
|
|
87
|
-
if (entry.isDirectory()) {
|
|
88
|
-
fs.rmSync(target, { recursive: true, force: true });
|
|
89
|
-
} else {
|
|
90
|
-
fs.unlinkSync(target);
|
|
91
|
-
}
|
|
92
|
-
didCleanup = true;
|
|
93
|
-
} catch {
|
|
94
|
-
// Ignore cleanup races and permission issues in best-effort mode.
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
writeCleanupMarker(artifactsRoot, markerFile);
|
|
98
|
-
} catch {
|
|
99
|
-
// Ignore unreadable roots in best-effort mode.
|
|
100
|
-
}
|
|
101
|
-
if (!didCleanup) writeCleanupMarker(artifactsRoot, markerFile);
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
function resolveInside(baseDir: string, relativePath: string): string {
|
|
105
|
-
// Check if baseDir is a symlink on every call to prevent symlink attacks
|
|
106
|
-
try {
|
|
107
|
-
if (fs.lstatSync(baseDir).isSymbolicLink()) throw new Error(`Artifacts root is a symbolic link — not allowed: ${baseDir}`);
|
|
108
|
-
} catch (err) {
|
|
109
|
-
// If lstatSync fails because baseDir doesn't exist yet, that's fine —
|
|
110
|
-
// it will be created by writeArtifact. Any other error should propagate.
|
|
111
|
-
if ((err as NodeJS.ErrnoException).code !== "ENOENT") throw err;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
const normalizedRelativePath = relativePath.replaceAll("\\", "/").replace(/^\.\/+/, "");
|
|
115
|
-
if (
|
|
116
|
-
!normalizedRelativePath ||
|
|
117
|
-
normalizedRelativePath.split("/").some((segment) => segment === "..") ||
|
|
118
|
-
path.isAbsolute(normalizedRelativePath)
|
|
119
|
-
) {
|
|
120
|
-
throw new Error(`Invalid artifact path: ${relativePath}`);
|
|
121
|
-
}
|
|
122
|
-
// Use resolveRealContainedPath to resolve symlinks before checking containment,
|
|
123
|
-
// preventing TOCTOU attacks where intermediate directories are replaced with symlinks.
|
|
124
|
-
return resolveRealContainedPath(baseDir, normalizedRelativePath);
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
export function writeArtifact(artifactsRoot: string, options: ArtifactWriteOptions): ArtifactDescriptor {
|
|
128
|
-
// FIX: Create the artifactsRoot BEFORE calling resolveInside/resolveRealContainedPath,
|
|
129
|
-
// which open() the path to validate symlinks (fails with ENOENT if not yet created).
|
|
130
|
-
// Also reject symlinks in baseDir at this point (catches both pre-existing symlinks
|
|
131
|
-
// and the case where baseDir was created by mkdirSync as a regular dir).
|
|
132
|
-
fs.mkdirSync(artifactsRoot, { recursive: true });
|
|
133
|
-
if (fs.lstatSync(artifactsRoot).isSymbolicLink()) {
|
|
134
|
-
throw new Error(`Artifacts root is a symbolic link — not allowed: ${artifactsRoot}`);
|
|
135
|
-
}
|
|
136
|
-
const filePath = resolveInside(artifactsRoot, options.relativePath);
|
|
137
|
-
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
138
|
-
resolveRealContainedPath(artifactsRoot, path.dirname(filePath));
|
|
139
|
-
let content = options.content;
|
|
140
|
-
// Structural JSON redaction first — catches quoted-JSON secrets
|
|
141
|
-
// ("api_key":"sk-...") and nested keys that flat redactSecretString misses.
|
|
142
|
-
// The flat scan below still catches free-text patterns (Bearer/JWT/Auth
|
|
143
|
-
// headers) that may live inside JSON string values. See security review M2.
|
|
144
|
-
//
|
|
145
|
-
// Formatting preservation: re-stringify with the SAME indentation as the
|
|
146
|
-
// input so pretty-printed artifacts (e.g. group-join metadata expected by
|
|
147
|
-
// test/integration/phase4-runtime.test.ts to contain `"partial": false`)
|
|
148
|
-
// keep their whitespace. Detect pretty-vs-compact from the raw input.
|
|
149
|
-
const trimmed = content.trim();
|
|
150
|
-
if (trimmed.startsWith("{") || trimmed.startsWith("[")) {
|
|
151
|
-
try {
|
|
152
|
-
const parsed: unknown = JSON.parse(content);
|
|
153
|
-
const isPretty = /\n|"\s*:\s/.test(content);
|
|
154
|
-
content = JSON.stringify(redactSecrets(parsed), null, isPretty ? 2 : undefined);
|
|
155
|
-
} catch {
|
|
156
|
-
// not valid JSON — fall through to flat redaction only
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
content = redactSecretString(content);
|
|
160
|
-
atomicWriteFile(filePath, content);
|
|
161
|
-
// Compute hash on written bytes for integrity verification.
|
|
162
|
-
// Read back the actual file content to handle atomicWrite fallback path
|
|
163
|
-
// where the written content might differ from the input (e.g., concurrent writes).
|
|
164
|
-
const writtenContent = fs.readFileSync(filePath, "utf-8");
|
|
165
|
-
const contentHash = hashContent(writtenContent);
|
|
166
|
-
const stats = fs.statSync(filePath);
|
|
167
|
-
return {
|
|
168
|
-
kind: options.kind,
|
|
169
|
-
path: filePath,
|
|
170
|
-
createdAt: new Date().toISOString(),
|
|
171
|
-
producer: options.producer,
|
|
172
|
-
sizeBytes: stats.size,
|
|
173
|
-
contentHash,
|
|
174
|
-
retention: options.retention ?? "run",
|
|
175
|
-
};
|
|
176
|
-
}
|