pi-crew 0.1.45 → 0.1.46

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 (198) hide show
  1. package/README.md +5 -5
  2. package/agents/analyst.md +1 -1
  3. package/agents/critic.md +1 -1
  4. package/agents/executor.md +1 -1
  5. package/agents/explorer.md +1 -1
  6. package/agents/planner.md +1 -1
  7. package/agents/reviewer.md +1 -1
  8. package/agents/security-reviewer.md +1 -1
  9. package/agents/test-engineer.md +1 -1
  10. package/agents/verifier.md +1 -1
  11. package/agents/writer.md +1 -1
  12. package/docs/next-upgrade-roadmap.md +733 -0
  13. package/docs/refactor-tasks-phase3.md +394 -394
  14. package/docs/refactor-tasks-phase4.md +564 -564
  15. package/docs/refactor-tasks-phase5.md +402 -402
  16. package/docs/refactor-tasks-phase6.md +662 -662
  17. package/docs/research-awesome-agent-skills-distillation.md +100 -0
  18. package/docs/research-extension-examples.md +297 -297
  19. package/docs/research-extension-system.md +324 -324
  20. package/docs/research-oh-my-pi-distillation.md +322 -0
  21. package/docs/research-optimization-plan.md +548 -548
  22. package/docs/research-phase10-distillation.md +198 -198
  23. package/docs/research-phase11-distillation.md +201 -201
  24. package/docs/research-pi-coding-agent.md +357 -357
  25. package/docs/research-source-pi-crew-reference.md +174 -174
  26. package/docs/runtime-flow.md +148 -148
  27. package/docs/source-runtime-refactor-map.md +107 -83
  28. package/docs/usage.md +3 -3
  29. package/index.ts +6 -6
  30. package/install.mjs +52 -8
  31. package/package.json +1 -1
  32. package/schema.json +2 -1
  33. package/skills/async-worker-recovery/SKILL.md +42 -0
  34. package/skills/context-artifact-hygiene/SKILL.md +52 -0
  35. package/skills/delegation-patterns/SKILL.md +54 -0
  36. package/skills/mailbox-interactive/SKILL.md +40 -0
  37. package/skills/model-routing-context/SKILL.md +39 -0
  38. package/skills/multi-perspective-review/SKILL.md +58 -0
  39. package/skills/observability-reliability/SKILL.md +41 -0
  40. package/skills/ownership-session-security/SKILL.md +41 -0
  41. package/skills/pi-extension-lifecycle/SKILL.md +39 -0
  42. package/skills/requirements-to-task-packet/SKILL.md +63 -0
  43. package/skills/resource-discovery-config/SKILL.md +41 -0
  44. package/skills/runtime-state-reader/SKILL.md +44 -0
  45. package/skills/secure-agent-orchestration-review/SKILL.md +45 -0
  46. package/skills/state-mutation-locking/SKILL.md +42 -0
  47. package/skills/systematic-debugging/SKILL.md +67 -0
  48. package/skills/ui-render-performance/SKILL.md +39 -0
  49. package/skills/verification-before-done/SKILL.md +57 -0
  50. package/skills/worktree-isolation/SKILL.md +39 -0
  51. package/src/agents/agent-serializer.ts +34 -34
  52. package/src/agents/discover-agents.ts +12 -11
  53. package/src/config/config.ts +48 -24
  54. package/src/config/defaults.ts +14 -0
  55. package/src/extension/cross-extension-rpc.ts +82 -82
  56. package/src/extension/project-init.ts +62 -2
  57. package/src/extension/register.ts +11 -9
  58. package/src/extension/registration/commands.ts +32 -25
  59. package/src/extension/registration/compaction-guard.ts +125 -125
  60. package/src/extension/registration/subagent-helpers.ts +8 -0
  61. package/src/extension/registration/subagent-tools.ts +149 -148
  62. package/src/extension/registration/team-tool.ts +8 -6
  63. package/src/extension/run-bundle-schema.ts +89 -89
  64. package/src/extension/run-index.ts +13 -5
  65. package/src/extension/run-maintenance.ts +62 -43
  66. package/src/extension/team-tool/api.ts +25 -8
  67. package/src/extension/team-tool/cancel.ts +33 -4
  68. package/src/extension/team-tool/context.ts +5 -0
  69. package/src/extension/team-tool/handle-settings.ts +188 -188
  70. package/src/extension/team-tool/inspect.ts +41 -41
  71. package/src/extension/team-tool/lifecycle-actions.ts +91 -79
  72. package/src/extension/team-tool/plan.ts +19 -19
  73. package/src/extension/team-tool/respond.ts +37 -17
  74. package/src/extension/team-tool/run.ts +52 -10
  75. package/src/extension/team-tool/status.ts +12 -1
  76. package/src/extension/team-tool-types.ts +2 -0
  77. package/src/extension/team-tool.ts +32 -11
  78. package/src/i18n.ts +184 -184
  79. package/src/observability/event-to-metric.ts +8 -1
  80. package/src/observability/exporters/otlp-exporter.ts +77 -77
  81. package/src/prompt/prompt-runtime.ts +72 -72
  82. package/src/runtime/agent-control.ts +63 -63
  83. package/src/runtime/agent-memory.ts +72 -72
  84. package/src/runtime/agent-observability.ts +114 -114
  85. package/src/runtime/async-marker.ts +26 -26
  86. package/src/runtime/attention-events.ts +28 -28
  87. package/src/runtime/background-runner.ts +59 -53
  88. package/src/runtime/cancellation.ts +51 -0
  89. package/src/runtime/child-pi.ts +457 -444
  90. package/src/runtime/completion-guard.ts +190 -190
  91. package/src/runtime/crash-recovery.ts +1 -0
  92. package/src/runtime/crew-agent-records.ts +38 -6
  93. package/src/runtime/deadletter.ts +1 -0
  94. package/src/runtime/delivery-coordinator.ts +46 -25
  95. package/src/runtime/direct-run.ts +35 -35
  96. package/src/runtime/effectiveness.ts +76 -0
  97. package/src/runtime/foreground-control.ts +82 -82
  98. package/src/runtime/green-contract.ts +46 -46
  99. package/src/runtime/group-join.ts +106 -106
  100. package/src/runtime/heartbeat-gradient.ts +28 -28
  101. package/src/runtime/heartbeat-watcher.ts +124 -124
  102. package/src/runtime/live-agent-control.ts +88 -87
  103. package/src/runtime/live-agent-manager.ts +103 -85
  104. package/src/runtime/live-control-realtime.ts +36 -36
  105. package/src/runtime/live-session-runtime.ts +309 -305
  106. package/src/runtime/manifest-cache.ts +17 -2
  107. package/src/runtime/model-fallback.ts +6 -4
  108. package/src/runtime/parallel-research.ts +44 -44
  109. package/src/runtime/pi-args.ts +18 -3
  110. package/src/runtime/pi-json-output.ts +111 -111
  111. package/src/runtime/policy-engine.ts +79 -79
  112. package/src/runtime/process-status.ts +5 -1
  113. package/src/runtime/progress-event-coalescer.ts +43 -43
  114. package/src/runtime/recovery-recipes.ts +74 -74
  115. package/src/runtime/retry-executor.ts +81 -64
  116. package/src/runtime/role-permission.ts +39 -39
  117. package/src/runtime/runtime-resolver.ts +22 -6
  118. package/src/runtime/session-resources.ts +25 -25
  119. package/src/runtime/session-snapshot.ts +59 -59
  120. package/src/runtime/session-usage.ts +79 -79
  121. package/src/runtime/sidechain-output.ts +29 -29
  122. package/src/runtime/skill-instructions.ts +222 -0
  123. package/src/runtime/stale-reconciler.ts +4 -14
  124. package/src/runtime/subagent-manager.ts +3 -0
  125. package/src/runtime/supervisor-contact.ts +59 -59
  126. package/src/runtime/task-display.ts +38 -38
  127. package/src/runtime/task-output-context.ts +127 -127
  128. package/src/runtime/task-runner/capabilities.ts +78 -0
  129. package/src/runtime/task-runner/live-executor.ts +105 -101
  130. package/src/runtime/task-runner/progress.ts +119 -119
  131. package/src/runtime/task-runner/prompt-builder.ts +3 -1
  132. package/src/runtime/task-runner/prompt-pipeline.ts +64 -0
  133. package/src/runtime/task-runner/result-utils.ts +14 -14
  134. package/src/runtime/task-runner/state-helpers.ts +22 -22
  135. package/src/runtime/task-runner.ts +44 -5
  136. package/src/runtime/team-runner.ts +78 -15
  137. package/src/runtime/worker-heartbeat.ts +21 -21
  138. package/src/runtime/worker-startup.ts +57 -57
  139. package/src/schema/config-schema.ts +1 -0
  140. package/src/schema/team-tool-schema.ts +3 -3
  141. package/src/state/active-run-registry.ts +165 -0
  142. package/src/state/contracts.ts +1 -1
  143. package/src/state/mailbox.ts +44 -4
  144. package/src/state/state-store.ts +8 -1
  145. package/src/state/task-claims.ts +44 -44
  146. package/src/state/types.ts +44 -2
  147. package/src/state/usage.ts +29 -29
  148. package/src/subagents/async-entry.ts +1 -1
  149. package/src/subagents/index.ts +3 -3
  150. package/src/subagents/live/control.ts +1 -1
  151. package/src/subagents/live/manager.ts +1 -1
  152. package/src/subagents/live/realtime.ts +1 -1
  153. package/src/subagents/live/session-runtime.ts +1 -1
  154. package/src/subagents/manager.ts +1 -1
  155. package/src/subagents/spawn.ts +1 -1
  156. package/src/teams/team-config.ts +1 -0
  157. package/src/teams/team-serializer.ts +38 -38
  158. package/src/types/diff.d.ts +18 -18
  159. package/src/ui/crew-footer.ts +101 -101
  160. package/src/ui/crew-select-list.ts +111 -111
  161. package/src/ui/crew-widget.ts +4 -3
  162. package/src/ui/dashboard-panes/metrics-pane.ts +34 -34
  163. package/src/ui/dashboard-panes/progress-pane.ts +2 -0
  164. package/src/ui/dynamic-border.ts +25 -25
  165. package/src/ui/layout-primitives.ts +106 -106
  166. package/src/ui/loaders.ts +158 -158
  167. package/src/ui/render-diff.ts +119 -119
  168. package/src/ui/render-scheduler.ts +143 -143
  169. package/src/ui/run-snapshot-cache.ts +10 -2
  170. package/src/ui/snapshot-types.ts +2 -0
  171. package/src/ui/spinner.ts +17 -17
  172. package/src/ui/status-colors.ts +58 -58
  173. package/src/ui/syntax-highlight.ts +116 -116
  174. package/src/utils/atomic-write.ts +33 -33
  175. package/src/utils/completion-dedupe.ts +63 -63
  176. package/src/utils/frontmatter.ts +68 -68
  177. package/src/utils/git.ts +262 -262
  178. package/src/utils/ids.ts +12 -12
  179. package/src/utils/names.ts +27 -27
  180. package/src/utils/paths.ts +4 -2
  181. package/src/utils/redaction.ts +44 -44
  182. package/src/utils/safe-paths.ts +47 -47
  183. package/src/utils/sleep.ts +32 -32
  184. package/src/workflows/validate-workflow.ts +40 -40
  185. package/src/workflows/workflow-config.ts +1 -0
  186. package/src/worktree/branch-freshness.ts +45 -45
  187. package/teams/default.team.md +12 -12
  188. package/teams/fast-fix.team.md +11 -11
  189. package/teams/implementation.team.md +18 -18
  190. package/teams/parallel-research.team.md +14 -14
  191. package/teams/research.team.md +11 -11
  192. package/teams/review.team.md +12 -12
  193. package/workflows/default.workflow.md +29 -29
  194. package/workflows/fast-fix.workflow.md +22 -22
  195. package/workflows/implementation.workflow.md +38 -38
  196. package/workflows/parallel-research.workflow.md +46 -46
  197. package/workflows/research.workflow.md +22 -22
  198. package/workflows/review.workflow.md +30 -30
