pi-crew 0.2.3 → 0.2.4
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/AGENTS.md +57 -32
- package/CHANGELOG.md +466 -448
- package/LICENSE +21 -21
- package/NOTICE.md +16 -16
- package/README.md +323 -323
- package/docs/FEATURE_INTAKE.md +126 -0
- package/docs/HARNESS.md +86 -0
- package/docs/HARNESS_BACKLOG.md +41 -0
- package/docs/TEST_MATRIX.md +49 -0
- package/docs/actions-reference.md +595 -595
- package/docs/architecture.md +180 -180
- package/docs/code-review-2026-05-11.md +592 -592
- package/docs/commands-reference.md +347 -347
- package/docs/comparison-pi-subagents-vs-pi-crew.md +303 -0
- package/docs/decisions/0001-durable-state.md +41 -0
- package/docs/decisions/0002-child-process-for-async.md +42 -0
- package/docs/decisions/0003-depth-guard.md +36 -0
- package/docs/decisions/0004-execfile-over-exec.md +34 -0
- package/docs/decisions/0005-no-parameter-properties.md +49 -0
- package/docs/decisions/0006-publish-bundled-esm.md +63 -0
- package/docs/decisions/0007-active-run-binary-index.md +54 -0
- package/docs/decisions/0008-child-pi-warm-pool.md +61 -0
- package/docs/decisions/README.md +23 -0
- package/docs/followup-review-round4-2026-05-13.md +107 -0
- package/docs/implementation-plan-top3.md +333 -0
- package/docs/live-mailbox-runtime.md +36 -36
- package/docs/next-upgrade-roadmap.md +808 -808
- package/docs/oh-my-pi-research.md +509 -0
- package/docs/perf/baseline-2026-05.md +113 -0
- package/docs/perf/final-report-2026-05.md +206 -0
- package/docs/perf/sprint-1-report.md +71 -0
- package/docs/perf/sprint-2-report.md +81 -0
- package/docs/perf/sprint-2.5-report.md +53 -0
- package/docs/perf/sprint-3-report.md +36 -0
- package/docs/perf/sprint-4-report.md +47 -0
- package/docs/perf/sprint-5-report.md +51 -0
- package/docs/perf/sprint-6-report.md +94 -0
- package/docs/perf/sprint-7-report.md +74 -0
- package/docs/perf/upgrade-plan-2026-05.md +147 -0
- package/docs/pi-subagents3-deep-analysis.md +508 -0
- package/docs/product/README.md +31 -0
- package/docs/product/platform.md +27 -0
- package/docs/product/runtime-safety.md +37 -0
- package/docs/product/team-run.md +39 -0
- package/docs/product/team-tool.md +37 -0
- package/docs/publishing.md +65 -65
- package/docs/resource-formats.md +134 -134
- package/docs/runtime-analysis-child-vs-live.md +171 -0
- package/docs/runtime-flow.md +148 -148
- package/docs/runtime-migration-in-process-analysis.md +250 -0
- package/docs/stories/README.md +30 -0
- package/docs/stories/backlog.md +36 -0
- package/docs/templates/decision.md +27 -0
- package/docs/templates/story.md +44 -0
- package/docs/templates/validation-report.md +32 -0
- package/docs/usage.md +238 -238
- package/index.ts +7 -6
- package/install.mjs +65 -65
- package/package.json +107 -100
- package/schema.json +222 -222
- package/skills/child-pi-spawning/SKILL.md +213 -0
- package/skills/context-artifact-hygiene/SKILL.md +32 -0
- package/skills/event-log-tracing/SKILL.md +299 -0
- package/skills/git-master/SKILL.md +225 -24
- package/skills/live-agent-lifecycle/SKILL.md +192 -0
- package/skills/mailbox-interactive/SKILL.md +300 -19
- package/skills/model-routing-context/SKILL.md +94 -0
- package/skills/multi-perspective-review/SKILL.md +88 -0
- package/skills/read-only-explorer/SKILL.md +250 -26
- package/skills/safe-bash/SKILL.md +307 -21
- package/skills/verification-before-done/SKILL.md +11 -2
- package/skills/widget-rendering/SKILL.md +258 -0
- package/skills/workspace-isolation/SKILL.md +202 -0
- package/skills/worktree-isolation/SKILL.md +202 -18
- package/src/adapters/claude-adapter.ts +25 -25
- package/src/adapters/codex-adapter.ts +21 -21
- package/src/adapters/cursor-adapter.ts +17 -17
- package/src/adapters/export-util.ts +137 -137
- package/src/adapters/index.ts +15 -15
- package/src/adapters/registry.ts +18 -18
- package/src/adapters/types.ts +23 -23
- package/src/agents/agent-config.ts +38 -38
- package/src/agents/agent-serializer.ts +38 -38
- package/src/agents/discover-agents.ts +121 -118
- package/src/config/config.ts +740 -858
- package/src/config/defaults.ts +96 -96
- package/src/config/drift-detector.ts +211 -211
- package/src/config/markers.ts +327 -327
- package/src/config/resilient-parser.ts +109 -108
- package/src/config/suggestions.ts +74 -74
- package/src/config/types.ts +199 -0
- package/src/extension/async-notifier.ts +123 -89
- package/src/extension/autonomous-policy.ts +169 -169
- package/src/extension/cross-extension-rpc.ts +104 -104
- package/src/extension/help.ts +47 -47
- package/src/extension/import-index.ts +69 -69
- package/src/extension/management.ts +395 -382
- package/src/extension/notification-router.ts +116 -116
- package/src/extension/notification-sink.ts +51 -51
- package/src/extension/project-init.ts +168 -168
- package/src/extension/register.ts +859 -668
- package/src/extension/registration/artifact-cleanup.ts +15 -15
- package/src/extension/registration/command-utils.ts +54 -54
- package/src/extension/registration/commands.ts +559 -452
- package/src/extension/registration/compaction-guard.ts +125 -125
- package/src/extension/registration/subagent-helpers.ts +102 -102
- package/src/extension/registration/subagent-tools.ts +220 -159
- package/src/extension/registration/team-tool.ts +159 -99
- package/src/extension/registration/viewers.ts +29 -0
- package/src/extension/result-watcher.ts +128 -128
- package/src/extension/run-bundle-schema.ts +89 -89
- package/src/extension/run-export.ts +73 -73
- package/src/extension/run-import.ts +84 -84
- package/src/extension/run-index.ts +94 -94
- package/src/extension/run-maintenance.ts +142 -142
- package/src/extension/session-summary.ts +8 -8
- package/src/extension/team-manager-command.ts +96 -96
- package/src/extension/team-recommendation.ts +188 -188
- package/src/extension/team-tool/api.ts +5 -2
- package/src/extension/team-tool/cancel.ts +224 -209
- package/src/extension/team-tool/config-patch.ts +36 -36
- package/src/extension/team-tool/context.ts +60 -60
- package/src/extension/team-tool/doctor.ts +242 -242
- package/src/extension/team-tool/handle-settings.ts +421 -195
- package/src/extension/team-tool/inspect.ts +41 -41
- package/src/extension/team-tool/lifecycle-actions.ts +139 -139
- package/src/extension/team-tool/parallel-dispatch.ts +156 -156
- package/src/extension/team-tool/plan.ts +19 -19
- package/src/extension/team-tool/respond.ts +112 -111
- package/src/extension/team-tool/run.ts +246 -229
- package/src/extension/team-tool/status.ts +110 -110
- package/src/extension/team-tool-types.ts +13 -13
- package/src/extension/team-tool.ts +344 -344
- package/src/extension/tool-result.ts +16 -16
- package/src/extension/validate-resources.ts +77 -77
- package/src/hooks/registry.ts +61 -61
- package/src/hooks/types.ts +40 -40
- package/src/i18n.ts +184 -184
- package/src/observability/correlation.ts +35 -35
- package/src/observability/event-to-metric.ts +68 -68
- package/src/observability/exporters/adapter.ts +30 -30
- package/src/observability/exporters/otlp-exporter.ts +106 -92
- package/src/observability/exporters/prometheus-exporter.ts +54 -54
- package/src/observability/metric-registry.ts +87 -87
- package/src/observability/metric-retention.ts +54 -54
- package/src/observability/metric-sink.ts +81 -56
- package/src/observability/metrics-primitives.ts +167 -167
- package/src/prompt/prompt-runtime.ts +72 -72
- package/src/runtime/adaptive-plan.ts +338 -0
- package/src/runtime/agent-control.ts +169 -169
- package/src/runtime/agent-memory.ts +72 -72
- package/src/runtime/agent-observability.ts +114 -114
- package/src/runtime/async-marker.ts +26 -26
- package/src/runtime/async-runner.ts +153 -153
- package/src/runtime/attention-events.ts +28 -28
- package/src/runtime/auto-resume.ts +100 -100
- package/src/runtime/background-runner.ts +122 -89
- package/src/runtime/cancellation.ts +61 -61
- package/src/runtime/capability-inventory.ts +116 -116
- package/src/runtime/child-pi-pool.ts +68 -0
- package/src/runtime/child-pi.ts +541 -461
- package/src/runtime/code-summary.ts +247 -247
- package/src/runtime/compaction-summary.ts +271 -271
- package/src/runtime/concurrency.ts +58 -58
- package/src/runtime/crash-recovery.ts +317 -301
- package/src/runtime/crew-agent-records.ts +379 -281
- package/src/runtime/crew-agent-runtime.ts +60 -60
- package/src/runtime/cross-extension-rpc.ts +72 -0
- package/src/runtime/custom-tools/irc-tool.ts +201 -201
- package/src/runtime/custom-tools/submit-result-tool.ts +90 -90
- package/src/runtime/deadletter.ts +47 -47
- package/src/runtime/delivery-coordinator.ts +176 -176
- package/src/runtime/delta-conflict.ts +360 -360
- package/src/runtime/diagnostic-export.ts +102 -102
- package/src/runtime/direct-run.ts +35 -35
- package/src/runtime/effectiveness.ts +82 -81
- package/src/runtime/errors/crew-errors.ts +166 -0
- package/src/runtime/event-stream-bridge.ts +92 -92
- package/src/runtime/foreground-control.ts +82 -82
- package/src/runtime/green-contract.ts +46 -46
- package/src/runtime/group-join.ts +234 -106
- package/src/runtime/heartbeat-watcher.ts +145 -124
- package/src/runtime/iteration-hooks.ts +267 -267
- package/src/runtime/live-agent-control.ts +88 -88
- package/src/runtime/live-agent-manager.ts +377 -179
- package/src/runtime/live-control-realtime.ts +36 -36
- package/src/runtime/live-session-runtime.ts +676 -600
- package/src/runtime/loop-gates.ts +129 -129
- package/src/runtime/manifest-cache.ts +263 -263
- package/src/runtime/mcp-proxy.ts +113 -113
- package/src/runtime/metric-parser.ts +40 -40
- package/src/runtime/model-fallback.ts +282 -274
- package/src/runtime/model-resolver.ts +118 -0
- package/src/runtime/output-validator.ts +187 -187
- package/src/runtime/overflow-recovery.ts +175 -175
- package/src/runtime/parallel-research.ts +44 -44
- package/src/runtime/parallel-utils.ts +156 -156
- package/src/runtime/parent-guard.ts +80 -80
- package/src/runtime/phase-progress.ts +217 -217
- package/src/runtime/pi-args.ts +165 -165
- package/src/runtime/pi-json-output.ts +111 -111
- package/src/runtime/pi-spawn.ts +167 -167
- package/src/runtime/policy-engine.ts +79 -79
- package/src/runtime/post-checks.ts +125 -125
- package/src/runtime/post-exit-stdio-guard.ts +86 -86
- package/src/runtime/process-status.ts +97 -73
- package/src/runtime/progress-event-coalescer.ts +43 -43
- package/src/runtime/recovery-recipes.ts +74 -74
- package/src/runtime/retry-executor.ts +81 -81
- package/src/runtime/role-permission.ts +39 -39
- package/src/runtime/run-tracker.ts +99 -0
- package/src/runtime/runtime-policy.ts +21 -0
- package/src/runtime/runtime-resolver.ts +94 -91
- package/src/runtime/scheduler.ts +294 -0
- package/src/runtime/semaphore.ts +131 -131
- package/src/runtime/sensitive-paths.ts +92 -92
- package/src/runtime/session-usage.ts +79 -79
- package/src/runtime/settings-store.ts +103 -0
- package/src/runtime/sidechain-output.ts +29 -29
- package/src/runtime/skill-instructions.ts +222 -222
- package/src/runtime/stale-reconciler.ts +198 -189
- package/src/runtime/streaming-output.ts +47 -0
- package/src/runtime/subagent-manager.ts +404 -400
- package/src/runtime/subprocess-tool-registry.ts +67 -67
- package/src/runtime/task-display.ts +38 -38
- package/src/runtime/task-graph-scheduler.ts +122 -122
- package/src/runtime/task-graph.ts +207 -207
- package/src/runtime/task-output-context.ts +177 -177
- package/src/runtime/task-packet.ts +93 -93
- package/src/runtime/task-quality.ts +207 -207
- package/src/runtime/task-runner/capabilities.ts +78 -78
- package/src/runtime/task-runner/live-executor.ts +131 -113
- package/src/runtime/task-runner/progress.ts +119 -119
- package/src/runtime/task-runner/prompt-builder.ts +139 -139
- package/src/runtime/task-runner/prompt-pipeline.ts +64 -64
- package/src/runtime/task-runner/result-utils.ts +14 -14
- package/src/runtime/task-runner/run-projection.ts +103 -103
- package/src/runtime/task-runner/state-helpers.ts +22 -22
- package/src/runtime/task-runner.ts +469 -459
- package/src/runtime/team-runner.ts +693 -945
- package/src/runtime/usage-tracker.ts +71 -0
- package/src/runtime/worker-heartbeat.ts +21 -21
- package/src/runtime/worker-startup.ts +57 -57
- package/src/runtime/workflow-state.ts +187 -187
- package/src/runtime/yield-handler.ts +190 -190
- package/src/schema/config-schema.ts +172 -168
- package/src/schema/team-tool-schema.ts +126 -126
- package/src/schema/validation-types.ts +151 -148
- package/src/skills/discover-skills.ts +67 -67
- package/src/skills/skill-templates.ts +374 -374
- package/src/state/active-run-registry.ts +227 -191
- package/src/state/artifact-store.ts +130 -129
- package/src/state/atomic-write.ts +262 -195
- package/src/state/blob-store.ts +116 -116
- package/src/state/contracts.ts +111 -111
- package/src/state/event-log-rotation.ts +161 -158
- package/src/state/event-log.ts +383 -303
- package/src/state/event-reconstructor.ts +217 -217
- package/src/state/jsonl-writer.ts +82 -82
- package/src/state/locks.ts +146 -146
- package/src/state/mailbox.ts +446 -405
- package/src/state/state-store.ts +364 -351
- package/src/state/task-claims.ts +44 -44
- package/src/state/types.ts +285 -285
- package/src/state/usage.ts +29 -29
- package/src/subagents/async-entry.ts +1 -1
- package/src/subagents/index.ts +3 -3
- package/src/subagents/live/control.ts +1 -1
- package/src/subagents/live/manager.ts +1 -1
- package/src/subagents/live/realtime.ts +1 -1
- package/src/subagents/live/session-runtime.ts +1 -1
- package/src/subagents/manager.ts +1 -1
- package/src/subagents/spawn.ts +1 -1
- package/src/teams/discover-teams.ts +116 -116
- package/src/teams/team-config.ts +27 -27
- package/src/teams/team-serializer.ts +38 -38
- package/src/types/diff.d.ts +18 -18
- package/src/ui/agent-management-overlay.ts +144 -144
- package/src/ui/crew-widget.ts +487 -370
- package/src/ui/dashboard-panes/agents-pane.ts +109 -28
- package/src/ui/dashboard-panes/cancellation-pane.ts +42 -42
- package/src/ui/dashboard-panes/capability-pane.ts +59 -59
- package/src/ui/dashboard-panes/health-pane.ts +30 -30
- package/src/ui/dashboard-panes/mailbox-pane.ts +35 -35
- package/src/ui/dashboard-panes/progress-pane.ts +30 -30
- package/src/ui/dashboard-panes/transcript-pane.ts +10 -10
- package/src/ui/heartbeat-aggregator.ts +63 -63
- package/src/ui/keybinding-map.ts +97 -94
- package/src/ui/live-conversation-overlay.ts +152 -0
- package/src/ui/live-run-sidebar.ts +180 -180
- package/src/ui/mascot.ts +442 -442
- package/src/ui/overlays/agent-picker-overlay.ts +57 -57
- package/src/ui/overlays/confirm-overlay.ts +58 -58
- package/src/ui/overlays/mailbox-compose-overlay.ts +144 -144
- package/src/ui/overlays/mailbox-compose-preview.ts +63 -63
- package/src/ui/overlays/mailbox-detail-overlay.ts +122 -122
- package/src/ui/pi-ui-compat.ts +57 -57
- package/src/ui/powerbar-publisher.ts +221 -197
- package/src/ui/render-scheduler.ts +216 -143
- package/src/ui/run-action-dispatcher.ts +118 -118
- package/src/ui/run-dashboard.ts +526 -464
- package/src/ui/run-event-bus.ts +208 -208
- package/src/ui/run-snapshot-cache.ts +826 -777
- package/src/ui/settings-overlay.ts +721 -0
- package/src/ui/snapshot-types.ts +86 -70
- package/src/ui/theme-adapter.ts +190 -190
- package/src/ui/tool-progress-formatter.ts +89 -0
- package/src/ui/transcript-cache.ts +94 -94
- package/src/ui/transcript-viewer.ts +335 -335
- package/src/utils/conflict-detect.ts +662 -0
- package/src/utils/file-coalescer.ts +86 -86
- package/src/utils/frontmatter.ts +68 -68
- package/src/utils/fs-watch.ts +88 -31
- package/src/utils/gh-protocol.ts +479 -0
- package/src/utils/ids.ts +17 -17
- package/src/utils/incremental-reader.ts +104 -104
- package/src/utils/internal-error.ts +6 -6
- package/src/utils/names.ts +27 -27
- package/src/utils/paths.ts +102 -63
- package/src/utils/redaction.ts +44 -44
- package/src/utils/safe-paths.ts +47 -47
- package/src/utils/scan-cache.ts +136 -136
- package/src/utils/sse-parser.ts +134 -134
- package/src/utils/task-name-generator.ts +337 -337
- package/src/utils/timings.ts +33 -33
- package/src/utils/visual.ts +243 -198
- package/src/workflows/discover-workflows.ts +139 -139
- package/src/workflows/validate-workflow.ts +40 -40
- package/src/workflows/workflow-config.ts +26 -26
- package/src/workflows/workflow-serializer.ts +32 -32
- package/src/worktree/branch-freshness.ts +45 -45
- package/src/worktree/cleanup.ts +75 -75
- package/src/worktree/worktree-manager.ts +188 -188
- package/teams/default.team.md +12 -12
- package/teams/fast-fix.team.md +11 -11
- package/teams/implementation.team.md +18 -18
- package/teams/parallel-research.team.md +14 -14
- package/teams/research.team.md +11 -11
- package/teams/review.team.md +12 -12
- package/tsconfig.json +19 -19
- package/workflows/default.workflow.md +30 -30
- package/workflows/fast-fix.workflow.md +23 -23
- package/workflows/implementation.workflow.md +43 -43
- package/workflows/parallel-research.workflow.md +46 -46
- package/workflows/research.workflow.md +22 -22
- package/workflows/review.workflow.md +30 -30
- package/skills/task-packet/SKILL.md +0 -28
- package/skills/verify-evidence/SKILL.md +0 -27
|
@@ -1,43 +1,43 @@
|
|
|
1
|
-
export interface ProgressEventSummary {
|
|
2
|
-
eventType: string;
|
|
3
|
-
currentTool?: string;
|
|
4
|
-
toolCount?: number;
|
|
5
|
-
tokens?: number;
|
|
6
|
-
turns?: number;
|
|
7
|
-
activityState?: string;
|
|
8
|
-
lastActivityAt?: string;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
export interface ProgressEventCoalesceDecision {
|
|
12
|
-
shouldAppend: boolean;
|
|
13
|
-
reason: string;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export interface ProgressEventCoalesceInput {
|
|
17
|
-
previous?: ProgressEventSummary;
|
|
18
|
-
next: ProgressEventSummary;
|
|
19
|
-
nowMs: number;
|
|
20
|
-
lastAppendMs?: number;
|
|
21
|
-
minIntervalMs: number;
|
|
22
|
-
force?: boolean;
|
|
23
|
-
tokenThreshold?: number;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
const DEFAULT_TOKEN_THRESHOLD = 256;
|
|
27
|
-
|
|
28
|
-
function numericIncrease(previous: number | undefined, next: number | undefined): number {
|
|
29
|
-
return next !== undefined && previous !== undefined ? next - previous : next !== undefined ? next : 0;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export function shouldAppendProgressEventUpdate(input: ProgressEventCoalesceInput): ProgressEventCoalesceDecision {
|
|
33
|
-
if (input.force) return { shouldAppend: true, reason: "force" };
|
|
34
|
-
if (!input.previous) return { shouldAppend: true, reason: "first" };
|
|
35
|
-
if (input.previous.activityState !== input.next.activityState) return { shouldAppend: true, reason: "activity_changed" };
|
|
36
|
-
if (input.previous.currentTool !== input.next.currentTool) return { shouldAppend: true, reason: "tool_changed" };
|
|
37
|
-
if (numericIncrease(input.previous.toolCount, input.next.toolCount) > 0) return { shouldAppend: true, reason: "tool_count_increased" };
|
|
38
|
-
if (numericIncrease(input.previous.turns, input.next.turns) > 0) return { shouldAppend: true, reason: "turns_increased" };
|
|
39
|
-
const tokenIncrease = numericIncrease(input.previous.tokens, input.next.tokens);
|
|
40
|
-
if (tokenIncrease >= (input.tokenThreshold ?? DEFAULT_TOKEN_THRESHOLD)) return { shouldAppend: true, reason: "tokens_increased" };
|
|
41
|
-
if (input.lastAppendMs === undefined || input.nowMs - input.lastAppendMs >= input.minIntervalMs) return { shouldAppend: true, reason: "interval" };
|
|
42
|
-
return { shouldAppend: false, reason: "coalesced" };
|
|
43
|
-
}
|
|
1
|
+
export interface ProgressEventSummary {
|
|
2
|
+
eventType: string;
|
|
3
|
+
currentTool?: string;
|
|
4
|
+
toolCount?: number;
|
|
5
|
+
tokens?: number;
|
|
6
|
+
turns?: number;
|
|
7
|
+
activityState?: string;
|
|
8
|
+
lastActivityAt?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface ProgressEventCoalesceDecision {
|
|
12
|
+
shouldAppend: boolean;
|
|
13
|
+
reason: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface ProgressEventCoalesceInput {
|
|
17
|
+
previous?: ProgressEventSummary;
|
|
18
|
+
next: ProgressEventSummary;
|
|
19
|
+
nowMs: number;
|
|
20
|
+
lastAppendMs?: number;
|
|
21
|
+
minIntervalMs: number;
|
|
22
|
+
force?: boolean;
|
|
23
|
+
tokenThreshold?: number;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const DEFAULT_TOKEN_THRESHOLD = 256;
|
|
27
|
+
|
|
28
|
+
function numericIncrease(previous: number | undefined, next: number | undefined): number {
|
|
29
|
+
return next !== undefined && previous !== undefined ? next - previous : next !== undefined ? next : 0;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function shouldAppendProgressEventUpdate(input: ProgressEventCoalesceInput): ProgressEventCoalesceDecision {
|
|
33
|
+
if (input.force) return { shouldAppend: true, reason: "force" };
|
|
34
|
+
if (!input.previous) return { shouldAppend: true, reason: "first" };
|
|
35
|
+
if (input.previous.activityState !== input.next.activityState) return { shouldAppend: true, reason: "activity_changed" };
|
|
36
|
+
if (input.previous.currentTool !== input.next.currentTool) return { shouldAppend: true, reason: "tool_changed" };
|
|
37
|
+
if (numericIncrease(input.previous.toolCount, input.next.toolCount) > 0) return { shouldAppend: true, reason: "tool_count_increased" };
|
|
38
|
+
if (numericIncrease(input.previous.turns, input.next.turns) > 0) return { shouldAppend: true, reason: "turns_increased" };
|
|
39
|
+
const tokenIncrease = numericIncrease(input.previous.tokens, input.next.tokens);
|
|
40
|
+
if (tokenIncrease >= (input.tokenThreshold ?? DEFAULT_TOKEN_THRESHOLD)) return { shouldAppend: true, reason: "tokens_increased" };
|
|
41
|
+
if (input.lastAppendMs === undefined || input.nowMs - input.lastAppendMs >= input.minIntervalMs) return { shouldAppend: true, reason: "interval" };
|
|
42
|
+
return { shouldAppend: false, reason: "coalesced" };
|
|
43
|
+
}
|
|
@@ -1,74 +1,74 @@
|
|
|
1
|
-
import type { PolicyDecision, PolicyDecisionReason } from "../state/types.ts";
|
|
2
|
-
|
|
3
|
-
export type FailureScenario = "trust_prompt_unresolved" | "prompt_misdelivery" | "stale_branch" | "compile_red_cross_crate" | "mcp_handshake_failure" | "partial_plugin_startup" | "provider_failure" | "task_failed" | "worker_stale" | "green_unsatisfied";
|
|
4
|
-
export type RecoveryStep = "accept_trust_prompt" | "redirect_prompt_to_agent" | "rebase_branch" | "clean_build" | "retry_mcp_handshake" | "restart_plugin" | "restart_worker" | "rerun_task" | "collect_verification_evidence" | "escalate_to_human";
|
|
5
|
-
export type RecoveryResultState = "planned" | "skipped" | "escalation_required";
|
|
6
|
-
|
|
7
|
-
export interface RecoveryRecipe {
|
|
8
|
-
scenario: FailureScenario;
|
|
9
|
-
steps: RecoveryStep[];
|
|
10
|
-
maxAttempts: number;
|
|
11
|
-
escalationPolicy: "alert_human" | "log_and_continue" | "abort";
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export interface RecoveryLedgerEntry {
|
|
15
|
-
scenario: FailureScenario;
|
|
16
|
-
taskId?: string;
|
|
17
|
-
decisionReason: PolicyDecisionReason;
|
|
18
|
-
attempt: number;
|
|
19
|
-
state: RecoveryResultState;
|
|
20
|
-
steps: RecoveryStep[];
|
|
21
|
-
message: string;
|
|
22
|
-
createdAt: string;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export interface RecoveryLedger {
|
|
26
|
-
entries: RecoveryLedgerEntry[];
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
export function scenarioForPolicyReason(reason: PolicyDecisionReason): FailureScenario {
|
|
30
|
-
switch (reason) {
|
|
31
|
-
case "branch_stale": return "stale_branch";
|
|
32
|
-
case "worker_stale": return "worker_stale";
|
|
33
|
-
case "green_unsatisfied": return "green_unsatisfied";
|
|
34
|
-
case "task_failed": return "task_failed";
|
|
35
|
-
default: return "provider_failure";
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
export function recipeFor(scenario: FailureScenario): RecoveryRecipe {
|
|
40
|
-
switch (scenario) {
|
|
41
|
-
case "trust_prompt_unresolved": return { scenario, steps: ["accept_trust_prompt"], maxAttempts: 1, escalationPolicy: "alert_human" };
|
|
42
|
-
case "prompt_misdelivery": return { scenario, steps: ["redirect_prompt_to_agent"], maxAttempts: 1, escalationPolicy: "alert_human" };
|
|
43
|
-
case "stale_branch": return { scenario, steps: ["rebase_branch", "clean_build"], maxAttempts: 1, escalationPolicy: "alert_human" };
|
|
44
|
-
case "compile_red_cross_crate": return { scenario, steps: ["clean_build"], maxAttempts: 1, escalationPolicy: "alert_human" };
|
|
45
|
-
case "mcp_handshake_failure": return { scenario, steps: ["retry_mcp_handshake"], maxAttempts: 1, escalationPolicy: "abort" };
|
|
46
|
-
case "partial_plugin_startup": return { scenario, steps: ["restart_plugin", "retry_mcp_handshake"], maxAttempts: 1, escalationPolicy: "log_and_continue" };
|
|
47
|
-
case "worker_stale": return { scenario, steps: ["restart_worker"], maxAttempts: 1, escalationPolicy: "alert_human" };
|
|
48
|
-
case "green_unsatisfied": return { scenario, steps: ["collect_verification_evidence"], maxAttempts: 1, escalationPolicy: "alert_human" };
|
|
49
|
-
case "task_failed": return { scenario, steps: ["rerun_task"], maxAttempts: 1, escalationPolicy: "alert_human" };
|
|
50
|
-
case "provider_failure": return { scenario, steps: ["restart_worker"], maxAttempts: 1, escalationPolicy: "alert_human" };
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
export function buildRecoveryLedger(decisions: PolicyDecision[], previous: RecoveryLedger = { entries: [] }): RecoveryLedger {
|
|
55
|
-
const entries = [...previous.entries];
|
|
56
|
-
for (const item of decisions) {
|
|
57
|
-
if (!["retry", "escalate", "block"].includes(item.action)) continue;
|
|
58
|
-
const scenario = scenarioForPolicyReason(item.reason);
|
|
59
|
-
const recipe = recipeFor(scenario);
|
|
60
|
-
const priorAttempts = entries.filter((entry) => entry.scenario === scenario && entry.taskId === item.taskId).length;
|
|
61
|
-
const attempt = priorAttempts + 1;
|
|
62
|
-
entries.push({
|
|
63
|
-
scenario,
|
|
64
|
-
taskId: item.taskId,
|
|
65
|
-
decisionReason: item.reason,
|
|
66
|
-
attempt,
|
|
67
|
-
state: attempt <= recipe.maxAttempts && item.action !== "block" ? "planned" : "escalation_required",
|
|
68
|
-
steps: attempt <= recipe.maxAttempts ? recipe.steps : ["escalate_to_human"],
|
|
69
|
-
message: item.message,
|
|
70
|
-
createdAt: new Date().toISOString(),
|
|
71
|
-
});
|
|
72
|
-
}
|
|
73
|
-
return { entries };
|
|
74
|
-
}
|
|
1
|
+
import type { PolicyDecision, PolicyDecisionReason } from "../state/types.ts";
|
|
2
|
+
|
|
3
|
+
export type FailureScenario = "trust_prompt_unresolved" | "prompt_misdelivery" | "stale_branch" | "compile_red_cross_crate" | "mcp_handshake_failure" | "partial_plugin_startup" | "provider_failure" | "task_failed" | "worker_stale" | "green_unsatisfied";
|
|
4
|
+
export type RecoveryStep = "accept_trust_prompt" | "redirect_prompt_to_agent" | "rebase_branch" | "clean_build" | "retry_mcp_handshake" | "restart_plugin" | "restart_worker" | "rerun_task" | "collect_verification_evidence" | "escalate_to_human";
|
|
5
|
+
export type RecoveryResultState = "planned" | "skipped" | "escalation_required";
|
|
6
|
+
|
|
7
|
+
export interface RecoveryRecipe {
|
|
8
|
+
scenario: FailureScenario;
|
|
9
|
+
steps: RecoveryStep[];
|
|
10
|
+
maxAttempts: number;
|
|
11
|
+
escalationPolicy: "alert_human" | "log_and_continue" | "abort";
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface RecoveryLedgerEntry {
|
|
15
|
+
scenario: FailureScenario;
|
|
16
|
+
taskId?: string;
|
|
17
|
+
decisionReason: PolicyDecisionReason;
|
|
18
|
+
attempt: number;
|
|
19
|
+
state: RecoveryResultState;
|
|
20
|
+
steps: RecoveryStep[];
|
|
21
|
+
message: string;
|
|
22
|
+
createdAt: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface RecoveryLedger {
|
|
26
|
+
entries: RecoveryLedgerEntry[];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function scenarioForPolicyReason(reason: PolicyDecisionReason): FailureScenario {
|
|
30
|
+
switch (reason) {
|
|
31
|
+
case "branch_stale": return "stale_branch";
|
|
32
|
+
case "worker_stale": return "worker_stale";
|
|
33
|
+
case "green_unsatisfied": return "green_unsatisfied";
|
|
34
|
+
case "task_failed": return "task_failed";
|
|
35
|
+
default: return "provider_failure";
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function recipeFor(scenario: FailureScenario): RecoveryRecipe {
|
|
40
|
+
switch (scenario) {
|
|
41
|
+
case "trust_prompt_unresolved": return { scenario, steps: ["accept_trust_prompt"], maxAttempts: 1, escalationPolicy: "alert_human" };
|
|
42
|
+
case "prompt_misdelivery": return { scenario, steps: ["redirect_prompt_to_agent"], maxAttempts: 1, escalationPolicy: "alert_human" };
|
|
43
|
+
case "stale_branch": return { scenario, steps: ["rebase_branch", "clean_build"], maxAttempts: 1, escalationPolicy: "alert_human" };
|
|
44
|
+
case "compile_red_cross_crate": return { scenario, steps: ["clean_build"], maxAttempts: 1, escalationPolicy: "alert_human" };
|
|
45
|
+
case "mcp_handshake_failure": return { scenario, steps: ["retry_mcp_handshake"], maxAttempts: 1, escalationPolicy: "abort" };
|
|
46
|
+
case "partial_plugin_startup": return { scenario, steps: ["restart_plugin", "retry_mcp_handshake"], maxAttempts: 1, escalationPolicy: "log_and_continue" };
|
|
47
|
+
case "worker_stale": return { scenario, steps: ["restart_worker"], maxAttempts: 1, escalationPolicy: "alert_human" };
|
|
48
|
+
case "green_unsatisfied": return { scenario, steps: ["collect_verification_evidence"], maxAttempts: 1, escalationPolicy: "alert_human" };
|
|
49
|
+
case "task_failed": return { scenario, steps: ["rerun_task"], maxAttempts: 1, escalationPolicy: "alert_human" };
|
|
50
|
+
case "provider_failure": return { scenario, steps: ["restart_worker"], maxAttempts: 1, escalationPolicy: "alert_human" };
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function buildRecoveryLedger(decisions: PolicyDecision[], previous: RecoveryLedger = { entries: [] }): RecoveryLedger {
|
|
55
|
+
const entries = [...previous.entries];
|
|
56
|
+
for (const item of decisions) {
|
|
57
|
+
if (!["retry", "escalate", "block"].includes(item.action)) continue;
|
|
58
|
+
const scenario = scenarioForPolicyReason(item.reason);
|
|
59
|
+
const recipe = recipeFor(scenario);
|
|
60
|
+
const priorAttempts = entries.filter((entry) => entry.scenario === scenario && entry.taskId === item.taskId).length;
|
|
61
|
+
const attempt = priorAttempts + 1;
|
|
62
|
+
entries.push({
|
|
63
|
+
scenario,
|
|
64
|
+
taskId: item.taskId,
|
|
65
|
+
decisionReason: item.reason,
|
|
66
|
+
attempt,
|
|
67
|
+
state: attempt <= recipe.maxAttempts && item.action !== "block" ? "planned" : "escalation_required",
|
|
68
|
+
steps: attempt <= recipe.maxAttempts ? recipe.steps : ["escalate_to_human"],
|
|
69
|
+
message: item.message,
|
|
70
|
+
createdAt: new Date().toISOString(),
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
return { entries };
|
|
74
|
+
}
|
|
@@ -1,81 +1,81 @@
|
|
|
1
|
-
import { sleep } from "../utils/sleep.ts";
|
|
2
|
-
import { throwIfCancelled } from "./cancellation.ts";
|
|
3
|
-
|
|
4
|
-
export interface RetryPolicy {
|
|
5
|
-
maxAttempts: number;
|
|
6
|
-
backoffMs: number;
|
|
7
|
-
jitterRatio: number;
|
|
8
|
-
exponentialFactor: number;
|
|
9
|
-
retryableErrors?: string[];
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
export interface RetryAttemptInfo {
|
|
13
|
-
attempt: number;
|
|
14
|
-
attemptId: string;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export interface RetryHooks {
|
|
18
|
-
onAttemptFailed?: (attempt: number, error: Error, nextDelayMs: number, info: RetryAttemptInfo) => void;
|
|
19
|
-
onRetryGivenUp?: (attempts: number, error: Error, info: RetryAttemptInfo) => void;
|
|
20
|
-
attemptId?: (attempt: number) => string;
|
|
21
|
-
signal?: AbortSignal;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export const DEFAULT_RETRY_POLICY: RetryPolicy = { maxAttempts: 3, backoffMs: 1000, jitterRatio: 0.3, exponentialFactor: 2 };
|
|
25
|
-
|
|
26
|
-
function asError(error: unknown): Error {
|
|
27
|
-
return error instanceof Error ? error : new Error(String(error));
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
function globToRegex(pattern: string): RegExp {
|
|
31
|
-
const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
|
|
32
|
-
return new RegExp(`^${escaped}$`, "i");
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
function isRetryable(error: Error, policy: RetryPolicy): boolean {
|
|
36
|
-
const patterns = policy.retryableErrors ?? [];
|
|
37
|
-
if (!patterns.length) return true;
|
|
38
|
-
return patterns.some((pattern) => globToRegex(pattern).test(error.message));
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
export function calculateRetryDelay(attempt: number, policy: RetryPolicy = DEFAULT_RETRY_POLICY, random = Math.random): number {
|
|
42
|
-
const base = policy.backoffMs * Math.pow(policy.exponentialFactor, Math.max(0, attempt - 1));
|
|
43
|
-
const jitter = (random() * 2 - 1) * policy.jitterRatio * base;
|
|
44
|
-
return Math.max(0, base + jitter);
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
function retryAttemptInfo(attempt: number, hooks: RetryHooks): RetryAttemptInfo {
|
|
48
|
-
return { attempt, attemptId: hooks.attemptId?.(attempt) ?? `retry_attempt_${attempt}` };
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
export async function executeWithRetry<T>(fn: (attempt: number, info: RetryAttemptInfo) => Promise<T>, policy: RetryPolicy = DEFAULT_RETRY_POLICY, hooks: RetryHooks = {}): Promise<T> {
|
|
52
|
-
const normalized: RetryPolicy = { ...DEFAULT_RETRY_POLICY, ...policy, maxAttempts: Math.max(1, policy.maxAttempts ?? DEFAULT_RETRY_POLICY.maxAttempts) };
|
|
53
|
-
let lastError: Error | undefined;
|
|
54
|
-
for (let attempt = 1; attempt <= normalized.maxAttempts; attempt += 1) {
|
|
55
|
-
throwIfCancelled(hooks.signal);
|
|
56
|
-
const info = retryAttemptInfo(attempt, hooks);
|
|
57
|
-
try {
|
|
58
|
-
return await fn(attempt, info);
|
|
59
|
-
} catch (error) {
|
|
60
|
-
lastError = asError(error);
|
|
61
|
-
// Never retry if aborted — sleep() would immediately reject on every attempt.
|
|
62
|
-
if (hooks.signal?.aborted) {
|
|
63
|
-
hooks.onRetryGivenUp?.(attempt, lastError, info);
|
|
64
|
-
throw lastError;
|
|
65
|
-
}
|
|
66
|
-
if (attempt >= normalized.maxAttempts || !isRetryable(lastError, normalized)) {
|
|
67
|
-
hooks.onRetryGivenUp?.(attempt, lastError, info);
|
|
68
|
-
throw lastError;
|
|
69
|
-
}
|
|
70
|
-
const delay = calculateRetryDelay(attempt, normalized);
|
|
71
|
-
hooks.onAttemptFailed?.(attempt, lastError, delay, info);
|
|
72
|
-
try {
|
|
73
|
-
await sleep(delay, hooks.signal);
|
|
74
|
-
} catch (sleepError) {
|
|
75
|
-
if (hooks.signal?.aborted) throwIfCancelled(hooks.signal);
|
|
76
|
-
throw sleepError;
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
throw lastError ?? new Error("Retry failed without error.");
|
|
81
|
-
}
|
|
1
|
+
import { sleep } from "../utils/sleep.ts";
|
|
2
|
+
import { throwIfCancelled } from "./cancellation.ts";
|
|
3
|
+
|
|
4
|
+
export interface RetryPolicy {
|
|
5
|
+
maxAttempts: number;
|
|
6
|
+
backoffMs: number;
|
|
7
|
+
jitterRatio: number;
|
|
8
|
+
exponentialFactor: number;
|
|
9
|
+
retryableErrors?: string[];
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface RetryAttemptInfo {
|
|
13
|
+
attempt: number;
|
|
14
|
+
attemptId: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface RetryHooks {
|
|
18
|
+
onAttemptFailed?: (attempt: number, error: Error, nextDelayMs: number, info: RetryAttemptInfo) => void;
|
|
19
|
+
onRetryGivenUp?: (attempts: number, error: Error, info: RetryAttemptInfo) => void;
|
|
20
|
+
attemptId?: (attempt: number) => string;
|
|
21
|
+
signal?: AbortSignal;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export const DEFAULT_RETRY_POLICY: RetryPolicy = { maxAttempts: 3, backoffMs: 1000, jitterRatio: 0.3, exponentialFactor: 2 };
|
|
25
|
+
|
|
26
|
+
function asError(error: unknown): Error {
|
|
27
|
+
return error instanceof Error ? error : new Error(String(error));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function globToRegex(pattern: string): RegExp {
|
|
31
|
+
const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
|
|
32
|
+
return new RegExp(`^${escaped}$`, "i");
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function isRetryable(error: Error, policy: RetryPolicy): boolean {
|
|
36
|
+
const patterns = policy.retryableErrors ?? [];
|
|
37
|
+
if (!patterns.length) return true;
|
|
38
|
+
return patterns.some((pattern) => globToRegex(pattern).test(error.message));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function calculateRetryDelay(attempt: number, policy: RetryPolicy = DEFAULT_RETRY_POLICY, random = Math.random): number {
|
|
42
|
+
const base = policy.backoffMs * Math.pow(policy.exponentialFactor, Math.max(0, attempt - 1));
|
|
43
|
+
const jitter = (random() * 2 - 1) * policy.jitterRatio * base;
|
|
44
|
+
return Math.max(0, base + jitter);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function retryAttemptInfo(attempt: number, hooks: RetryHooks): RetryAttemptInfo {
|
|
48
|
+
return { attempt, attemptId: hooks.attemptId?.(attempt) ?? `retry_attempt_${attempt}` };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export async function executeWithRetry<T>(fn: (attempt: number, info: RetryAttemptInfo) => Promise<T>, policy: RetryPolicy = DEFAULT_RETRY_POLICY, hooks: RetryHooks = {}): Promise<T> {
|
|
52
|
+
const normalized: RetryPolicy = { ...DEFAULT_RETRY_POLICY, ...policy, maxAttempts: Math.max(1, policy.maxAttempts ?? DEFAULT_RETRY_POLICY.maxAttempts) };
|
|
53
|
+
let lastError: Error | undefined;
|
|
54
|
+
for (let attempt = 1; attempt <= normalized.maxAttempts; attempt += 1) {
|
|
55
|
+
throwIfCancelled(hooks.signal);
|
|
56
|
+
const info = retryAttemptInfo(attempt, hooks);
|
|
57
|
+
try {
|
|
58
|
+
return await fn(attempt, info);
|
|
59
|
+
} catch (error) {
|
|
60
|
+
lastError = asError(error);
|
|
61
|
+
// Never retry if aborted — sleep() would immediately reject on every attempt.
|
|
62
|
+
if (hooks.signal?.aborted) {
|
|
63
|
+
hooks.onRetryGivenUp?.(attempt, lastError, info);
|
|
64
|
+
throw lastError;
|
|
65
|
+
}
|
|
66
|
+
if (attempt >= normalized.maxAttempts || !isRetryable(lastError, normalized)) {
|
|
67
|
+
hooks.onRetryGivenUp?.(attempt, lastError, info);
|
|
68
|
+
throw lastError;
|
|
69
|
+
}
|
|
70
|
+
const delay = calculateRetryDelay(attempt, normalized);
|
|
71
|
+
hooks.onAttemptFailed?.(attempt, lastError, delay, info);
|
|
72
|
+
try {
|
|
73
|
+
await sleep(delay, hooks.signal);
|
|
74
|
+
} catch (sleepError) {
|
|
75
|
+
if (hooks.signal?.aborted) throwIfCancelled(hooks.signal);
|
|
76
|
+
throw sleepError;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
throw lastError ?? new Error("Retry failed without error.");
|
|
81
|
+
}
|
|
@@ -1,39 +1,39 @@
|
|
|
1
|
-
export type RolePermissionMode = "read_only" | "workspace_write" | "danger_full_access" | "explicit_confirm";
|
|
2
|
-
|
|
3
|
-
const READ_ONLY_ROLES = new Set(["explorer", "reviewer", "security-reviewer", "verifier", "analyst", "critic", "planner", "writer"]);
|
|
4
|
-
const WRITE_ROLES = new Set(["executor", "test-engineer"]);
|
|
5
|
-
const READ_ONLY_COMMANDS = new Set(["cat", "head", "tail", "less", "more", "wc", "ls", "find", "grep", "rg", "awk", "sed", "echo", "printf", "which", "where", "whoami", "pwd", "env", "printenv", "date", "df", "du", "uname", "file", "stat", "diff", "sort", "uniq", "tr", "cut", "paste", "test", "true", "false", "type", "readlink", "realpath", "basename", "dirname", "sha256sum", "md5sum", "xxd", "hexdump", "od", "strings", "tree", "jq", "git", "gh"]);
|
|
6
|
-
|
|
7
|
-
export interface PermissionCheckResult {
|
|
8
|
-
allowed: boolean;
|
|
9
|
-
mode: RolePermissionMode;
|
|
10
|
-
reason?: string;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export function permissionForRole(role: string): RolePermissionMode {
|
|
14
|
-
if (READ_ONLY_ROLES.has(role)) return "read_only";
|
|
15
|
-
if (WRITE_ROLES.has(role)) return "workspace_write";
|
|
16
|
-
return "workspace_write";
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export function isReadOnlyCommand(command: string): boolean {
|
|
20
|
-
const first = command.trim().split(/\s+/)[0]?.split(/[\\/]/).pop() ?? "";
|
|
21
|
-
return READ_ONLY_COMMANDS.has(first) && !/\s(-i|--in-place)\b|\s>{1,2}\s|\brm\b|\bmv\b|\bcp\b|\b(?:npm|pnpm|yarn|bun)\s+(install|add|ci|remove)\b|\bgit\s+(commit|push|merge|rebase|reset|checkout|clean)\b/.test(command);
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export function checkRolePermission(role: string, command: string): PermissionCheckResult {
|
|
25
|
-
const mode = permissionForRole(role);
|
|
26
|
-
if (mode === "read_only" && !isReadOnlyCommand(command)) return { allowed: false, mode, reason: `Role '${role}' is read-only and command may modify state.` };
|
|
27
|
-
return { allowed: true, mode };
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export function currentCrewRole(env: NodeJS.ProcessEnv = process.env): string | undefined {
|
|
31
|
-
return env.PI_CREW_ROLE?.trim() || env.PI_TEAMS_ROLE?.trim() || undefined;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
export function checkSubagentSpawnPermission(role: string | undefined): PermissionCheckResult {
|
|
35
|
-
if (!role) return { allowed: true, mode: "workspace_write" };
|
|
36
|
-
const mode = permissionForRole(role);
|
|
37
|
-
if (mode === "read_only") return { allowed: false, mode, reason: `Role '${role}' is read-only and cannot spawn additional subagents.` };
|
|
38
|
-
return { allowed: true, mode };
|
|
39
|
-
}
|
|
1
|
+
export type RolePermissionMode = "read_only" | "workspace_write" | "danger_full_access" | "explicit_confirm";
|
|
2
|
+
|
|
3
|
+
const READ_ONLY_ROLES = new Set(["explorer", "reviewer", "security-reviewer", "verifier", "analyst", "critic", "planner", "writer"]);
|
|
4
|
+
const WRITE_ROLES = new Set(["executor", "test-engineer"]);
|
|
5
|
+
const READ_ONLY_COMMANDS = new Set(["cat", "head", "tail", "less", "more", "wc", "ls", "find", "grep", "rg", "awk", "sed", "echo", "printf", "which", "where", "whoami", "pwd", "env", "printenv", "date", "df", "du", "uname", "file", "stat", "diff", "sort", "uniq", "tr", "cut", "paste", "test", "true", "false", "type", "readlink", "realpath", "basename", "dirname", "sha256sum", "md5sum", "xxd", "hexdump", "od", "strings", "tree", "jq", "git", "gh"]);
|
|
6
|
+
|
|
7
|
+
export interface PermissionCheckResult {
|
|
8
|
+
allowed: boolean;
|
|
9
|
+
mode: RolePermissionMode;
|
|
10
|
+
reason?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function permissionForRole(role: string): RolePermissionMode {
|
|
14
|
+
if (READ_ONLY_ROLES.has(role)) return "read_only";
|
|
15
|
+
if (WRITE_ROLES.has(role)) return "workspace_write";
|
|
16
|
+
return "workspace_write";
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function isReadOnlyCommand(command: string): boolean {
|
|
20
|
+
const first = command.trim().split(/\s+/)[0]?.split(/[\\/]/).pop() ?? "";
|
|
21
|
+
return READ_ONLY_COMMANDS.has(first) && !/\s(-i|--in-place)\b|\s>{1,2}\s|\brm\b|\bmv\b|\bcp\b|\b(?:npm|pnpm|yarn|bun)\s+(install|add|ci|remove)\b|\bgit\s+(commit|push|merge|rebase|reset|checkout|clean)\b/.test(command);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function checkRolePermission(role: string, command: string): PermissionCheckResult {
|
|
25
|
+
const mode = permissionForRole(role);
|
|
26
|
+
if (mode === "read_only" && !isReadOnlyCommand(command)) return { allowed: false, mode, reason: `Role '${role}' is read-only and command may modify state.` };
|
|
27
|
+
return { allowed: true, mode };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function currentCrewRole(env: NodeJS.ProcessEnv = process.env): string | undefined {
|
|
31
|
+
return env.PI_CREW_ROLE?.trim() || env.PI_TEAMS_ROLE?.trim() || undefined;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function checkSubagentSpawnPermission(role: string | undefined): PermissionCheckResult {
|
|
35
|
+
if (!role) return { allowed: true, mode: "workspace_write" };
|
|
36
|
+
const mode = permissionForRole(role);
|
|
37
|
+
if (mode === "read_only") return { allowed: false, mode, reason: `Role '${role}' is read-only and cannot spawn additional subagents.` };
|
|
38
|
+
return { allowed: true, mode };
|
|
39
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import type { TeamRunManifest, TeamTaskState } from "../state/types.ts";
|
|
2
|
+
import { loadRunManifestById } from "../state/state-store.ts";
|
|
3
|
+
import { isFinishedRunStatus } from "./process-status.ts";
|
|
4
|
+
|
|
5
|
+
export interface ActiveRunPromise {
|
|
6
|
+
promise: Promise<{ manifest: TeamRunManifest; tasks: TeamTaskState[] }>;
|
|
7
|
+
resolve: (value: { manifest: TeamRunManifest; tasks: TeamTaskState[] }) => void;
|
|
8
|
+
reject: (reason: unknown) => void;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const activeRunPromises = new Map<string, ActiveRunPromise>();
|
|
12
|
+
|
|
13
|
+
export function registerRunPromise(runId: string): ActiveRunPromise {
|
|
14
|
+
let resolve!: (value: { manifest: TeamRunManifest; tasks: TeamTaskState[] }) => void;
|
|
15
|
+
let reject!: (reason: unknown) => void;
|
|
16
|
+
const promise = new Promise<{ manifest: TeamRunManifest; tasks: TeamTaskState[] }>((res, rej) => {
|
|
17
|
+
resolve = res;
|
|
18
|
+
reject = rej;
|
|
19
|
+
});
|
|
20
|
+
const entry: ActiveRunPromise = { promise, resolve, reject };
|
|
21
|
+
activeRunPromises.set(runId, entry);
|
|
22
|
+
return entry;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function resolveRunPromise(runId: string, result: { manifest: TeamRunManifest; tasks: TeamTaskState[] }): void {
|
|
26
|
+
const entry = activeRunPromises.get(runId);
|
|
27
|
+
if (entry) {
|
|
28
|
+
entry.resolve(result);
|
|
29
|
+
activeRunPromises.delete(runId);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function rejectRunPromise(runId: string, reason: unknown): void {
|
|
34
|
+
const entry = activeRunPromises.get(runId);
|
|
35
|
+
if (entry) {
|
|
36
|
+
entry.reject(reason);
|
|
37
|
+
activeRunPromises.delete(runId);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Wait for a team run to reach a terminal status.
|
|
43
|
+
* - If the run is already finished on disk, returns immediately.
|
|
44
|
+
* - If a foreground promise is registered for this runId, awaits it.
|
|
45
|
+
* - Otherwise falls back to lightweight fs.watchFile-based waiting.
|
|
46
|
+
*/
|
|
47
|
+
export async function waitForRun(
|
|
48
|
+
runId: string,
|
|
49
|
+
cwd: string,
|
|
50
|
+
options: { timeoutMs?: number; pollIntervalMs?: number } = {},
|
|
51
|
+
): Promise<{ manifest: TeamRunManifest; tasks: TeamTaskState[] }> {
|
|
52
|
+
const { timeoutMs = 300_000, pollIntervalMs = 500 } = options;
|
|
53
|
+
const deadline = Date.now() + timeoutMs;
|
|
54
|
+
|
|
55
|
+
// Fast path: already terminal on disk
|
|
56
|
+
const loaded = loadRunManifestById(cwd, runId);
|
|
57
|
+
if (loaded && isFinishedRunStatus(loaded.manifest.status)) {
|
|
58
|
+
return loaded;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Medium path: foreground promise registered in this process
|
|
62
|
+
const entry = activeRunPromises.get(runId);
|
|
63
|
+
if (entry) {
|
|
64
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
65
|
+
const timeoutPromise = new Promise<never>((_, reject) => {
|
|
66
|
+
timer = setTimeout(() => reject(new Error(`waitForRun timed out after ${timeoutMs}ms`)), timeoutMs);
|
|
67
|
+
});
|
|
68
|
+
try {
|
|
69
|
+
return await Promise.race([entry.promise, timeoutPromise]);
|
|
70
|
+
} finally {
|
|
71
|
+
if (timer) clearTimeout(timer);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Slow path: background run — poll with exponential backoff capped at pollIntervalMs
|
|
76
|
+
let attempt = 0;
|
|
77
|
+
while (Date.now() < deadline) {
|
|
78
|
+
const fresh = loadRunManifestById(cwd, runId);
|
|
79
|
+
if (fresh && isFinishedRunStatus(fresh.manifest.status)) {
|
|
80
|
+
return fresh;
|
|
81
|
+
}
|
|
82
|
+
const delay = Math.min(pollIntervalMs, 50 * 2 ** Math.min(attempt, 6)); // max ~3.2s
|
|
83
|
+
await new Promise((r) => setTimeout(r, delay));
|
|
84
|
+
attempt++;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
throw new Error(`waitForRun timed out after ${timeoutMs}ms`);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function hasActiveRunPromise(runId: string): boolean {
|
|
91
|
+
return activeRunPromises.has(runId);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function clearRunPromisesForTest(): void {
|
|
95
|
+
for (const entry of activeRunPromises.values()) {
|
|
96
|
+
entry.reject(new Error("Cleared by test"));
|
|
97
|
+
}
|
|
98
|
+
activeRunPromises.clear();
|
|
99
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { CrewRuntimeConfig } from "../config/config.ts";
|
|
2
|
+
import type { CrewRuntimeKind } from "./crew-agent-runtime.ts";
|
|
3
|
+
import { currentCrewDepth } from "./pi-args.ts";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Resolve the effective runtime kind for a given task role using isolation policy.
|
|
7
|
+
* - scaffold is never overridden — scaffold stays scaffold.
|
|
8
|
+
* - If already nested (PI_CREW_DEPTH > 0), force child-process to avoid live-session nesting issues.
|
|
9
|
+
* - If the role appears in `isolationPolicy.isolatedRoles`, use child-process (crash isolation).
|
|
10
|
+
* - Otherwise, use `isolationPolicy.defaultRuntime` when configured, then fall back to globalKind.
|
|
11
|
+
*/
|
|
12
|
+
export function resolveTaskRuntimeKind(globalKind: CrewRuntimeKind, role: string, isolationPolicy: CrewRuntimeConfig["isolationPolicy"], env: NodeJS.ProcessEnv = process.env): CrewRuntimeKind {
|
|
13
|
+
if (globalKind === "scaffold") return "scaffold";
|
|
14
|
+
// Safety: when already inside a pi-crew worker (depth > 0), never nest live-session.
|
|
15
|
+
// Live-session creates in-process Pi agent sessions, which would recursively
|
|
16
|
+
// try to use pi-crew, leading to "Cannot read properties of undefined" errors.
|
|
17
|
+
if (globalKind === "live-session" && currentCrewDepth(env) > 0) return "child-process";
|
|
18
|
+
const isolatedRoles = isolationPolicy?.isolatedRoles ?? [];
|
|
19
|
+
if (isolatedRoles.includes(role)) return "child-process";
|
|
20
|
+
return isolationPolicy?.defaultRuntime ?? globalKind;
|
|
21
|
+
}
|