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.
Files changed (159) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/README.md +80 -27
  3. package/agents/analyst.md +11 -11
  4. package/agents/critic.md +11 -11
  5. package/agents/executor.md +11 -11
  6. package/agents/explorer.md +11 -11
  7. package/agents/planner.md +11 -11
  8. package/agents/reviewer.md +11 -11
  9. package/agents/security-reviewer.md +11 -11
  10. package/agents/test-engineer.md +11 -11
  11. package/agents/verifier.md +11 -11
  12. package/agents/writer.md +11 -11
  13. package/docs/architecture.md +173 -164
  14. package/docs/refactor-tasks-phase3.md +394 -394
  15. package/docs/refactor-tasks-phase4.md +564 -564
  16. package/docs/refactor-tasks-phase5.md +402 -402
  17. package/docs/refactor-tasks-phase6.md +662 -662
  18. package/docs/research-extension-examples.md +297 -0
  19. package/docs/research-extension-system.md +324 -0
  20. package/docs/research-optimization-plan.md +548 -0
  21. package/docs/research-pi-coding-agent.md +357 -0
  22. package/docs/resource-formats.md +4 -3
  23. package/docs/runtime-flow.md +148 -148
  24. package/docs/source-runtime-refactor-map.md +83 -83
  25. package/docs/usage.md +3 -3
  26. package/index.ts +6 -6
  27. package/package.json +1 -1
  28. package/schema.json +53 -1
  29. package/skills/git-master/SKILL.md +24 -19
  30. package/skills/read-only-explorer/SKILL.md +26 -21
  31. package/skills/safe-bash/SKILL.md +21 -16
  32. package/skills/task-packet/SKILL.md +28 -23
  33. package/skills/verify-evidence/SKILL.md +27 -22
  34. package/src/agents/agent-serializer.ts +34 -34
  35. package/src/agents/discover-agents.ts +102 -102
  36. package/src/config/config.ts +14 -1
  37. package/src/config/defaults.ts +3 -2
  38. package/src/extension/cross-extension-rpc.ts +82 -82
  39. package/src/extension/import-index.ts +4 -3
  40. package/src/extension/management.ts +2 -2
  41. package/src/extension/project-init.ts +9 -7
  42. package/src/extension/register.ts +63 -0
  43. package/src/extension/registration/artifact-cleanup.ts +15 -14
  44. package/src/extension/registration/commands.ts +208 -208
  45. package/src/extension/registration/compaction-guard.ts +125 -0
  46. package/src/extension/registration/subagent-tools.ts +27 -8
  47. package/src/extension/registration/team-tool.ts +61 -44
  48. package/src/extension/result-watcher.ts +98 -98
  49. package/src/extension/run-import.ts +4 -4
  50. package/src/extension/run-index.ts +14 -14
  51. package/src/extension/run-maintenance.ts +24 -24
  52. package/src/extension/team-tool/api.ts +3 -0
  53. package/src/extension/team-tool/cancel.ts +31 -31
  54. package/src/extension/team-tool/doctor.ts +179 -178
  55. package/src/extension/team-tool/inspect.ts +41 -41
  56. package/src/extension/team-tool/lifecycle-actions.ts +79 -79
  57. package/src/extension/team-tool/plan.ts +19 -19
  58. package/src/extension/team-tool/status.ts +73 -73
  59. package/src/prompt/prompt-runtime.ts +68 -68
  60. package/src/runtime/agent-control.ts +64 -64
  61. package/src/runtime/agent-memory.ts +72 -72
  62. package/src/runtime/agent-observability.ts +113 -113
  63. package/src/runtime/async-marker.ts +26 -26
  64. package/src/runtime/background-runner.ts +53 -53
  65. package/src/runtime/crew-agent-runtime.ts +58 -58
  66. package/src/runtime/direct-run.ts +35 -35
  67. package/src/runtime/foreground-control.ts +82 -82
  68. package/src/runtime/green-contract.ts +46 -46
  69. package/src/runtime/group-join.ts +88 -88
  70. package/src/runtime/live-agent-control.ts +78 -78
  71. package/src/runtime/live-agent-manager.ts +85 -85
  72. package/src/runtime/live-control-realtime.ts +36 -36
  73. package/src/runtime/live-session-runtime.ts +299 -299
  74. package/src/runtime/manifest-cache.ts +212 -214
  75. package/src/runtime/model-fallback.ts +261 -261
  76. package/src/runtime/parallel-research.ts +44 -44
  77. package/src/runtime/parallel-utils.ts +99 -99
  78. package/src/runtime/pi-json-output.ts +111 -111
  79. package/src/runtime/pi-spawn.ts +96 -96
  80. package/src/runtime/policy-engine.ts +78 -78
  81. package/src/runtime/post-exit-stdio-guard.ts +86 -86
  82. package/src/runtime/process-status.ts +56 -56
  83. package/src/runtime/progress-event-coalescer.ts +43 -43
  84. package/src/runtime/recovery-recipes.ts +74 -74
  85. package/src/runtime/role-permission.ts +39 -39
  86. package/src/runtime/session-usage.ts +79 -79
  87. package/src/runtime/sidechain-output.ts +28 -28
  88. package/src/runtime/subagent-manager.ts +29 -2
  89. package/src/runtime/task-display.ts +38 -38
  90. package/src/runtime/task-output-context.ts +106 -106
  91. package/src/runtime/task-packet.ts +84 -84
  92. package/src/runtime/task-runner/live-executor.ts +98 -98
  93. package/src/runtime/task-runner/progress.ts +111 -111
  94. package/src/runtime/task-runner/prompt-builder.ts +72 -72
  95. package/src/runtime/task-runner/result-utils.ts +14 -14
  96. package/src/runtime/task-runner/state-helpers.ts +22 -22
  97. package/src/runtime/worker-heartbeat.ts +21 -21
  98. package/src/runtime/worker-startup.ts +57 -57
  99. package/src/schema/config-schema.ts +12 -0
  100. package/src/schema/team-tool-schema.ts +100 -100
  101. package/src/state/artifact-store.ts +108 -108
  102. package/src/state/contracts.ts +105 -105
  103. package/src/state/jsonl-writer.ts +77 -77
  104. package/src/state/state-store.ts +8 -9
  105. package/src/state/task-claims.ts +42 -42
  106. package/src/state/types.ts +180 -180
  107. package/src/state/usage.ts +29 -29
  108. package/src/subagents/async-entry.ts +1 -1
  109. package/src/subagents/index.ts +3 -3
  110. package/src/subagents/live/control.ts +1 -1
  111. package/src/subagents/live/manager.ts +1 -1
  112. package/src/subagents/live/realtime.ts +1 -1
  113. package/src/subagents/live/session-runtime.ts +1 -1
  114. package/src/subagents/manager.ts +1 -1
  115. package/src/subagents/spawn.ts +1 -1
  116. package/src/teams/discover-teams.ts +2 -2
  117. package/src/teams/team-serializer.ts +36 -36
  118. package/src/types/diff.d.ts +18 -0
  119. package/src/ui/crew-footer.ts +101 -101
  120. package/src/ui/crew-select-list.ts +111 -111
  121. package/src/ui/crew-widget.ts +285 -285
  122. package/src/ui/dynamic-border.ts +25 -25
  123. package/src/ui/layout-primitives.ts +106 -106
  124. package/src/ui/loaders.ts +158 -158
  125. package/src/ui/mascot.ts +441 -441
  126. package/src/ui/powerbar-publisher.ts +94 -94
  127. package/src/ui/render-diff.ts +119 -119
  128. package/src/ui/run-dashboard.ts +372 -372
  129. package/src/ui/status-colors.ts +54 -54
  130. package/src/ui/syntax-highlight.ts +116 -116
  131. package/src/ui/transcript-viewer.ts +302 -302
  132. package/src/utils/completion-dedupe.ts +63 -63
  133. package/src/utils/file-coalescer.ts +84 -84
  134. package/src/utils/frontmatter.ts +36 -36
  135. package/src/utils/fs-watch.ts +31 -31
  136. package/src/utils/git.ts +262 -262
  137. package/src/utils/ids.ts +12 -12
  138. package/src/utils/names.ts +26 -26
  139. package/src/utils/paths.ts +34 -7
  140. package/src/utils/sleep.ts +32 -32
  141. package/src/utils/timings.ts +31 -31
  142. package/src/utils/visual.ts +159 -159
  143. package/src/workflows/discover-workflows.ts +2 -2
  144. package/src/workflows/validate-workflow.ts +40 -40
  145. package/src/worktree/branch-freshness.ts +45 -45
  146. package/src/worktree/cleanup.ts +71 -69
  147. package/src/worktree/worktree-manager.ts +3 -1
  148. package/teams/default.team.md +12 -12
  149. package/teams/fast-fix.team.md +11 -11
  150. package/teams/implementation.team.md +18 -18
  151. package/teams/parallel-research.team.md +14 -14
  152. package/teams/research.team.md +11 -11
  153. package/teams/review.team.md +12 -12
  154. package/workflows/default.workflow.md +29 -29
  155. package/workflows/fast-fix.workflow.md +22 -22
  156. package/workflows/implementation.workflow.md +38 -38
  157. package/workflows/parallel-research.workflow.md +46 -46
  158. package/workflows/research.workflow.md +22 -22
  159. 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 { projectPiRoot } from "../utils/paths.ts";
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(projectPiRoot(cwd), "teams", "state", "subagents", `${id}.json`);
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
+ }