@@ -1,59 +1,59 @@
1
- import type { TeamRunManifest, TeamTaskState } from "../state/types.ts";
2
-
3
- /**
4
- * Creates a lightweight snapshot of task state for event emission.
5
- * Prevents mutation-during-callback issues by copying relevant fields.
6
- */
7
- export function snapshotTaskState(task: TeamTaskState): Readonly<TeamTaskState> {
8
- return {
9
- ...task,
10
- dependsOn: [...task.dependsOn],
11
- usage: task.usage ? { ...task.usage } : undefined,
12
- agentProgress: task.agentProgress ? { ...task.agentProgress } : undefined,
13
- heartbeat: task.heartbeat ? { ...task.heartbeat } : undefined,
14
- modelAttempts: task.modelAttempts?.map((a) => ({ ...a })),
15
- modelRouting: task.modelRouting ? { ...task.modelRouting } : undefined,
16
- claim: task.claim ? { ...task.claim } : undefined,
17
- checkpoint: task.checkpoint ? { ...task.checkpoint } : undefined,
18
- attempts: task.attempts?.map((a) => ({ ...a })),
19
- worktree: task.worktree ? { ...task.worktree } : undefined,
20
- };
21
- }
22
-
23
- /**
24
- * Session state snapshot for persistence before session switches.
25
- * Captures the minimal set of data needed to resume operations.
26
- */
27
- export interface SessionStateSnapshot {
28
- /** ISO timestamp of the snapshot */
29
- capturedAt: string;
30
- /** Active run IDs at time of snapshot */
31
- activeRunIds: string[];
32
- /** Number of pending deliveries */
33
- pendingDeliveryCount: number;
34
- /** Session generation counter */
35
- sessionGeneration: number;
36
- /** Summary of active tasks by status */
37
- taskSummary: Record<string, number>;
38
- }
39
-
40
- /**
41
- * Create a session state snapshot for pre-switch persistence.
42
- */
43
- export function createSessionSnapshot(
44
- activeRuns: TeamRunManifest[],
45
- pendingDeliveryCount: number,
46
- sessionGeneration: number,
47
- ): SessionStateSnapshot {
48
- const taskSummary: Record<string, number> = {};
49
- for (const run of activeRuns) {
50
- taskSummary[run.status] = (taskSummary[run.status] ?? 0) + 1;
51
- }
52
- return {
53
- capturedAt: new Date().toISOString(),
54
- activeRunIds: activeRuns.map((r) => r.runId),
55
- pendingDeliveryCount,
56
- sessionGeneration,
57
- taskSummary,
58
- };
59
- }
1
+ import type { TeamRunManifest, TeamTaskState } from "../state/types.ts";
2
+
3
+ /**
4
+ * Creates a lightweight snapshot of task state for event emission.
5
+ * Prevents mutation-during-callback issues by copying relevant fields.
6
+ */
7
+ export function snapshotTaskState(task: TeamTaskState): Readonly<TeamTaskState> {
8
+ return {
9
+ ...task,
10
+ dependsOn: [...task.dependsOn],
11
+ usage: task.usage ? { ...task.usage } : undefined,
12
+ agentProgress: task.agentProgress ? { ...task.agentProgress } : undefined,
13
+ heartbeat: task.heartbeat ? { ...task.heartbeat } : undefined,
14
+ modelAttempts: task.modelAttempts?.map((a) => ({ ...a })),
15
+ modelRouting: task.modelRouting ? { ...task.modelRouting } : undefined,
16
+ claim: task.claim ? { ...task.claim } : undefined,
17
+ checkpoint: task.checkpoint ? { ...task.checkpoint } : undefined,
18
+ attempts: task.attempts?.map((a) => ({ ...a })),
19
+ worktree: task.worktree ? { ...task.worktree } : undefined,
20
+ };
21
+ }
22
+
23
+ /**
24
+ * Session state snapshot for persistence before session switches.
25
+ * Captures the minimal set of data needed to resume operations.
26
+ */
27
+ export interface SessionStateSnapshot {
28
+ /** ISO timestamp of the snapshot */
29
+ capturedAt: string;
30
+ /** Active run IDs at time of snapshot */
31
+ activeRunIds: string[];
32
+ /** Number of pending deliveries */
33
+ pendingDeliveryCount: number;
34
+ /** Session generation counter */
35
+ sessionGeneration: number;
36
+ /** Summary of active tasks by status */
37
+ taskSummary: Record<string, number>;
38
+ }
39
+
40
+ /**
41
+ * Create a session state snapshot for pre-switch persistence.
42
+ */
43
+ export function createSessionSnapshot(
44
+ activeRuns: TeamRunManifest[],
45
+ pendingDeliveryCount: number,
46
+ sessionGeneration: number,
47
+ ): SessionStateSnapshot {
48
+ const taskSummary: Record<string, number> = {};
49
+ for (const run of activeRuns) {
50
+ taskSummary[run.status] = (taskSummary[run.status] ?? 0) + 1;
51
+ }
52
+ return {
53
+ capturedAt: new Date().toISOString(),
54
+ activeRunIds: activeRuns.map((r) => r.runId),
55
+ pendingDeliveryCount,
56
+ sessionGeneration,
57
+ taskSummary,
58
+ };
59
+ }
@@ -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
+ }
@@ -0,0 +1,222 @@
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import type { AgentConfig } from "../agents/agent-config.ts";
5
+ import type { TeamRole } from "../teams/team-config.ts";
6
+ import type { WorkflowStep } from "../workflows/workflow-config.ts";
7
+ import { isSafePathId, resolveContainedPath, resolveRealContainedPath } from "../utils/safe-paths.ts";
8
+
9
+ const PACKAGE_SKILLS_DIR = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "..", "skills");
10
+ const MAX_SKILL_CHARS = 1500;
11
+ const MAX_TOTAL_CHARS = 6000;
12
+ const MAX_SKILL_NAME_CHARS = 80;
13
+ const MAX_SELECTED_SKILLS = 32;
14
+ const SKILL_CACHE_MAX_ENTRIES = 128;
15
+
16
+ const DEFAULT_ROLE_SKILLS: Record<string, string[]> = {
17
+ explorer: ["read-only-explorer", "context-artifact-hygiene"],
18
+ analyst: ["read-only-explorer", "requirements-to-task-packet"],
19
+ planner: ["delegation-patterns", "requirements-to-task-packet"],
20
+ critic: ["read-only-explorer", "multi-perspective-review"],
21
+ executor: ["state-mutation-locking", "safe-bash", "verification-before-done"],
22
+ reviewer: ["read-only-explorer", "multi-perspective-review"],
23
+ "security-reviewer": ["secure-agent-orchestration-review", "ownership-session-security"],
24
+ "test-engineer": ["verification-before-done", "safe-bash"],
25
+ verifier: ["verification-before-done", "runtime-state-reader"],
26
+ writer: ["context-artifact-hygiene", "verify-evidence"],
27
+ };
28
+
29
+ export interface ResolveTaskSkillsInput {
30
+ role: string;
31
+ agent?: Pick<AgentConfig, "skills">;
32
+ teamRole?: Pick<TeamRole, "skills">;
33
+ step?: Pick<WorkflowStep, "skills">;
34
+ override?: string[] | false;
35
+ }
36
+
37
+ export interface RenderSkillInstructionsInput extends ResolveTaskSkillsInput {
38
+ cwd: string;
39
+ }
40
+
41
+ function isValidSkillName(name: string): boolean {
42
+ return name.length > 0 && name.length <= MAX_SKILL_NAME_CHARS && isSafePathId(name);
43
+ }
44
+
45
+ function sanitizeSkillName(name: string): string {
46
+ return name.replace(/[^A-Za-z0-9_-]/g, "_").slice(0, MAX_SKILL_NAME_CHARS) || "invalid";
47
+ }
48
+
49
+ function unique(items: string[]): string[] {
50
+ const seen = new Set<string>();
51
+ const result: string[] = [];
52
+ for (const item of items.map((entry) => entry.trim()).filter(Boolean)) {
53
+ if (!isValidSkillName(item)) continue;
54
+ if (seen.has(item)) continue;
55
+ seen.add(item);
56
+ result.push(item);
57
+ }
58
+ return result;
59
+ }
60
+
61
+ export function normalizeSkillOverride(value: string | string[] | boolean | undefined): string[] | false | undefined {
62
+ if (value === false) return false;
63
+ if (typeof value === "string") return value.split(",").map((entry) => entry.trim()).filter(Boolean);
64
+ if (value === true) return undefined;
65
+ if (Array.isArray(value)) return value.map((entry) => entry.trim()).filter(Boolean);
66
+ return undefined;
67
+ }
68
+
69
+ export function defaultSkillsForRole(role: string): string[] {
70
+ return DEFAULT_ROLE_SKILLS[role] ?? [];
71
+ }
72
+
73
+ function collectTaskSkillNames(input: ResolveTaskSkillsInput): string[] {
74
+ if (input.override === false) return [];
75
+ const roleDefaultsDisabled = input.teamRole?.skills === false || input.step?.skills === false;
76
+ const names = roleDefaultsDisabled ? [] : defaultSkillsForRole(input.role);
77
+ if (input.agent?.skills?.length) names.push(...input.agent.skills);
78
+ if (Array.isArray(input.teamRole?.skills)) names.push(...input.teamRole.skills);
79
+ if (Array.isArray(input.step?.skills)) names.push(...input.step.skills);
80
+ if (Array.isArray(input.override)) names.push(...input.override);
81
+ return unique(names);
82
+ }
83
+
84
+ export function resolveTaskSkillNames(input: ResolveTaskSkillsInput): string[] {
85
+ return collectTaskSkillNames(input).slice(0, MAX_SELECTED_SKILLS);
86
+ }
87
+
88
+ function candidateSkillDirs(cwd: string): Array<{ root: string; source: "project" | "package" }> {
89
+ return [
90
+ { root: path.resolve(cwd, "skills"), source: "project" },
91
+ { root: PACKAGE_SKILLS_DIR, source: "package" },
92
+ ];
93
+ }
94
+
95
+ interface CachedSkillMarkdown {
96
+ path: string;
97
+ source: "project" | "package";
98
+ content: string;
99
+ mtimeMs: number;
100
+ size: number;
101
+ }
102
+
103
+ const skillReadCache = new Map<string, CachedSkillMarkdown>();
104
+
105
+ function rememberSkill(key: string, value: CachedSkillMarkdown): CachedSkillMarkdown {
106
+ if (skillReadCache.has(key)) skillReadCache.delete(key);
107
+ skillReadCache.set(key, value);
108
+ while (skillReadCache.size > SKILL_CACHE_MAX_ENTRIES) {
109
+ const oldest = skillReadCache.keys().next().value;
110
+ if (!oldest) break;
111
+ skillReadCache.delete(oldest);
112
+ }
113
+ return value;
114
+ }
115
+
116
+ export function clearSkillInstructionCache(): void {
117
+ skillReadCache.clear();
118
+ }
119
+
120
+ function cachedSkillFresh(value: CachedSkillMarkdown): boolean {
121
+ try {
122
+ const stat = fs.statSync(value.path);
123
+ return stat.mtimeMs === value.mtimeMs && stat.size === value.size;
124
+ } catch {
125
+ return false;
126
+ }
127
+ }
128
+
129
+ function readSkillMarkdown(cwd: string, name: string): { path: string; source: "project" | "package"; content: string } | undefined {
130
+ if (!isValidSkillName(name)) return undefined;
131
+ const cacheKey = `${path.resolve(cwd)}:${name}`;
132
+ const cached = skillReadCache.get(cacheKey);
133
+ if (cached && cachedSkillFresh(cached)) return cached;
134
+ if (cached) skillReadCache.delete(cacheKey);
135
+ for (const entry of candidateSkillDirs(cwd)) {
136
+ try {
137
+ const relative = path.join(name, "SKILL.md");
138
+ const contained = resolveContainedPath(entry.root, relative);
139
+ if (!fs.existsSync(contained)) continue;
140
+ if (fs.lstatSync(contained).isSymbolicLink()) continue;
141
+ const filePath = resolveRealContainedPath(entry.root, relative);
142
+ const stat = fs.statSync(filePath);
143
+ return rememberSkill(cacheKey, { path: filePath, source: entry.source, content: fs.readFileSync(filePath, "utf-8"), mtimeMs: stat.mtimeMs, size: stat.size });
144
+ } catch {
145
+ continue;
146
+ }
147
+ }
148
+ return undefined;
149
+ }
150
+
151
+ function frontmatterDescription(content: string): string | undefined {
152
+ const match = /^---\r?\n([\s\S]*?)\r?\n---/.exec(content);
153
+ if (!match) return undefined;
154
+ const line = match[1].split(/\r?\n/).find((entry) => entry.startsWith("description:"));
155
+ return line?.slice("description:".length).trim();
156
+ }
157
+
158
+ function stripFrontmatter(content: string): string {
159
+ return content.replace(/^---\r?\n[\s\S]*?\r?\n---\r?\n/, "").trim();
160
+ }
161
+
162
+ function compactSkillContent(content: string): string {
163
+ const body = stripFrontmatter(content);
164
+ if (body.length <= MAX_SKILL_CHARS) return body;
165
+ const preferred = body.split(/\r?\n## Verification\r?\n/)[0]?.trim() ?? body;
166
+ const truncated = preferred.length > MAX_SKILL_CHARS ? preferred.slice(0, MAX_SKILL_CHARS - 40).trimEnd() : preferred;
167
+ return `${truncated}\n\n[skill instructions truncated]`;
168
+ }
169
+
170
+ export interface RenderedSkillInstructions {
171
+ names: string[];
172
+ paths: string[];
173
+ block: string;
174
+ }
175
+
176
+ export function renderSkillInstructions(input: RenderSkillInstructionsInput): RenderedSkillInstructions {
177
+ const allNames = collectTaskSkillNames(input);
178
+ const names = allNames.slice(0, MAX_SELECTED_SKILLS);
179
+ const overflowCount = Math.max(0, allNames.length - names.length);
180
+ if (names.length === 0) return { names, paths: [], block: "" };
181
+ const sections: string[] = [];
182
+ const skillPaths: string[] = [];
183
+ let total = 0;
184
+ let omittedCount = overflowCount;
185
+ const pushSection = (section: string): boolean => {
186
+ if (total + section.length > MAX_TOTAL_CHARS) return false;
187
+ sections.push(section);
188
+ total += section.length;
189
+ return true;
190
+ };
191
+ for (const name of names) {
192
+ const safeName = sanitizeSkillName(name);
193
+ const loaded = readSkillMarkdown(input.cwd, name);
194
+ if (!loaded) {
195
+ const missing = `## ${safeName}\n\nSkill '${safeName}' was selected but no SKILL.md file was found. Continue with the task packet and report this missing skill.`;
196
+ if (!pushSection(missing)) omittedCount += 1;
197
+ continue;
198
+ }
199
+ skillPaths.push(path.dirname(loaded.path));
200
+ const description = frontmatterDescription(loaded.content);
201
+ const source = loaded.source === "project" ? `project:skills/${safeName}` : `package:skills/${safeName}`;
202
+ const header = [`## ${safeName}`, description ? `Description: ${description}` : undefined, `Source: ${source}`].filter(Boolean).join("\n");
203
+ const section = `${header}\n\n${compactSkillContent(loaded.content)}`;
204
+ if (!pushSection(section)) omittedCount += 1;
205
+ }
206
+ if (omittedCount > 0) {
207
+ const summary = `## Omitted skills\n\n[omitted ${omittedCount} selected skill(s): skill instruction budget exceeded]`;
208
+ if (!pushSection(summary) && sections.length > 0) {
209
+ sections[sections.length - 1] = summary;
210
+ }
211
+ }
212
+ return {
213
+ names,
214
+ paths: [...new Set(skillPaths)],
215
+ block: [
216
+ "# Applicable Skills",
217
+ "The following skills were selected for this worker. Follow them when they match the current task. If a selected skill conflicts with the explicit task packet, project AGENTS.md, or user request, follow the stricter/higher-priority instruction and report the conflict.",
218
+ "",
219
+ sections.join("\n\n---\n\n"),
220
+ ].join("\n"),
221
+ };
222
+ }
@@ -1,9 +1,5 @@
1
- import * as fs from "node:fs";
2
- import * as path from "node:path";
3
1
  import type { TeamRunManifest, TeamTaskState } from "../state/types.ts";
4
2
  import { checkProcessLiveness } from "./process-status.ts";
5
- import { logInternalError } from "../utils/internal-error.ts";
6
- import { writeAtomicJson } from "../utils/atomic-write.ts";
7
3
 
8
4
  /**
9
5
  * Result of reconciling a single stale run.
@@ -16,6 +12,8 @@ export interface ReconcileResult {
16
12
  repaired: boolean;
17
13
  /** Human-readable detail */
18
14
  detail: string;
15
+ /** Repaired task state, returned to a locked caller for persistence. */
16
+ repairedTasks?: TeamTaskState[];
19
17
  }
