pi-crew 0.1.37 → 0.1.39

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 (162) hide show
  1. package/AGENTS.md +1 -1
  2. package/CHANGELOG.md +27 -0
  3. package/README.md +5 -0
  4. package/agents/analyst.md +11 -11
  5. package/agents/critic.md +11 -11
  6. package/agents/executor.md +11 -11
  7. package/agents/explorer.md +11 -11
  8. package/agents/planner.md +11 -11
  9. package/agents/reviewer.md +11 -11
  10. package/agents/security-reviewer.md +11 -11
  11. package/agents/test-engineer.md +11 -11
  12. package/agents/verifier.md +11 -11
  13. package/agents/writer.md +11 -11
  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 -297
  19. package/docs/research-extension-system.md +324 -324
  20. package/docs/research-optimization-plan.md +548 -548
  21. package/docs/research-pi-coding-agent.md +357 -357
  22. package/docs/research-source-pi-crew-reference.md +174 -174
  23. package/docs/resource-formats.md +10 -8
  24. package/docs/runtime-flow.md +148 -148
  25. package/docs/source-runtime-refactor-map.md +83 -83
  26. package/docs/usage.md +6 -0
  27. package/index.ts +6 -6
  28. package/package.json +3 -3
  29. package/schema.json +2 -2
  30. package/src/agents/agent-serializer.ts +34 -34
  31. package/src/config/config.ts +8 -4
  32. package/src/extension/cross-extension-rpc.ts +82 -82
  33. package/src/extension/import-index.ts +18 -2
  34. package/src/extension/register.ts +11 -1
  35. package/src/extension/registration/compaction-guard.ts +125 -125
  36. package/src/extension/registration/subagent-helpers.ts +30 -6
  37. package/src/extension/registration/subagent-tools.ts +8 -3
  38. package/src/extension/result-watcher.ts +98 -98
  39. package/src/extension/run-import.ts +12 -2
  40. package/src/extension/run-index.ts +12 -2
  41. package/src/extension/run-maintenance.ts +24 -24
  42. package/src/extension/team-tool/api.ts +54 -14
  43. package/src/extension/team-tool/cancel.ts +31 -31
  44. package/src/extension/team-tool/doctor.ts +179 -179
  45. package/src/extension/team-tool/inspect.ts +41 -41
  46. package/src/extension/team-tool/lifecycle-actions.ts +79 -79
  47. package/src/extension/team-tool/plan.ts +19 -19
  48. package/src/extension/team-tool/status.ts +73 -73
  49. package/src/observability/correlation.ts +35 -35
  50. package/src/observability/event-to-metric.ts +54 -54
  51. package/src/observability/exporters/adapter.ts +24 -24
  52. package/src/observability/exporters/otlp-exporter.ts +65 -65
  53. package/src/observability/exporters/prometheus-exporter.ts +47 -47
  54. package/src/observability/metric-registry.ts +72 -72
  55. package/src/observability/metric-retention.ts +46 -46
  56. package/src/observability/metric-sink.ts +51 -51
  57. package/src/observability/metrics-primitives.ts +166 -166
  58. package/src/prompt/prompt-runtime.ts +68 -68
  59. package/src/runtime/agent-control.ts +64 -64
  60. package/src/runtime/agent-memory.ts +72 -72
  61. package/src/runtime/agent-observability.ts +114 -113
  62. package/src/runtime/async-marker.ts +26 -26
  63. package/src/runtime/background-runner.ts +53 -53
  64. package/src/runtime/crash-recovery.ts +56 -56
  65. package/src/runtime/crew-agent-records.ts +54 -9
  66. package/src/runtime/crew-agent-runtime.ts +58 -58
  67. package/src/runtime/deadletter.ts +36 -36
  68. package/src/runtime/direct-run.ts +35 -35
  69. package/src/runtime/foreground-control.ts +82 -82
  70. package/src/runtime/green-contract.ts +46 -46
  71. package/src/runtime/group-join.ts +88 -88
  72. package/src/runtime/heartbeat-gradient.ts +28 -28
  73. package/src/runtime/heartbeat-watcher.ts +80 -80
  74. package/src/runtime/live-agent-control.ts +87 -78
  75. package/src/runtime/live-agent-manager.ts +85 -85
  76. package/src/runtime/live-control-realtime.ts +36 -36
  77. package/src/runtime/live-session-runtime.ts +299 -299
  78. package/src/runtime/manifest-cache.ts +248 -212
  79. package/src/runtime/model-fallback.ts +261 -261
  80. package/src/runtime/parallel-research.ts +44 -44
  81. package/src/runtime/parallel-utils.ts +99 -99
  82. package/src/runtime/pi-json-output.ts +111 -111
  83. package/src/runtime/policy-engine.ts +78 -78
  84. package/src/runtime/post-exit-stdio-guard.ts +86 -86
  85. package/src/runtime/process-status.ts +56 -56
  86. package/src/runtime/progress-event-coalescer.ts +43 -43
  87. package/src/runtime/recovery-recipes.ts +74 -74
  88. package/src/runtime/retry-executor.ts +59 -59
  89. package/src/runtime/role-permission.ts +39 -39
  90. package/src/runtime/session-usage.ts +79 -79
  91. package/src/runtime/sidechain-output.ts +28 -28
  92. package/src/runtime/subagent-manager.ts +80 -12
  93. package/src/runtime/task-display.ts +38 -38
  94. package/src/runtime/task-output-context.ts +127 -106
  95. package/src/runtime/task-runner/live-executor.ts +98 -98
  96. package/src/runtime/task-runner/progress.ts +111 -111
  97. package/src/runtime/task-runner/result-utils.ts +14 -14
  98. package/src/runtime/task-runner/state-helpers.ts +22 -22
  99. package/src/runtime/team-runner.ts +1 -1
  100. package/src/runtime/worker-heartbeat.ts +21 -21
  101. package/src/runtime/worker-startup.ts +57 -57
  102. package/src/schema/config-schema.ts +21 -21
  103. package/src/schema/team-tool-schema.ts +100 -100
  104. package/src/state/artifact-store.ts +122 -108
  105. package/src/state/contracts.ts +105 -105
  106. package/src/state/jsonl-writer.ts +77 -77
  107. package/src/state/mailbox.ts +67 -22
  108. package/src/state/state-store.ts +36 -5
  109. package/src/state/task-claims.ts +42 -42
  110. package/src/state/usage.ts +29 -29
  111. package/src/subagents/async-entry.ts +1 -1
  112. package/src/subagents/index.ts +3 -3
  113. package/src/subagents/live/control.ts +1 -1
  114. package/src/subagents/live/manager.ts +1 -1
  115. package/src/subagents/live/realtime.ts +1 -1
  116. package/src/subagents/live/session-runtime.ts +1 -1
  117. package/src/subagents/manager.ts +1 -1
  118. package/src/subagents/spawn.ts +1 -1
  119. package/src/teams/discover-teams.ts +27 -5
  120. package/src/teams/team-serializer.ts +38 -36
  121. package/src/types/diff.d.ts +18 -18
  122. package/src/ui/crew-footer.ts +101 -101
  123. package/src/ui/crew-select-list.ts +111 -111
  124. package/src/ui/dashboard-panes/metrics-pane.ts +34 -34
  125. package/src/ui/dynamic-border.ts +25 -25
  126. package/src/ui/layout-primitives.ts +106 -106
  127. package/src/ui/loaders.ts +158 -158
  128. package/src/ui/mascot.ts +441 -441
  129. package/src/ui/render-diff.ts +119 -119
  130. package/src/ui/run-dashboard.ts +5 -2
  131. package/src/ui/run-snapshot-cache.ts +19 -8
  132. package/src/ui/spinner.ts +17 -17
  133. package/src/ui/status-colors.ts +54 -54
  134. package/src/ui/syntax-highlight.ts +116 -116
  135. package/src/ui/transcript-viewer.ts +15 -1
  136. package/src/utils/completion-dedupe.ts +63 -63
  137. package/src/utils/file-coalescer.ts +84 -84
  138. package/src/utils/frontmatter.ts +36 -36
  139. package/src/utils/fs-watch.ts +31 -31
  140. package/src/utils/git.ts +262 -262
  141. package/src/utils/ids.ts +12 -12
  142. package/src/utils/names.ts +26 -26
  143. package/src/utils/paths.ts +3 -2
  144. package/src/utils/safe-paths.ts +34 -0
  145. package/src/utils/sleep.ts +32 -32
  146. package/src/utils/timings.ts +31 -31
  147. package/src/utils/visual.ts +159 -159
  148. package/src/workflows/discover-workflows.ts +30 -3
  149. package/src/workflows/validate-workflow.ts +40 -40
  150. package/src/worktree/branch-freshness.ts +45 -45
  151. package/teams/default.team.md +12 -12
  152. package/teams/fast-fix.team.md +11 -11
  153. package/teams/implementation.team.md +18 -18
  154. package/teams/parallel-research.team.md +14 -14
  155. package/teams/research.team.md +11 -11
  156. package/teams/review.team.md +12 -12
  157. package/workflows/default.workflow.md +29 -29
  158. package/workflows/fast-fix.workflow.md +22 -22
  159. package/workflows/implementation.workflow.md +38 -38
  160. package/workflows/parallel-research.workflow.md +46 -46
  161. package/workflows/research.workflow.md +22 -22
  162. package/workflows/review.workflow.md +30 -30
