pi-crew 0.1.41 → 0.1.43
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 +47 -0
- package/README.md +51 -0
- package/agents/analyst.md +11 -11
- package/agents/critic.md +11 -11
- package/agents/executor.md +11 -11
- package/agents/explorer.md +11 -11
- package/agents/planner.md +11 -11
- package/agents/reviewer.md +11 -11
- package/agents/security-reviewer.md +11 -11
- package/agents/test-engineer.md +11 -11
- package/agents/verifier.md +11 -11
- package/agents/writer.md +11 -11
- package/docs/refactor-tasks-phase3.md +394 -394
- package/docs/refactor-tasks-phase4.md +564 -564
- package/docs/refactor-tasks-phase5.md +402 -402
- package/docs/refactor-tasks-phase6.md +662 -662
- package/docs/research-extension-examples.md +297 -297
- package/docs/research-extension-system.md +324 -324
- package/docs/research-optimization-plan.md +548 -548
- package/docs/research-pi-coding-agent.md +357 -357
- package/docs/research-source-pi-crew-reference.md +174 -174
- package/docs/runtime-flow.md +148 -148
- package/docs/source-runtime-refactor-map.md +83 -83
- package/index.ts +6 -6
- package/package.json +1 -1
- package/src/agents/agent-serializer.ts +34 -34
- package/src/agents/discover-agents.ts +4 -4
- package/src/config/config.ts +9 -4
- package/src/extension/cross-extension-rpc.ts +82 -82
- package/src/extension/management.ts +37 -8
- package/src/extension/notification-router.ts +2 -2
- package/src/extension/register.ts +3 -0
- package/src/extension/registration/commands.ts +11 -9
- package/src/extension/registration/compaction-guard.ts +125 -125
- package/src/extension/registration/subagent-tools.ts +28 -19
- package/src/extension/result-watcher.ts +4 -4
- package/src/extension/run-bundle-schema.ts +8 -4
- package/src/extension/run-import.ts +4 -0
- package/src/extension/run-index.ts +4 -1
- package/src/extension/run-maintenance.ts +43 -24
- package/src/extension/team-tool/api.ts +1 -1
- package/src/extension/team-tool/cancel.ts +31 -31
- package/src/extension/team-tool/doctor.ts +8 -1
- package/src/extension/team-tool/handle-settings.ts +188 -0
- package/src/extension/team-tool/inspect.ts +41 -41
- package/src/extension/team-tool/lifecycle-actions.ts +79 -79
- package/src/extension/team-tool/plan.ts +19 -19
- package/src/extension/team-tool/run.ts +4 -2
- package/src/extension/team-tool/status.ts +93 -93
- package/src/extension/team-tool.ts +3 -1
- package/src/i18n.ts +184 -0
- package/src/observability/correlation.ts +2 -2
- package/src/observability/event-to-metric.ts +4 -3
- package/src/observability/exporters/adapter.ts +7 -1
- package/src/observability/exporters/otlp-exporter.ts +14 -2
- package/src/observability/exporters/prometheus-exporter.ts +9 -2
- package/src/observability/metric-registry.ts +18 -3
- package/src/observability/metric-retention.ts +11 -3
- package/src/observability/metric-sink.ts +9 -4
- package/src/observability/metrics-primitives.ts +4 -3
- package/src/prompt/prompt-runtime.ts +72 -68
- package/src/runtime/agent-control.ts +63 -63
- 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/attention-events.ts +28 -23
- package/src/runtime/background-runner.ts +53 -53
- package/src/runtime/child-pi.ts +4 -4
- package/src/runtime/completion-guard.ts +103 -99
- package/src/runtime/concurrency.ts +1 -1
- package/src/runtime/crash-recovery.ts +2 -1
- package/src/runtime/crew-agent-runtime.ts +58 -58
- package/src/runtime/deadletter.ts +14 -4
- package/src/runtime/direct-run.ts +35 -35
- package/src/runtime/foreground-control.ts +82 -82
- package/src/runtime/green-contract.ts +46 -46
- package/src/runtime/group-join.ts +106 -106
- package/src/runtime/heartbeat-gradient.ts +28 -28
- package/src/runtime/heartbeat-watcher.ts +48 -4
- package/src/runtime/live-agent-control.ts +87 -87
- package/src/runtime/live-agent-manager.ts +85 -85
- package/src/runtime/live-control-realtime.ts +36 -36
- package/src/runtime/live-session-runtime.ts +305 -305
- package/src/runtime/manifest-cache.ts +2 -2
- package/src/runtime/model-fallback.ts +269 -261
- package/src/runtime/parallel-research.ts +44 -44
- package/src/runtime/parallel-utils.ts +1 -1
- package/src/runtime/pi-json-output.ts +111 -111
- package/src/runtime/policy-engine.ts +79 -78
- package/src/runtime/post-exit-stdio-guard.ts +2 -2
- package/src/runtime/process-status.ts +56 -56
- package/src/runtime/progress-event-coalescer.ts +43 -43
- package/src/runtime/recovery-recipes.ts +74 -74
- package/src/runtime/retry-executor.ts +5 -0
- package/src/runtime/role-permission.ts +39 -39
- package/src/runtime/runtime-resolver.ts +1 -1
- package/src/runtime/session-usage.ts +79 -79
- package/src/runtime/sidechain-output.ts +29 -29
- package/src/runtime/subagent-manager.ts +3 -3
- package/src/runtime/task-display.ts +38 -38
- package/src/runtime/task-output-context.ts +127 -127
- package/src/runtime/task-runner/live-executor.ts +101 -101
- package/src/runtime/task-runner/progress.ts +119 -111
- package/src/runtime/task-runner/result-utils.ts +14 -14
- package/src/runtime/task-runner/state-helpers.ts +22 -22
- package/src/runtime/team-runner.ts +3 -6
- package/src/runtime/worker-heartbeat.ts +21 -21
- package/src/runtime/worker-startup.ts +57 -57
- package/src/schema/config-schema.ts +1 -1
- package/src/schema/team-tool-schema.ts +110 -109
- package/src/state/artifact-store.ts +4 -2
- package/src/state/atomic-write.ts +12 -4
- package/src/state/contracts.ts +105 -105
- package/src/state/event-log.ts +3 -4
- package/src/state/jsonl-writer.ts +4 -1
- package/src/state/locks.ts +9 -1
- package/src/state/task-claims.ts +44 -42
- 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 +2 -2
- package/src/teams/team-serializer.ts +38 -38
- package/src/types/diff.d.ts +18 -18
- package/src/ui/crew-footer.ts +101 -101
- package/src/ui/crew-select-list.ts +111 -111
- package/src/ui/dashboard-panes/metrics-pane.ts +34 -34
- package/src/ui/dynamic-border.ts +25 -25
- package/src/ui/layout-primitives.ts +106 -106
- package/src/ui/live-run-sidebar.ts +1 -1
- package/src/ui/loaders.ts +158 -158
- package/src/ui/mascot.ts +3 -2
- package/src/ui/powerbar-publisher.ts +4 -3
- package/src/ui/render-diff.ts +119 -119
- package/src/ui/render-scheduler.ts +54 -14
- package/src/ui/run-dashboard.ts +39 -11
- package/src/ui/run-snapshot-cache.ts +63 -37
- package/src/ui/spinner.ts +17 -17
- package/src/ui/status-colors.ts +54 -54
- package/src/ui/syntax-highlight.ts +116 -116
- package/src/ui/theme-adapter.ts +1 -1
- package/src/ui/transcript-viewer.ts +7 -2
- package/src/utils/completion-dedupe.ts +63 -63
- package/src/utils/file-coalescer.ts +5 -3
- package/src/utils/frontmatter.ts +68 -36
- package/src/utils/git.ts +262 -262
- package/src/utils/ids.ts +12 -12
- package/src/utils/internal-error.ts +1 -1
- package/src/utils/names.ts +27 -26
- package/src/utils/paths.ts +1 -1
- package/src/utils/redaction.ts +44 -41
- package/src/utils/safe-paths.ts +47 -34
- package/src/utils/sleep.ts +2 -2
- package/src/utils/timings.ts +2 -0
- package/src/utils/visual.ts +9 -1
- package/src/workflows/discover-workflows.ts +4 -1
- package/src/workflows/validate-workflow.ts +40 -40
- package/src/worktree/branch-freshness.ts +45 -45
- package/src/worktree/worktree-manager.ts +6 -1
- 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/workflows/default.workflow.md +29 -29
- package/workflows/fast-fix.workflow.md +22 -22
- package/workflows/implementation.workflow.md +38 -38
- package/workflows/parallel-research.workflow.md +46 -46
- package/workflows/research.workflow.md +22 -22
- package/workflows/review.workflow.md +30 -30
|
@@ -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
|
+
}
|
|
@@ -46,6 +46,11 @@ export async function executeWithRetry<T>(fn: (attempt: number) => Promise<T>, p
|
|
|
46
46
|
return await fn(attempt);
|
|
47
47
|
} catch (error) {
|
|
48
48
|
lastError = asError(error);
|
|
49
|
+
// Never retry if aborted — sleep() would immediately reject on every attempt.
|
|
50
|
+
if (hooks.signal?.aborted) {
|
|
51
|
+
hooks.onRetryGivenUp?.(attempt, lastError);
|
|
52
|
+
throw lastError;
|
|
53
|
+
}
|
|
49
54
|
if (attempt >= normalized.maxAttempts || !isRetryable(lastError, normalized)) {
|
|
50
55
|
hooks.onRetryGivenUp?.(attempt, lastError);
|
|
51
56
|
throw lastError;
|
|
@@ -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|\
|
|
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
|
+
}
|
|
@@ -41,7 +41,7 @@ export async function isLiveSessionRuntimeAvailable(timeoutMs = 1500, env: NodeJ
|
|
|
41
41
|
probe(),
|
|
42
42
|
new Promise<{ available: boolean; reason: string }>((resolve) => {
|
|
43
43
|
timer = setTimeout(() => resolve({ available: false, reason: `Timed out probing optional Pi SDK live-session runtime after ${timeoutMs}ms.` }), timeoutMs);
|
|
44
|
-
timer.unref
|
|
44
|
+
timer.unref();
|
|
45
45
|
}),
|
|
46
46
|
]);
|
|
47
47
|
} finally {
|
|
@@ -1,79 +1,79 @@
|
|
|
1
|
-
import * as fs from "node:fs";
|
|
2
|
-
import type { UsageState } from "../state/types.ts";
|
|
3
|
-
|
|
4
|
-
function asRecord(value: unknown): Record<string, unknown> | undefined {
|
|
5
|
-
return value && typeof value === "object" && !Array.isArray(value) ? value as Record<string, unknown> : undefined;
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
function numberField(obj: Record<string, unknown>, keys: string[]): number | undefined {
|
|
9
|
-
for (const key of keys) {
|
|
10
|
-
const value = obj[key];
|
|
11
|
-
if (typeof value === "number" && Number.isFinite(value)) return value;
|
|
12
|
-
}
|
|
13
|
-
return undefined;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
function usageFromValue(value: unknown): UsageState | undefined {
|
|
17
|
-
const obj = asRecord(value);
|
|
18
|
-
if (!obj) return undefined;
|
|
19
|
-
const direct: UsageState = {
|
|
20
|
-
input: numberField(obj, ["input", "inputTokens", "input_tokens"]),
|
|
21
|
-
output: numberField(obj, ["output", "outputTokens", "output_tokens"]),
|
|
22
|
-
cacheRead: numberField(obj, ["cacheRead", "cache_read", "cacheReadTokens", "cache_read_tokens"]),
|
|
23
|
-
cacheWrite: numberField(obj, ["cacheWrite", "cache_write", "cacheWriteTokens", "cache_write_tokens"]),
|
|
24
|
-
cost: numberField(obj, ["cost", "costUsd", "cost_usd"]),
|
|
25
|
-
turns: numberField(obj, ["turns", "turnCount", "turn_count"]),
|
|
26
|
-
};
|
|
27
|
-
if (Object.values(direct).some((entry) => entry !== undefined)) return direct;
|
|
28
|
-
for (const key of ["usage", "tokenUsage", "tokens", "stats"]) {
|
|
29
|
-
const nested = usageFromValue(obj[key]);
|
|
30
|
-
if (nested) return nested;
|
|
31
|
-
}
|
|
32
|
-
const message = asRecord(obj.message);
|
|
33
|
-
return message ? usageFromValue(message.usage) : undefined;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
function addUsage(total: UsageState, usage: UsageState): UsageState {
|
|
37
|
-
return {
|
|
38
|
-
input: (total.input ?? 0) + (usage.input ?? 0),
|
|
39
|
-
output: (total.output ?? 0) + (usage.output ?? 0),
|
|
40
|
-
cacheRead: (total.cacheRead ?? 0) + (usage.cacheRead ?? 0),
|
|
41
|
-
cacheWrite: (total.cacheWrite ?? 0) + (usage.cacheWrite ?? 0),
|
|
42
|
-
cost: (total.cost ?? 0) + (usage.cost ?? 0),
|
|
43
|
-
turns: (total.turns ?? 0) + (usage.turns ?? 0),
|
|
44
|
-
};
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
function compactUsage(total: UsageState, foundKeys: Set<keyof UsageState>): UsageState | undefined {
|
|
48
|
-
if (foundKeys.size === 0) return undefined;
|
|
49
|
-
const compact: UsageState = {};
|
|
50
|
-
for (const key of foundKeys) compact[key] = total[key];
|
|
51
|
-
return compact;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
export function parseSessionUsageFromJsonlText(text: string): UsageState | undefined {
|
|
55
|
-
let total: UsageState = {};
|
|
56
|
-
const foundKeys = new Set<keyof UsageState>();
|
|
57
|
-
for (const line of text.split(/\r?\n/)) {
|
|
58
|
-
const trimmed = line.trim();
|
|
59
|
-
if (!trimmed) continue;
|
|
60
|
-
try {
|
|
61
|
-
const usage = usageFromValue(JSON.parse(trimmed) as unknown);
|
|
62
|
-
if (!usage) continue;
|
|
63
|
-
for (const key of Object.keys(usage) as Array<keyof UsageState>) foundKeys.add(key);
|
|
64
|
-
total = addUsage(total, usage);
|
|
65
|
-
} catch {
|
|
66
|
-
// Session JSONL can contain partial/corrupt lines after interrupted workers.
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
return compactUsage(total, foundKeys);
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
export function parseSessionUsage(filePath: string): UsageState | undefined {
|
|
73
|
-
try {
|
|
74
|
-
if (!fs.existsSync(filePath)) return undefined;
|
|
75
|
-
return parseSessionUsageFromJsonlText(fs.readFileSync(filePath, "utf-8"));
|
|
76
|
-
} catch {
|
|
77
|
-
return undefined;
|
|
78
|
-
}
|
|
79
|
-
}
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import type { UsageState } from "../state/types.ts";
|
|
3
|
+
|
|
4
|
+
function asRecord(value: unknown): Record<string, unknown> | undefined {
|
|
5
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value as Record<string, unknown> : undefined;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function numberField(obj: Record<string, unknown>, keys: string[]): number | undefined {
|
|
9
|
+
for (const key of keys) {
|
|
10
|
+
const value = obj[key];
|
|
11
|
+
if (typeof value === "number" && Number.isFinite(value)) return value;
|
|
12
|
+
}
|
|
13
|
+
return undefined;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function usageFromValue(value: unknown): UsageState | undefined {
|
|
17
|
+
const obj = asRecord(value);
|
|
18
|
+
if (!obj) return undefined;
|
|
19
|
+
const direct: UsageState = {
|
|
20
|
+
input: numberField(obj, ["input", "inputTokens", "input_tokens"]),
|
|
21
|
+
output: numberField(obj, ["output", "outputTokens", "output_tokens"]),
|
|
22
|
+
cacheRead: numberField(obj, ["cacheRead", "cache_read", "cacheReadTokens", "cache_read_tokens"]),
|
|
23
|
+
cacheWrite: numberField(obj, ["cacheWrite", "cache_write", "cacheWriteTokens", "cache_write_tokens"]),
|
|
24
|
+
cost: numberField(obj, ["cost", "costUsd", "cost_usd"]),
|
|
25
|
+
turns: numberField(obj, ["turns", "turnCount", "turn_count"]),
|
|
26
|
+
};
|
|
27
|
+
if (Object.values(direct).some((entry) => entry !== undefined)) return direct;
|
|
28
|
+
for (const key of ["usage", "tokenUsage", "tokens", "stats"]) {
|
|
29
|
+
const nested = usageFromValue(obj[key]);
|
|
30
|
+
if (nested) return nested;
|
|
31
|
+
}
|
|
32
|
+
const message = asRecord(obj.message);
|
|
33
|
+
return message ? usageFromValue(message.usage) : undefined;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function addUsage(total: UsageState, usage: UsageState): UsageState {
|
|
37
|
+
return {
|
|
38
|
+
input: (total.input ?? 0) + (usage.input ?? 0),
|
|
39
|
+
output: (total.output ?? 0) + (usage.output ?? 0),
|
|
40
|
+
cacheRead: (total.cacheRead ?? 0) + (usage.cacheRead ?? 0),
|
|
41
|
+
cacheWrite: (total.cacheWrite ?? 0) + (usage.cacheWrite ?? 0),
|
|
42
|
+
cost: (total.cost ?? 0) + (usage.cost ?? 0),
|
|
43
|
+
turns: (total.turns ?? 0) + (usage.turns ?? 0),
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function compactUsage(total: UsageState, foundKeys: Set<keyof UsageState>): UsageState | undefined {
|
|
48
|
+
if (foundKeys.size === 0) return undefined;
|
|
49
|
+
const compact: UsageState = {};
|
|
50
|
+
for (const key of foundKeys) compact[key] = total[key];
|
|
51
|
+
return compact;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function parseSessionUsageFromJsonlText(text: string): UsageState | undefined {
|
|
55
|
+
let total: UsageState = {};
|
|
56
|
+
const foundKeys = new Set<keyof UsageState>();
|
|
57
|
+
for (const line of text.split(/\r?\n/)) {
|
|
58
|
+
const trimmed = line.trim();
|
|
59
|
+
if (!trimmed) continue;
|
|
60
|
+
try {
|
|
61
|
+
const usage = usageFromValue(JSON.parse(trimmed) as unknown);
|
|
62
|
+
if (!usage) continue;
|
|
63
|
+
for (const key of Object.keys(usage) as Array<keyof UsageState>) foundKeys.add(key);
|
|
64
|
+
total = addUsage(total, usage);
|
|
65
|
+
} catch {
|
|
66
|
+
// Session JSONL can contain partial/corrupt lines after interrupted workers.
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return compactUsage(total, foundKeys);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function parseSessionUsage(filePath: string): UsageState | undefined {
|
|
73
|
+
try {
|
|
74
|
+
if (!fs.existsSync(filePath)) return undefined;
|
|
75
|
+
return parseSessionUsageFromJsonlText(fs.readFileSync(filePath, "utf-8"));
|
|
76
|
+
} catch {
|
|
77
|
+
return undefined;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
@@ -1,29 +1,29 @@
|
|
|
1
|
-
import * as fs from "node:fs";
|
|
2
|
-
import * as path from "node:path";
|
|
3
|
-
import { redactSecrets } from "../utils/redaction.ts";
|
|
4
|
-
|
|
5
|
-
export interface SidechainEntry {
|
|
6
|
-
isSidechain: true;
|
|
7
|
-
agentId: string;
|
|
8
|
-
type: string;
|
|
9
|
-
message: unknown;
|
|
10
|
-
timestamp: string;
|
|
11
|
-
cwd: string;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export function writeSidechainEntry(filePath: string, entry: Omit<SidechainEntry, "isSidechain" | "timestamp">): void {
|
|
15
|
-
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
16
|
-
fs.appendFileSync(filePath, `${JSON.stringify(redactSecrets({ isSidechain: true, timestamp: new Date().toISOString(), ...entry }))}\n`, "utf-8");
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export function sidechainOutputPath(stateRoot: string, taskId: string): string {
|
|
20
|
-
return path.join(stateRoot, "agents", taskId, "sidechain.output.jsonl");
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export function eventToSidechainType(event: unknown): string | undefined {
|
|
24
|
-
if (!event || typeof event !== "object" || Array.isArray(event)) return undefined;
|
|
25
|
-
const type = (event as { type?: unknown }).type;
|
|
26
|
-
if (type === "message_start" || type === "message_update" || type === "message_end") return "message";
|
|
27
|
-
if (type === "tool_execution_start" || type === "tool_execution_update" || type === "tool_execution_end") return "tool";
|
|
28
|
-
return typeof type === "string" ? type : undefined;
|
|
29
|
-
}
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import { redactSecrets } from "../utils/redaction.ts";
|
|
4
|
+
|
|
5
|
+
export interface SidechainEntry {
|
|
6
|
+
isSidechain: true;
|
|
7
|
+
agentId: string;
|
|
8
|
+
type: string;
|
|
9
|
+
message: unknown;
|
|
10
|
+
timestamp: string;
|
|
11
|
+
cwd: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function writeSidechainEntry(filePath: string, entry: Omit<SidechainEntry, "isSidechain" | "timestamp">): void {
|
|
15
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
16
|
+
fs.appendFileSync(filePath, `${JSON.stringify(redactSecrets({ isSidechain: true, timestamp: new Date().toISOString(), ...entry }))}\n`, "utf-8");
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function sidechainOutputPath(stateRoot: string, taskId: string): string {
|
|
20
|
+
return path.join(stateRoot, "agents", taskId, "sidechain.output.jsonl");
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function eventToSidechainType(event: unknown): string | undefined {
|
|
24
|
+
if (!event || typeof event !== "object" || Array.isArray(event)) return undefined;
|
|
25
|
+
const type = (event as { type?: unknown }).type;
|
|
26
|
+
if (type === "message_start" || type === "message_update" || type === "message_end") return "message";
|
|
27
|
+
if (type === "tool_execution_start" || type === "tool_execution_update" || type === "tool_execution_end") return "tool";
|
|
28
|
+
return typeof type === "string" ? type : undefined;
|
|
29
|
+
}
|
|
@@ -344,7 +344,7 @@ export class SubagentManager {
|
|
|
344
344
|
const loaded = loadRunManifestById(cwd, current.runId);
|
|
345
345
|
if (!loaded || loaded.manifest.status === "blocked" || loaded.manifest.status === "running" || loaded.manifest.status === "planning" || loaded.manifest.status === "queued") {
|
|
346
346
|
const timer = setTimeout(poll, this.pollIntervalMs);
|
|
347
|
-
timer.unref
|
|
347
|
+
timer.unref();
|
|
348
348
|
return;
|
|
349
349
|
}
|
|
350
350
|
const persisted = readPersistedSubagentRecord(cwd, current.id);
|
|
@@ -363,7 +363,7 @@ export class SubagentManager {
|
|
|
363
363
|
this.onComplete?.(current);
|
|
364
364
|
};
|
|
365
365
|
const timer = setTimeout(poll, this.pollIntervalMs);
|
|
366
|
-
timer.unref
|
|
366
|
+
timer.unref();
|
|
367
367
|
}
|
|
368
368
|
|
|
369
369
|
private scheduleStuckBlockedNotify(cwd: string, record: SubagentRecord): void {
|
|
@@ -386,6 +386,6 @@ export class SubagentManager {
|
|
|
386
386
|
return;
|
|
387
387
|
}
|
|
388
388
|
const timer = setTimeout(fire, threshold);
|
|
389
|
-
timer.unref
|
|
389
|
+
timer.unref();
|
|
390
390
|
}
|
|
391
391
|
}
|
|
@@ -1,38 +1,38 @@
|
|
|
1
|
-
import type { TeamTaskState } from "../state/types.ts";
|
|
2
|
-
import type { CrewAgentRecord, CrewRuntimeKind } from "./crew-agent-runtime.ts";
|
|
3
|
-
import { recordFromTask } from "./crew-agent-records.ts";
|
|
4
|
-
import type { TeamRunManifest } from "../state/types.ts";
|
|
5
|
-
|
|
6
|
-
export function shouldMaterializeAgent(task: TeamTaskState): boolean {
|
|
7
|
-
return task.status !== "queued" && task.status !== "skipped";
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
export function recordsForMaterializedTasks(manifest: TeamRunManifest, tasks: TeamTaskState[], runtime: CrewRuntimeKind): CrewAgentRecord[] {
|
|
11
|
-
return tasks.filter(shouldMaterializeAgent).map((task) => recordFromTask(manifest, task, runtime));
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export function taskById(tasks: TeamTaskState[]): Map<string, TeamTaskState> {
|
|
15
|
-
const map = new Map<string, TeamTaskState>();
|
|
16
|
-
for (const task of tasks) {
|
|
17
|
-
map.set(task.id, task);
|
|
18
|
-
if (task.stepId) map.set(task.stepId, task);
|
|
19
|
-
}
|
|
20
|
-
return map;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export function waitingReason(task: TeamTaskState, tasks: TeamTaskState[]): string | undefined {
|
|
24
|
-
if (task.status !== "queued") return undefined;
|
|
25
|
-
const byId = taskById(tasks);
|
|
26
|
-
const waiting = task.dependsOn.map((id) => byId.get(id)?.id ?? id).filter((id) => byId.get(id)?.status !== "completed");
|
|
27
|
-
if (waiting.length === 0) return "ready";
|
|
28
|
-
return `waiting for ${waiting.join(", ")}`;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
export function formatTaskGraphLines(tasks: TeamTaskState[]): string[] {
|
|
32
|
-
if (tasks.length === 0) return ["- (none)"];
|
|
33
|
-
return tasks.map((task) => {
|
|
34
|
-
const icon = task.status === "completed" ? "✓" : task.status === "running" ? "⠋" : task.status === "failed" ? "✗" : task.status === "cancelled" || task.status === "skipped" ? "■" : "◦";
|
|
35
|
-
const wait = waitingReason(task, tasks);
|
|
36
|
-
return `- ${icon} ${task.id} [${task.status}] ${task.role}->${task.agent}${wait && wait !== "ready" ? ` (${wait})` : ""}`;
|
|
37
|
-
});
|
|
38
|
-
}
|
|
1
|
+
import type { TeamTaskState } from "../state/types.ts";
|
|
2
|
+
import type { CrewAgentRecord, CrewRuntimeKind } from "./crew-agent-runtime.ts";
|
|
3
|
+
import { recordFromTask } from "./crew-agent-records.ts";
|
|
4
|
+
import type { TeamRunManifest } from "../state/types.ts";
|
|
5
|
+
|
|
6
|
+
export function shouldMaterializeAgent(task: TeamTaskState): boolean {
|
|
7
|
+
return task.status !== "queued" && task.status !== "skipped";
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function recordsForMaterializedTasks(manifest: TeamRunManifest, tasks: TeamTaskState[], runtime: CrewRuntimeKind): CrewAgentRecord[] {
|
|
11
|
+
return tasks.filter(shouldMaterializeAgent).map((task) => recordFromTask(manifest, task, runtime));
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function taskById(tasks: TeamTaskState[]): Map<string, TeamTaskState> {
|
|
15
|
+
const map = new Map<string, TeamTaskState>();
|
|
16
|
+
for (const task of tasks) {
|
|
17
|
+
map.set(task.id, task);
|
|
18
|
+
if (task.stepId) map.set(task.stepId, task);
|
|
19
|
+
}
|
|
20
|
+
return map;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function waitingReason(task: TeamTaskState, tasks: TeamTaskState[]): string | undefined {
|
|
24
|
+
if (task.status !== "queued") return undefined;
|
|
25
|
+
const byId = taskById(tasks);
|
|
26
|
+
const waiting = task.dependsOn.map((id) => byId.get(id)?.id ?? id).filter((id) => byId.get(id)?.status !== "completed");
|
|
27
|
+
if (waiting.length === 0) return "ready";
|
|
28
|
+
return `waiting for ${waiting.join(", ")}`;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function formatTaskGraphLines(tasks: TeamTaskState[]): string[] {
|
|
32
|
+
if (tasks.length === 0) return ["- (none)"];
|
|
33
|
+
return tasks.map((task) => {
|
|
34
|
+
const icon = task.status === "completed" ? "✓" : task.status === "running" ? "⠋" : task.status === "failed" ? "✗" : task.status === "cancelled" || task.status === "skipped" ? "■" : "◦";
|
|
35
|
+
const wait = waitingReason(task, tasks);
|
|
36
|
+
return `- ${icon} ${task.id} [${task.status}] ${task.role}->${task.agent}${wait && wait !== "ready" ? ` (${wait})` : ""}`;
|
|
37
|
+
});
|
|
38
|
+
}
|