20
18
 
21
19
  const STALE_ALIVE_PID_MS = 24 * 60 * 60 * 1000; // 24 hours
@@ -106,16 +104,6 @@ function repairStaleRun(
106
104
  return task;
107
105
  });
108
106
 
109
- // Write repaired tasks atomically
110
- const tasksPath = manifest.tasksPath;
111
- if (tasksPath) {
112
- try {
113
- writeAtomicJson(tasksPath, repairedTasks);
114
- } catch (error) {
115
- logInternalError("stale-reconciler.repair-tasks", error, `runId=${manifest.runId}`);
116
- }
117
- }
118
-
119
107
  return repairedTasks;
120
108
  }
121
109
 
@@ -167,6 +155,7 @@ export function reconcileStaleRun(
167
155
  verdict: "no_status",
168
156
  repaired: true,
169
157
  detail: `No PID; stale ${Math.round((now - updatedAt) / 3600_000)}h; repaired ${repaired.filter((t) => t.status === "cancelled").length} tasks`,
158
+ repairedTasks: repaired,
170
159
  };
171
160
  }
172
161
  return {
@@ -195,5 +184,6 @@ export function reconcileStaleRun(
195
184
  verdict: pidStatus.alive ? "pid_alive_stale" : "pid_dead",
196
185
  repaired: true,
197
186
  detail: `PID ${pid}: ${pidStatus.detail}; ${staleness.reason}; repaired ${repaired.filter((t) => t.status === "cancelled").length} tasks`,
187
+ repairedTasks: repaired,
198
188
  };
199
189
  }
@@ -17,6 +17,7 @@ export interface SubagentSpawnOptions {
17
17
  prompt: string;
18
18
  background: boolean;
19
19
  model?: string;
20
+ skill?: string | string[] | false;
20
21
  maxTurns?: number;
21
22
  ownerSessionGeneration?: number;
22
23
  }
@@ -34,6 +35,7 @@ export interface SubagentRecord {
34
35
  error?: string;
35
36
  resultConsumed?: boolean;
36
37
  model?: string;
38
+ skill?: string | string[] | false;
37
39
  background: boolean;
38
40
  ownerSessionGeneration?: number;
39
41
  stuckNotified?: boolean;
@@ -138,6 +140,7 @@ export class SubagentManager {
138
140
  status: options.background && this.runningBackground >= this.maxConcurrent ? "queued" : "running",
139
141
  startedAt: Date.now(),
140
142
  model: options.model,
143
+ skill: options.skill,
141
144
  background: options.background,
142
145
  ownerSessionGeneration: options.ownerSessionGeneration,
143
146
  };