@@ -1,100 +1,100 @@
1
- import { Type } from "typebox";
2
-
3
- const SkillOverride = Type.Unsafe({
4
- type: ["string", "array", "boolean"],
5
- items: { type: "string" },
6
- description: "Skill name(s) to inject, array of skill names, or false to disable role defaults.",
7
- });
8
-
9
- export const TeamToolParams = Type.Object({
10
- action: Type.Optional(Type.Union([
11
- Type.Literal("run"),
12
- Type.Literal("plan"),
13
- Type.Literal("status"),
14
- Type.Literal("list"),
15
- Type.Literal("get"),
16
- Type.Literal("cancel"),
17
- Type.Literal("resume"),
18
- Type.Literal("create"),
19
- Type.Literal("update"),
20
- Type.Literal("delete"),
21
- Type.Literal("doctor"),
22
- Type.Literal("cleanup"),
23
- Type.Literal("events"),
24
- Type.Literal("artifacts"),
25
- Type.Literal("worktrees"),
26
- Type.Literal("forget"),
27
- Type.Literal("summary"),
28
- Type.Literal("prune"),
29
- Type.Literal("export"),
30
- Type.Literal("import"),
31
- Type.Literal("imports"),
32
- Type.Literal("help"),
33
- Type.Literal("validate"),
34
- Type.Literal("config"),
35
- Type.Literal("init"),
36
- Type.Literal("recommend"),
37
- Type.Literal("autonomy"),
38
- Type.Literal("api"),
39
- ], { description: "Team action. Defaults to 'list' when omitted." })),
40
- resource: Type.Optional(Type.Union([
41
- Type.Literal("agent"),
42
- Type.Literal("team"),
43
- Type.Literal("workflow"),
44
- ], { description: "Resource kind for get/create/update/delete/list. Defaults to all for list." })),
45
- team: Type.Optional(Type.String({ description: "Team name, e.g. default or implementation." })),
46
- workflow: Type.Optional(Type.String({ description: "Workflow name, e.g. default or review." })),
47
- role: Type.Optional(Type.String({ description: "Role name to run directly within a team." })),
48
- agent: Type.Optional(Type.String({ description: "Agent name to inspect or run directly." })),
49
- goal: Type.Optional(Type.String({ description: "High-level objective for a team run." })),
50
- task: Type.Optional(Type.String({ description: "Concrete task text for direct role/agent execution." })),
51
- runId: Type.Optional(Type.String({ description: "Run ID for status, cancel, or resume." })),
52
- async: Type.Optional(Type.Boolean({ description: "Run in background when execution support is enabled." })),
53
- workspaceMode: Type.Optional(Type.Union([
54
- Type.Literal("single"),
55
- Type.Literal("worktree"),
56
- ], { description: "Workspace isolation mode. Worktree mode is planned after MVP." })),
57
- context: Type.Optional(Type.Union([
58
- Type.Literal("fresh"),
59
- Type.Literal("fork"),
60
- ], { description: "Child context mode for workers." })),
61
- cwd: Type.Optional(Type.String({ description: "Working directory override." })),
62
- model: Type.Optional(Type.String({ description: "Model override for direct runs." })),
63
- skill: Type.Optional(SkillOverride),
64
- scope: Type.Optional(Type.Union([
65
- Type.Literal("user"),
66
- Type.Literal("project"),
67
- Type.Literal("both"),
68
- ], { description: "Resource scope for discovery or management." })),
69
- config: Type.Optional(Type.Unsafe({ description: "Resource config for management actions." })),
70
- dryRun: Type.Optional(Type.Boolean({ description: "Preview a management mutation without writing files." })),
71
- confirm: Type.Optional(Type.Boolean({ description: "Required for destructive management actions." })),
72
- force: Type.Optional(Type.Boolean({ description: "Override reference checks for destructive management actions." })),
73
- keep: Type.Optional(Type.Integer({ minimum: 0, description: "Number of finished runs to keep for prune." })),
74
- updateReferences: Type.Optional(Type.Boolean({ description: "When renaming agents or workflows, update team references in the same project/user scope." })),
75
- });
76
-
77
- export interface TeamToolParamsValue {
78
- action?: "run" | "plan" | "status" | "list" | "get" | "cancel" | "resume" | "create" | "update" | "delete" | "doctor" | "cleanup" | "events" | "artifacts" | "worktrees" | "forget" | "summary" | "prune" | "export" | "import" | "imports" | "help" | "validate" | "config" | "init" | "recommend" | "autonomy" | "api";
79
- resource?: "agent" | "team" | "workflow";
80
- team?: string;
81
- workflow?: string;
82
- role?: string;
83
- agent?: string;
84
- goal?: string;
85
- task?: string;
86
- runId?: string;
87
- async?: boolean;
88
- workspaceMode?: "single" | "worktree";
89
- context?: "fresh" | "fork";
90
- cwd?: string;
91
- model?: string;
92
- skill?: string | string[] | boolean;
93
- scope?: "user" | "project" | "both";
94
- config?: unknown;
95
- dryRun?: boolean;
96
- confirm?: boolean;
97
- force?: boolean;
98
- keep?: number;
99
- updateReferences?: boolean;
100
- }
1
+ import { Type } from "typebox";
2
+
3
+ const SkillOverride = Type.Unsafe({
4
+ type: ["string", "array", "boolean"],
5
+ items: { type: "string" },
6
+ description: "Skill name(s) to inject, array of skill names, or false to disable role defaults.",
7
+ });
8
+
9
+ export const TeamToolParams = Type.Object({
10
+ action: Type.Optional(Type.Union([
11
+ Type.Literal("run"),
12
+ Type.Literal("plan"),
13
+ Type.Literal("status"),
14
+ Type.Literal("list"),
15
+ Type.Literal("get"),
16
+ Type.Literal("cancel"),
17
+ Type.Literal("resume"),
18
+ Type.Literal("create"),
19
+ Type.Literal("update"),
20
+ Type.Literal("delete"),
21
+ Type.Literal("doctor"),
22
+ Type.Literal("cleanup"),
23
+ Type.Literal("events"),
24
+ Type.Literal("artifacts"),
25
+ Type.Literal("worktrees"),
26
+ Type.Literal("forget"),
27
+ Type.Literal("summary"),
28
+ Type.Literal("prune"),
29
+ Type.Literal("export"),
30
+ Type.Literal("import"),
31
+ Type.Literal("imports"),
32
+ Type.Literal("help"),
33
+ Type.Literal("validate"),
34
+ Type.Literal("config"),
35
+ Type.Literal("init"),
36
+ Type.Literal("recommend"),
37
+ Type.Literal("autonomy"),
38
+ Type.Literal("api"),
39
+ ], { description: "Team action. Defaults to 'list' when omitted." })),
40
+ resource: Type.Optional(Type.Union([
41
+ Type.Literal("agent"),
42
+ Type.Literal("team"),
43
+ Type.Literal("workflow"),
44
+ ], { description: "Resource kind for get/create/update/delete/list. Defaults to all for list." })),
45
+ team: Type.Optional(Type.String({ description: "Team name, e.g. default or implementation." })),
46
+ workflow: Type.Optional(Type.String({ description: "Workflow name, e.g. default or review." })),
47
+ role: Type.Optional(Type.String({ description: "Role name to run directly within a team." })),
48
+ agent: Type.Optional(Type.String({ description: "Agent name to inspect or run directly." })),
49
+ goal: Type.Optional(Type.String({ description: "High-level objective for a team run." })),
50
+ task: Type.Optional(Type.String({ description: "Concrete task text for direct role/agent execution." })),
51
+ runId: Type.Optional(Type.String({ description: "Run ID for status, cancel, or resume." })),
52
+ async: Type.Optional(Type.Boolean({ description: "Run in background when execution support is enabled." })),
53
+ workspaceMode: Type.Optional(Type.Union([
54
+ Type.Literal("single"),
55
+ Type.Literal("worktree"),
56
+ ], { description: "Workspace isolation mode. Worktree mode is planned after MVP." })),
57
+ context: Type.Optional(Type.Union([
58
+ Type.Literal("fresh"),
59
+ Type.Literal("fork"),
60
+ ], { description: "Child context mode for workers." })),
61
+ cwd: Type.Optional(Type.String({ description: "Working directory override." })),
62
+ model: Type.Optional(Type.String({ description: "Model override for direct runs." })),
63
+ skill: Type.Optional(SkillOverride),
64
+ scope: Type.Optional(Type.Union([
65
+ Type.Literal("user"),
66
+ Type.Literal("project"),
67
+ Type.Literal("both"),
68
+ ], { description: "Resource scope for discovery or management." })),
69
+ config: Type.Optional(Type.Unsafe({ description: "Resource config for management actions." })),
70
+ dryRun: Type.Optional(Type.Boolean({ description: "Preview a management mutation without writing files." })),
71
+ confirm: Type.Optional(Type.Boolean({ description: "Required for destructive management actions." })),
72
+ force: Type.Optional(Type.Boolean({ description: "Override reference checks for destructive management actions." })),
73
+ keep: Type.Optional(Type.Integer({ minimum: 0, description: "Number of finished runs to keep for prune." })),
74
+ updateReferences: Type.Optional(Type.Boolean({ description: "When renaming agents or workflows, update team references in the same project/user scope." })),
75
+ });
76
+
77
+ export interface TeamToolParamsValue {
78
+ action?: "run" | "plan" | "status" | "list" | "get" | "cancel" | "resume" | "create" | "update" | "delete" | "doctor" | "cleanup" | "events" | "artifacts" | "worktrees" | "forget" | "summary" | "prune" | "export" | "import" | "imports" | "help" | "validate" | "config" | "init" | "recommend" | "autonomy" | "api";
79
+ resource?: "agent" | "team" | "workflow";
80
+ team?: string;
81
+ workflow?: string;
82
+ role?: string;
83
+ agent?: string;
84
+ goal?: string;
85
+ task?: string;
86
+ runId?: string;
87
+ async?: boolean;
88
+ workspaceMode?: "single" | "worktree";
89
+ context?: "fresh" | "fork";
90
+ cwd?: string;
91
+ model?: string;
92
+ skill?: string | string[] | boolean;
93
+ scope?: "user" | "project" | "both";
94
+ config?: unknown;
95
+ dryRun?: boolean;
96
+ confirm?: boolean;
97
+ force?: boolean;
98
+ keep?: number;
99
+ updateReferences?: boolean;
100
+ }
@@ -1,108 +1,122 @@
1
- import * as fs from "node:fs";
2
- import * as path from "node:path";
3
- import { createHash } from "node:crypto";
4
- import type { ArtifactDescriptor } from "./types.ts";
5
- import { atomicWriteFile } from "./atomic-write.ts";
6
-
7
- function hashContent(content: string): string {
8
- return createHash("sha256").update(content).digest("hex");
9
- }
10
-
11
- export const CLEANUP_MARKER_FILE = ".last-cleanup";
12
-
13
- export interface ArtifactWriteOptions {
14
- kind: ArtifactDescriptor["kind"];
15
- relativePath: string;
16
- content: string;
17
- producer: string;
18
- retention?: ArtifactDescriptor["retention"];
19
- }
20
-
21
- export interface ArtifactCleanupOptions {
22
- maxAgeDays: number;
23
- maxAgeMs?: number;
24
- markerFile?: string;
25
- scanGraceMs?: number;
26
- }
27
-
28
- function parseAgeDays(value: unknown): number | undefined {
29
- if (typeof value !== "number" || !Number.isFinite(value) || value < 0) return undefined;
30
- return Math.floor(value);
31
- }
32
-
33
- function nowMs(): number {
34
- return Date.now();
35
- }
36
-
37
- function readMarkerMtime(artifactsRoot: string, markerFile: string): number | undefined {
38
- try {
39
- return fs.statSync(path.join(artifactsRoot, markerFile)).mtimeMs;
40
- } catch {
41
- return undefined;
42
- }
43
- }
44
-
45
- function shouldCleanup(artifactsRoot: string, markerFile: string, scanGraceMs: number): boolean {
46
- const marker = readMarkerMtime(artifactsRoot, markerFile);
47
- if (marker === undefined) return true;
48
- return nowMs() - marker >= scanGraceMs;
49
- }
50
-
51
- export function writeCleanupMarker(artifactsRoot: string, markerFile: string): void {
52
- fs.mkdirSync(artifactsRoot, { recursive: true });
53
- fs.writeFileSync(path.join(artifactsRoot, markerFile), String(nowMs()), "utf-8");
54
- }
55
-
56
- export function cleanupOldArtifacts(artifactsRoot: string, options: ArtifactCleanupOptions): void {
57
- if (!fs.existsSync(artifactsRoot)) return;
58
- const maxAgeDays = parseAgeDays(options.maxAgeDays);
59
- if (maxAgeDays === undefined) return;
60
- const markerFile = options.markerFile ?? CLEANUP_MARKER_FILE;
61
- const scanGraceMs = options.scanGraceMs ?? 24 * 60 * 60 * 1000;
62
- if (!shouldCleanup(artifactsRoot, markerFile, scanGraceMs)) return;
63
- const maxAgeMs = options.maxAgeMs ?? maxAgeDays * 24 * 60 * 60 * 1000;
64
- const cutoff = nowMs() - maxAgeMs;
65
- let didCleanup = false;
66
- try {
67
- const entries = fs.readdirSync(artifactsRoot, { withFileTypes: true });
68
- for (const entry of entries) {
69
- if (entry.name === markerFile) continue;
70
- const target = path.join(artifactsRoot, entry.name);
71
- try {
72
- const stat = fs.statSync(target);
73
- if (stat.mtimeMs >= cutoff) continue;
74
- if (stat.isDirectory()) {
75
- fs.rmSync(target, { recursive: true, force: true });
76
- } else {
77
- fs.unlinkSync(target);
78
- }
79
- didCleanup = true;
80
- } catch {
81
- // Ignore cleanup races and permission issues in best-effort mode.
82
- }
83
- }
84
- writeCleanupMarker(artifactsRoot, markerFile);
85
- } catch {
86
- // Ignore unreadable roots in best-effort mode.
87
- }
88
- if (!didCleanup) writeCleanupMarker(artifactsRoot, markerFile);
89
- }
90
-
91
- export function writeArtifact(artifactsRoot: string, options: ArtifactWriteOptions): ArtifactDescriptor {
92
- const normalizedRelativePath = options.relativePath.replaceAll("\\", "/").replace(/^\.\/+/, "");
93
- if (normalizedRelativePath.startsWith("../") || path.isAbsolute(normalizedRelativePath)) {
94
- throw new Error(`Invalid artifact path: ${options.relativePath}`);
95
- }
96
- const filePath = path.join(artifactsRoot, normalizedRelativePath);
97
- atomicWriteFile(filePath, options.content);
98
- const stats = fs.statSync(filePath);
99
- return {
100
- kind: options.kind,
101
- path: filePath,
102
- createdAt: new Date().toISOString(),
103
- producer: options.producer,
104
- sizeBytes: stats.size,
105
- contentHash: hashContent(options.content),
106
- retention: options.retention ?? "run",
107
- };
108
- }
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+ import { createHash } from "node:crypto";
4
+ import type { ArtifactDescriptor } from "./types.ts";
5
+ import { atomicWriteFile } from "./atomic-write.ts";
6
+ import { resolveRealContainedPath } from "../utils/safe-paths.ts";
7
+
8
+ function hashContent(content: string): string {
9
+ return createHash("sha256").update(content).digest("hex");
10
+ }
11
+
12
+ export const CLEANUP_MARKER_FILE = ".last-cleanup";
13
+
14
+ export interface ArtifactWriteOptions {
15
+ kind: ArtifactDescriptor["kind"];
16
+ relativePath: string;
17
+ content: string;
18
+ producer: string;
19
+ retention?: ArtifactDescriptor["retention"];
20
+ }
21
+
22
+ export interface ArtifactCleanupOptions {
23
+ maxAgeDays: number;
24
+ maxAgeMs?: number;
25
+ markerFile?: string;
26
+ scanGraceMs?: number;
27
+ }
28
+
29
+ function parseAgeDays(value: unknown): number | undefined {
30
+ if (typeof value !== "number" || !Number.isFinite(value) || value < 0) return undefined;
31
+ return Math.floor(value);
32
+ }
33
+
34
+ function nowMs(): number {
35
+ return Date.now();
36
+ }
37
+
38
+ function readMarkerMtime(artifactsRoot: string, markerFile: string): number | undefined {
39
+ try {
40
+ return fs.statSync(path.join(artifactsRoot, markerFile)).mtimeMs;
41
+ } catch {
42
+ return undefined;
43
+ }
44
+ }
45
+
46
+ function shouldCleanup(artifactsRoot: string, markerFile: string, scanGraceMs: number): boolean {
47
+ const marker = readMarkerMtime(artifactsRoot, markerFile);
48
+ if (marker === undefined) return true;
49
+ return nowMs() - marker >= scanGraceMs;
50
+ }
51
+
52
+ export function writeCleanupMarker(artifactsRoot: string, markerFile: string): void {
53
+ fs.mkdirSync(artifactsRoot, { recursive: true });
54
+ fs.writeFileSync(path.join(artifactsRoot, markerFile), String(nowMs()), "utf-8");
55
+ }
56
+
57
+ export function cleanupOldArtifacts(artifactsRoot: string, options: ArtifactCleanupOptions): void {
58
+ if (!fs.existsSync(artifactsRoot)) return;
59
+ const maxAgeDays = parseAgeDays(options.maxAgeDays);
60
+ if (maxAgeDays === undefined) return;
61
+ const markerFile = options.markerFile ?? CLEANUP_MARKER_FILE;
62
+ const scanGraceMs = options.scanGraceMs ?? 24 * 60 * 60 * 1000;
63
+ if (!shouldCleanup(artifactsRoot, markerFile, scanGraceMs)) return;
64
+ const maxAgeMs = options.maxAgeMs ?? maxAgeDays * 24 * 60 * 60 * 1000;
65
+ const cutoff = nowMs() - maxAgeMs;
66
+ let didCleanup = false;
67
+ try {
68
+ const entries = fs.readdirSync(artifactsRoot, { withFileTypes: true });
69
+ for (const entry of entries) {
70
+ if (entry.name === markerFile) continue;
71
+ const target = path.join(artifactsRoot, entry.name);
72
+ try {
73
+ const stat = fs.statSync(target);
74
+ if (stat.mtimeMs >= cutoff) continue;
75
+ if (stat.isDirectory()) {
76
+ fs.rmSync(target, { recursive: true, force: true });
77
+ } else {
78
+ fs.unlinkSync(target);
79
+ }
80
+ didCleanup = true;
81
+ } catch {
82
+ // Ignore cleanup races and permission issues in best-effort mode.
83
+ }
84
+ }
85
+ writeCleanupMarker(artifactsRoot, markerFile);
86
+ } catch {
87
+ // Ignore unreadable roots in best-effort mode.
88
+ }
89
+ if (!didCleanup) writeCleanupMarker(artifactsRoot, markerFile);
90
+ }
91
+
92
+ function resolveInside(baseDir: string, relativePath: string): string {
93
+ const normalizedRelativePath = relativePath.replaceAll("\\", "/").replace(/^\.\/+/, "");
94
+ if (!normalizedRelativePath || normalizedRelativePath.split("/").some((segment) => segment === "..") || path.isAbsolute(normalizedRelativePath)) {
95
+ throw new Error(`Invalid artifact path: ${relativePath}`);
96
+ }
97
+ const base = path.resolve(baseDir);
98
+ const resolved = path.resolve(base, normalizedRelativePath);
99
+ const relative = path.relative(base, resolved);
100
+ if (relative.startsWith("..") || path.isAbsolute(relative)) throw new Error(`Invalid artifact path: ${relativePath}`);
101
+ return resolved;
102
+ }
103
+
104
+ export function writeArtifact(artifactsRoot: string, options: ArtifactWriteOptions): ArtifactDescriptor {
105
+ const filePath = resolveInside(artifactsRoot, options.relativePath);
106
+ fs.mkdirSync(artifactsRoot, { recursive: true });
107
+ if (fs.lstatSync(artifactsRoot).isSymbolicLink()) throw new Error(`Path is outside ${path.dirname(artifactsRoot)}: ${artifactsRoot}`);
108
+ resolveRealContainedPath(path.dirname(artifactsRoot), path.basename(artifactsRoot));
109
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
110
+ resolveRealContainedPath(artifactsRoot, path.dirname(filePath));
111
+ atomicWriteFile(filePath, options.content);
112
+ const stats = fs.statSync(filePath);
113
+ return {
114
+ kind: options.kind,
115
+ path: filePath,
116
+ createdAt: new Date().toISOString(),
117
+ producer: options.producer,
118
+ sizeBytes: stats.size,
119
+ contentHash: hashContent(options.content),
120
+ retention: options.retention ?? "run",
121
+ };
122
+ }
@@ -1,105 +1,105 @@
1
- export const TEAM_RUN_STATUSES = ["queued", "planning", "running", "blocked", "completed", "failed", "cancelled"] as const;
2
- export type TeamRunStatus = typeof TEAM_RUN_STATUSES[number];
3
-
4
- export const TEAM_TASK_STATUSES = ["queued", "running", "completed", "failed", "cancelled", "skipped"] as const;
5
- export type TeamTaskStatus = typeof TEAM_TASK_STATUSES[number];
6
-
7
- export const TEAM_TERMINAL_RUN_STATUSES: ReadonlySet<TeamRunStatus> = new Set(["blocked", "completed", "failed", "cancelled"]);
8
- export const TEAM_TERMINAL_TASK_STATUSES: ReadonlySet<TeamTaskStatus> = new Set(["completed", "failed", "cancelled", "skipped"]);
9
-
10
- export const TEAM_RUN_STATUS_TRANSITIONS: Readonly<Record<TeamRunStatus, readonly TeamRunStatus[]>> = {
11
- queued: ["planning", "running", "cancelled", "failed"],
12
- planning: ["running", "blocked", "cancelled", "failed"],
13
- running: ["blocked", "completed", "failed", "cancelled"],
14
- blocked: ["running", "cancelled", "failed"],
15
- completed: ["running", "cancelled"],
16
- failed: ["running", "cancelled"],
17
- cancelled: ["running"],
18
- };
19
-
20
- export const TEAM_TASK_STATUS_TRANSITIONS: Readonly<Record<TeamTaskStatus, readonly TeamTaskStatus[]>> = {
21
- queued: ["running", "cancelled", "skipped", "failed"],
22
- running: ["completed", "failed", "cancelled", "queued"],
23
- completed: ["queued"],
24
- failed: ["queued", "cancelled"],
25
- cancelled: ["queued"],
26
- skipped: ["queued", "cancelled"],
27
- };
28
-
29
- export const TEAM_EVENT_TYPES = [
30
- "run.created",
31
- "run.queued",
32
- "run.planning",
33
- "run.running",
34
- "run.blocked",
35
- "run.completed",
36
- "run.failed",
37
- "run.cancelled",
38
- "task.started",
39
- "task.progress",
40
- "task.blocked",
41
- "task.green",
42
- "task.red",
43
- "task.completed",
44
- "task.failed",
45
- "task.cancelled",
46
- "task.skipped",
47
- "review.approved",
48
- "review.rejected",
49
- "policy.action",
50
- "policy.escalated",
51
- "recovery.attempted",
52
- "recovery.escalated",
53
- "branch.stale",
54
- "mailbox.timeout",
55
- "worktree.cleanup",
56
- "worktree.dirty",
57
- "async.spawned",
58
- "async.started",
59
- "async.completed",
60
- "async.failed",
61
- "async.stale",
62
- ] as const;
63
- export type TeamEventType = typeof TEAM_EVENT_TYPES[number];
64
-
65
- export const TEAM_WAKEABLE_EVENT_TYPES: ReadonlySet<TeamEventType> = new Set([
66
- "run.blocked",
67
- "run.completed",
68
- "run.failed",
69
- "run.cancelled",
70
- "task.completed",
71
- "task.failed",
72
- "task.cancelled",
73
- "task.skipped",
74
- "async.completed",
75
- "async.failed",
76
- "async.stale",
77
- ]);
78
-
79
- export function isTeamRunStatus(value: unknown): value is TeamRunStatus {
80
- return typeof value === "string" && TEAM_RUN_STATUSES.includes(value as TeamRunStatus);
81
- }
82
-
83
- export function isTeamTaskStatus(value: unknown): value is TeamTaskStatus {
84
- return typeof value === "string" && TEAM_TASK_STATUSES.includes(value as TeamTaskStatus);
85
- }
86
-
87
- export function isTerminalRunStatus(status: TeamRunStatus): boolean {
88
- return TEAM_TERMINAL_RUN_STATUSES.has(status);
89
- }
90
-
91
- export function isTerminalTaskStatus(status: TeamTaskStatus): boolean {
92
- return TEAM_TERMINAL_TASK_STATUSES.has(status);
93
- }
94
-
95
- export function canTransitionRunStatus(from: TeamRunStatus, to: TeamRunStatus): boolean {
96
- return from === to || (TEAM_RUN_STATUS_TRANSITIONS[from]?.includes(to) ?? false);
97
- }
98
-
99
- export function canTransitionTaskStatus(from: TeamTaskStatus, to: TeamTaskStatus): boolean {
100
- return from === to || (TEAM_TASK_STATUS_TRANSITIONS[from]?.includes(to) ?? false);
101
- }
102
-
103
- export function isWakeableTeamEventType(type: TeamEventType): boolean {
104
- return TEAM_WAKEABLE_EVENT_TYPES.has(type);
105
- }
1
+ export const TEAM_RUN_STATUSES = ["queued", "planning", "running", "blocked", "completed", "failed", "cancelled"] as const;
2
+ export type TeamRunStatus = typeof TEAM_RUN_STATUSES[number];
3
+
4
+ export const TEAM_TASK_STATUSES = ["queued", "running", "completed", "failed", "cancelled", "skipped"] as const;
5
+ export type TeamTaskStatus = typeof TEAM_TASK_STATUSES[number];
6
+
7
+ export const TEAM_TERMINAL_RUN_STATUSES: ReadonlySet<TeamRunStatus> = new Set(["blocked", "completed", "failed", "cancelled"]);
8
+ export const TEAM_TERMINAL_TASK_STATUSES: ReadonlySet<TeamTaskStatus> = new Set(["completed", "failed", "cancelled", "skipped"]);
9
+
10
+ export const TEAM_RUN_STATUS_TRANSITIONS: Readonly<Record<TeamRunStatus, readonly TeamRunStatus[]>> = {
11
+ queued: ["planning", "running", "cancelled", "failed"],
12
+ planning: ["running", "blocked", "cancelled", "failed"],
13
+ running: ["blocked", "completed", "failed", "cancelled"],
14
+ blocked: ["running", "cancelled", "failed"],
15
+ completed: ["running", "cancelled"],
16
+ failed: ["running", "cancelled"],
17
+ cancelled: ["running"],
18
+ };
19
+
20
+ export const TEAM_TASK_STATUS_TRANSITIONS: Readonly<Record<TeamTaskStatus, readonly TeamTaskStatus[]>> = {
21
+ queued: ["running", "cancelled", "skipped", "failed"],
22
+ running: ["completed", "failed", "cancelled", "queued"],
23
+ completed: ["queued"],
24
+ failed: ["queued", "cancelled"],
25
+ cancelled: ["queued"],
26
+ skipped: ["queued", "cancelled"],
27
+ };
28
+
29
+ export const TEAM_EVENT_TYPES = [
30
+ "run.created",
31
+ "run.queued",
32
+ "run.planning",
33
+ "run.running",
34
+ "run.blocked",
35
+ "run.completed",
36
+ "run.failed",
37
+ "run.cancelled",
38
+ "task.started",
39
+ "task.progress",
40
+ "task.blocked",
41
+ "task.green",
42
+ "task.red",
43
+ "task.completed",
44
+ "task.failed",
45
+ "task.cancelled",
46
+ "task.skipped",
47
+ "review.approved",
48
+ "review.rejected",
49
+ "policy.action",
50
+ "policy.escalated",
51
+ "recovery.attempted",
52
+ "recovery.escalated",
53
+ "branch.stale",
54
+ "mailbox.timeout",
55
+ "worktree.cleanup",
56
+ "worktree.dirty",
57
+ "async.spawned",
58
+ "async.started",
59
+ "async.completed",
60
+ "async.failed",
61
+ "async.stale",
62
+ ] as const;
63
+ export type TeamEventType = typeof TEAM_EVENT_TYPES[number];
64
+
65
+ export const TEAM_WAKEABLE_EVENT_TYPES: ReadonlySet<TeamEventType> = new Set([
66
+ "run.blocked",
67
+ "run.completed",
68
+ "run.failed",
69
+ "run.cancelled",
70
+ "task.completed",
71
+ "task.failed",
72
+ "task.cancelled",
73
+ "task.skipped",
74
+ "async.completed",
75
+ "async.failed",
76
+ "async.stale",
77
+ ]);
78
+
79
+ export function isTeamRunStatus(value: unknown): value is TeamRunStatus {
80
+ return typeof value === "string" && TEAM_RUN_STATUSES.includes(value as TeamRunStatus);
81
+ }
82
+
83
+ export function isTeamTaskStatus(value: unknown): value is TeamTaskStatus {
84
+ return typeof value === "string" && TEAM_TASK_STATUSES.includes(value as TeamTaskStatus);
85
+ }
86
+
87
+ export function isTerminalRunStatus(status: TeamRunStatus): boolean {
88
+ return TEAM_TERMINAL_RUN_STATUSES.has(status);
89
+ }
90
+
91
+ export function isTerminalTaskStatus(status: TeamTaskStatus): boolean {
92
+ return TEAM_TERMINAL_TASK_STATUSES.has(status);
93
+ }
94
+
95
+ export function canTransitionRunStatus(from: TeamRunStatus, to: TeamRunStatus): boolean {
96
+ return from === to || (TEAM_RUN_STATUS_TRANSITIONS[from]?.includes(to) ?? false);
97
+ }
98
+
99
+ export function canTransitionTaskStatus(from: TeamTaskStatus, to: TeamTaskStatus): boolean {
100
+ return from === to || (TEAM_TASK_STATUS_TRANSITIONS[from]?.includes(to) ?? false);
101
+ }
102
+
103
+ export function isWakeableTeamEventType(type: TeamEventType): boolean {
104
+ return TEAM_WAKEABLE_EVENT_TYPES.has(type);
105
+ }