pi-crew 0.1.30 → 0.1.32
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 +9 -0
- package/README.md +80 -27
- 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/architecture.md +173 -164
- 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 -0
- package/docs/research-extension-system.md +324 -0
- package/docs/research-optimization-plan.md +548 -0
- package/docs/research-pi-coding-agent.md +357 -0
- package/docs/resource-formats.md +4 -3
- package/docs/runtime-flow.md +148 -148
- package/docs/source-runtime-refactor-map.md +83 -83
- package/docs/usage.md +3 -3
- package/index.ts +6 -6
- package/package.json +1 -1
- package/schema.json +53 -1
- package/skills/git-master/SKILL.md +24 -19
- package/skills/read-only-explorer/SKILL.md +26 -21
- package/skills/safe-bash/SKILL.md +21 -16
- package/skills/task-packet/SKILL.md +28 -23
- package/skills/verify-evidence/SKILL.md +27 -22
- package/src/agents/agent-serializer.ts +34 -34
- package/src/agents/discover-agents.ts +102 -102
- package/src/config/config.ts +14 -1
- package/src/config/defaults.ts +3 -2
- package/src/extension/cross-extension-rpc.ts +82 -82
- package/src/extension/import-index.ts +4 -3
- package/src/extension/management.ts +2 -2
- package/src/extension/project-init.ts +9 -7
- package/src/extension/register.ts +63 -0
- package/src/extension/registration/artifact-cleanup.ts +15 -14
- package/src/extension/registration/commands.ts +208 -208
- package/src/extension/registration/compaction-guard.ts +125 -0
- package/src/extension/registration/subagent-tools.ts +27 -8
- package/src/extension/registration/team-tool.ts +61 -44
- package/src/extension/result-watcher.ts +98 -98
- package/src/extension/run-import.ts +4 -4
- package/src/extension/run-index.ts +14 -14
- package/src/extension/run-maintenance.ts +24 -24
- package/src/extension/team-tool/api.ts +3 -0
- package/src/extension/team-tool/cancel.ts +31 -31
- package/src/extension/team-tool/doctor.ts +179 -178
- 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/status.ts +73 -73
- package/src/prompt/prompt-runtime.ts +68 -68
- package/src/runtime/agent-control.ts +64 -64
- package/src/runtime/agent-memory.ts +72 -72
- package/src/runtime/agent-observability.ts +113 -113
- package/src/runtime/async-marker.ts +26 -26
- package/src/runtime/background-runner.ts +53 -53
- package/src/runtime/crew-agent-runtime.ts +58 -58
- 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 +88 -88
- package/src/runtime/live-agent-control.ts +78 -78
- 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 +299 -299
- package/src/runtime/manifest-cache.ts +212 -214
- package/src/runtime/model-fallback.ts +261 -261
- package/src/runtime/parallel-research.ts +44 -44
- package/src/runtime/parallel-utils.ts +99 -99
- package/src/runtime/pi-json-output.ts +111 -111
- package/src/runtime/pi-spawn.ts +96 -96
- package/src/runtime/policy-engine.ts +78 -78
- package/src/runtime/post-exit-stdio-guard.ts +86 -86
- 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/role-permission.ts +39 -39
- package/src/runtime/session-usage.ts +79 -79
- package/src/runtime/sidechain-output.ts +28 -28
- package/src/runtime/subagent-manager.ts +29 -2
- package/src/runtime/task-display.ts +38 -38
- package/src/runtime/task-output-context.ts +106 -106
- package/src/runtime/task-packet.ts +84 -84
- package/src/runtime/task-runner/live-executor.ts +98 -98
- package/src/runtime/task-runner/progress.ts +111 -111
- package/src/runtime/task-runner/prompt-builder.ts +72 -72
- package/src/runtime/task-runner/result-utils.ts +14 -14
- package/src/runtime/task-runner/state-helpers.ts +22 -22
- package/src/runtime/worker-heartbeat.ts +21 -21
- package/src/runtime/worker-startup.ts +57 -57
- package/src/schema/config-schema.ts +12 -0
- package/src/schema/team-tool-schema.ts +100 -100
- package/src/state/artifact-store.ts +108 -108
- package/src/state/contracts.ts +105 -105
- package/src/state/jsonl-writer.ts +77 -77
- package/src/state/state-store.ts +8 -9
- package/src/state/task-claims.ts +42 -42
- package/src/state/types.ts +180 -180
- 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 +36 -36
- package/src/types/diff.d.ts +18 -0
- package/src/ui/crew-footer.ts +101 -101
- package/src/ui/crew-select-list.ts +111 -111
- package/src/ui/crew-widget.ts +285 -285
- package/src/ui/dynamic-border.ts +25 -25
- package/src/ui/layout-primitives.ts +106 -106
- package/src/ui/loaders.ts +158 -158
- package/src/ui/mascot.ts +441 -441
- package/src/ui/powerbar-publisher.ts +94 -94
- package/src/ui/render-diff.ts +119 -119
- package/src/ui/run-dashboard.ts +372 -372
- package/src/ui/status-colors.ts +54 -54
- package/src/ui/syntax-highlight.ts +116 -116
- package/src/ui/transcript-viewer.ts +302 -302
- package/src/utils/completion-dedupe.ts +63 -63
- package/src/utils/file-coalescer.ts +84 -84
- package/src/utils/frontmatter.ts +36 -36
- package/src/utils/fs-watch.ts +31 -31
- package/src/utils/git.ts +262 -262
- package/src/utils/ids.ts +12 -12
- package/src/utils/names.ts +26 -26
- package/src/utils/paths.ts +34 -7
- package/src/utils/sleep.ts +32 -32
- package/src/utils/timings.ts +31 -31
- package/src/utils/visual.ts +159 -159
- package/src/workflows/discover-workflows.ts +2 -2
- package/src/workflows/validate-workflow.ts +40 -40
- package/src/worktree/branch-freshness.ts +45 -45
- package/src/worktree/cleanup.ts +71 -69
- package/src/worktree/worktree-manager.ts +3 -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
|
+
}
|
|
@@ -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|\bnpm\s+install\b|\bgit\s+(commit|push|merge|rebase|reset|checkout)\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|\bnpm\s+install\b|\bgit\s+(commit|push|merge|rebase|reset|checkout)\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,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,28 +1,28 @@
|
|
|
1
|
-
import * as fs from "node:fs";
|
|
2
|
-
import * as path from "node:path";
|
|
3
|
-
|
|
4
|
-
export interface SidechainEntry {
|
|
5
|
-
isSidechain: true;
|
|
6
|
-
agentId: string;
|
|
7
|
-
type: string;
|
|
8
|
-
message: unknown;
|
|
9
|
-
timestamp: string;
|
|
10
|
-
cwd: string;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export function writeSidechainEntry(filePath: string, entry: Omit<SidechainEntry, "isSidechain" | "timestamp">): void {
|
|
14
|
-
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
15
|
-
fs.appendFileSync(filePath, `${JSON.stringify({ isSidechain: true, timestamp: new Date().toISOString(), ...entry })}\n`, "utf-8");
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export function sidechainOutputPath(stateRoot: string, taskId: string): string {
|
|
19
|
-
return path.join(stateRoot, "agents", taskId, "sidechain.output.jsonl");
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export function eventToSidechainType(event: unknown): string | undefined {
|
|
23
|
-
if (!event || typeof event !== "object" || Array.isArray(event)) return undefined;
|
|
24
|
-
const type = (event as { type?: unknown }).type;
|
|
25
|
-
if (type === "message_start" || type === "message_update" || type === "message_end") return "message";
|
|
26
|
-
if (type === "tool_execution_start" || type === "tool_execution_update" || type === "tool_execution_end") return "tool";
|
|
27
|
-
return typeof type === "string" ? type : undefined;
|
|
28
|
-
}
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
|
|
4
|
+
export interface SidechainEntry {
|
|
5
|
+
isSidechain: true;
|
|
6
|
+
agentId: string;
|
|
7
|
+
type: string;
|
|
8
|
+
message: unknown;
|
|
9
|
+
timestamp: string;
|
|
10
|
+
cwd: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function writeSidechainEntry(filePath: string, entry: Omit<SidechainEntry, "isSidechain" | "timestamp">): void {
|
|
14
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
15
|
+
fs.appendFileSync(filePath, `${JSON.stringify({ isSidechain: true, timestamp: new Date().toISOString(), ...entry })}\n`, "utf-8");
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function sidechainOutputPath(stateRoot: string, taskId: string): string {
|
|
19
|
+
return path.join(stateRoot, "agents", taskId, "sidechain.output.jsonl");
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function eventToSidechainType(event: unknown): string | undefined {
|
|
23
|
+
if (!event || typeof event !== "object" || Array.isArray(event)) return undefined;
|
|
24
|
+
const type = (event as { type?: unknown }).type;
|
|
25
|
+
if (type === "message_start" || type === "message_update" || type === "message_end") return "message";
|
|
26
|
+
if (type === "tool_execution_start" || type === "tool_execution_update" || type === "tool_execution_end") return "tool";
|
|
27
|
+
return typeof type === "string" ? type : undefined;
|
|
28
|
+
}
|
|
@@ -3,7 +3,8 @@ import * as path from "node:path";
|
|
|
3
3
|
import { loadRunManifestById } from "../state/state-store.ts";
|
|
4
4
|
import type { PiTeamsToolResult } from "../extension/tool-result.ts";
|
|
5
5
|
import { DEFAULT_SUBAGENT } from "../config/defaults.ts";
|
|
6
|
-
import {
|
|
6
|
+
import { projectCrewRoot } from "../utils/paths.ts";
|
|
7
|
+
import { DEFAULT_PATHS } from "../config/defaults.ts";
|
|
7
8
|
import { logInternalError } from "../utils/internal-error.ts";
|
|
8
9
|
|
|
9
10
|
export type SubagentStatus = "queued" | "running" | "completed" | "failed" | "cancelled" | "error" | "blocked" | "stopped";
|
|
@@ -35,6 +36,10 @@ export interface SubagentRecord {
|
|
|
35
36
|
stuckNotified?: boolean;
|
|
36
37
|
blockedAt?: number;
|
|
37
38
|
promise?: Promise<void>;
|
|
39
|
+
// Phase 1.6: Telemetry baseline fields
|
|
40
|
+
turnCount?: number;
|
|
41
|
+
terminated?: boolean;
|
|
42
|
+
durationMs?: number;
|
|
38
43
|
}
|
|
39
44
|
|
|
40
45
|
type SpawnRunner = (options: SubagentSpawnOptions, signal?: AbortSignal) => Promise<PiTeamsToolResult>;
|
|
@@ -49,7 +54,7 @@ interface QueuedSpawn {
|
|
|
49
54
|
}
|
|
50
55
|
|
|
51
56
|
function persistedSubagentPath(cwd: string, id: string): string {
|
|
52
|
-
return path.join(
|
|
57
|
+
return path.join(projectCrewRoot(cwd), DEFAULT_PATHS.state.subagentsSubdir, `${id}.json`);
|
|
53
58
|
}
|
|
54
59
|
|
|
55
60
|
function serializableRecord(record: SubagentRecord): SubagentRecord {
|
|
@@ -85,6 +90,22 @@ function detailsRunId(result: PiTeamsToolResult): string | undefined {
|
|
|
85
90
|
return typeof details?.runId === "string" ? details.runId : undefined;
|
|
86
91
|
}
|
|
87
92
|
|
|
93
|
+
function totalRunTurns(cwd: string, runId: string | undefined): number | undefined {
|
|
94
|
+
if (!runId) return undefined;
|
|
95
|
+
const loaded = loadRunManifestById(cwd, runId);
|
|
96
|
+
if (!loaded) return undefined;
|
|
97
|
+
let total = 0;
|
|
98
|
+
let hasTurns = false;
|
|
99
|
+
for (const task of loaded.tasks) {
|
|
100
|
+
const turns = task.usage?.turns ?? task.agentProgress?.turns;
|
|
101
|
+
if (typeof turns === "number" && Number.isFinite(turns)) {
|
|
102
|
+
total += turns;
|
|
103
|
+
hasTurns = true;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return hasTurns ? total : undefined;
|
|
107
|
+
}
|
|
108
|
+
|
|
88
109
|
export class SubagentManager {
|
|
89
110
|
private readonly records = new Map<string, SubagentRecord>();
|
|
90
111
|
private queue: QueuedSpawn[] = [];
|
|
@@ -214,6 +235,10 @@ export class SubagentManager {
|
|
|
214
235
|
record.completedAt = record.completedAt ?? Date.now();
|
|
215
236
|
savePersistedSubagentRecord(options.cwd, record);
|
|
216
237
|
if (record.status === "completed" || record.status === "failed" || record.status === "cancelled" || record.status === "error" || record.status === "stopped") {
|
|
238
|
+
// Phase 1.6: Populate telemetry fields
|
|
239
|
+
record.turnCount = record.turnCount ?? totalRunTurns(options.cwd, record.runId);
|
|
240
|
+
record.durationMs = record.completedAt ? Math.max(0, record.completedAt - record.startedAt) : undefined;
|
|
241
|
+
savePersistedSubagentRecord(options.cwd, record);
|
|
217
242
|
this.onComplete?.(record);
|
|
218
243
|
}
|
|
219
244
|
this.drainQueue();
|
|
@@ -239,6 +264,7 @@ export class SubagentManager {
|
|
|
239
264
|
if (loaded.manifest.status === "completed") {
|
|
240
265
|
record.status = "completed";
|
|
241
266
|
record.error = undefined;
|
|
267
|
+
record.turnCount = record.turnCount ?? totalRunTurns(cwd, record.runId);
|
|
242
268
|
record.completedAt = Date.now();
|
|
243
269
|
savePersistedSubagentRecord(cwd, record);
|
|
244
270
|
return;
|
|
@@ -246,6 +272,7 @@ export class SubagentManager {
|
|
|
246
272
|
if (loaded.manifest.status === "failed" || loaded.manifest.status === "cancelled") {
|
|
247
273
|
record.status = loaded.manifest.status;
|
|
248
274
|
record.error = loaded.manifest.summary;
|
|
275
|
+
record.turnCount = record.turnCount ?? totalRunTurns(cwd, record.runId);
|
|
249
276
|
record.completedAt = Date.now();
|
|
250
277
|
savePersistedSubagentRecord(cwd, record);
|
|
251
278
|
return;
|
|
@@ -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
|
+
}
|