pi-crew 0.3.6 → 0.3.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +17 -0
- package/package.json +1 -1
- package/src/agents/discover-agents.ts +2 -1
- package/src/config/config.ts +760 -229
- package/src/config/types.ts +34 -5
- package/src/extension/help.ts +1 -0
- package/src/extension/management.ts +2 -1
- package/src/extension/register.ts +1176 -255
- package/src/extension/registration/commands.ts +15 -2
- package/src/extension/registration/team-tool.ts +1 -1
- package/src/extension/session-summary.ts +11 -1
- package/src/extension/team-tool/api.ts +4 -1
- package/src/extension/team-tool/cache-control.ts +23 -0
- package/src/extension/team-tool/cancel.ts +27 -16
- package/src/extension/team-tool/context.ts +2 -0
- package/src/extension/team-tool/handle-settings.ts +2 -0
- package/src/extension/team-tool/health-monitor.ts +563 -0
- package/src/extension/team-tool/inspect.ts +10 -3
- package/src/extension/team-tool/lifecycle-actions.ts +12 -5
- package/src/extension/team-tool/respond.ts +6 -3
- package/src/extension/team-tool/status.ts +4 -1
- package/src/extension/team-tool-types.ts +2 -0
- package/src/extension/team-tool.ts +901 -177
- package/src/runtime/adaptive-plan.ts +1 -1
- package/src/runtime/child-pi.ts +15 -2
- package/src/runtime/crash-recovery.ts +30 -0
- package/src/runtime/foreground-watchdog.ts +129 -0
- package/src/runtime/manifest-cache.ts +4 -2
- package/src/runtime/pi-args.ts +3 -2
- package/src/runtime/run-tracker.ts +11 -0
- package/src/runtime/runtime-policy.ts +15 -2
- package/src/runtime/skill-instructions.ts +11 -0
- package/src/runtime/stale-reconciler.ts +322 -18
- package/src/runtime/task-runner.ts +8 -1
- package/src/schema/config-schema.ts +1 -0
- package/src/schema/team-tool-schema.ts +204 -76
- package/src/state/atomic-write.ts +2 -2
- package/src/state/locks.ts +19 -0
- package/src/state/mailbox.ts +22 -5
- package/src/state/state-store.ts +13 -3
- package/src/teams/discover-teams.ts +2 -1
- package/src/ui/run-event-bus.ts +2 -1
- package/src/ui/settings-overlay.ts +2 -0
- package/src/workflows/discover-workflows.ts +5 -1
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
import { Type } from "@sinclair/typebox";
|
|
2
2
|
|
|
3
3
|
const SkillOverride = Type.Unsafe({
|
|
4
|
-
description:
|
|
4
|
+
description:
|
|
5
|
+
"Skill name(s) to add to role/default skills, an array of skill names, or false to disable all injected skills for this run.",
|
|
5
6
|
anyOf: [
|
|
6
7
|
{ type: "string", maxLength: 2048 },
|
|
7
|
-
{
|
|
8
|
+
{
|
|
9
|
+
type: "array",
|
|
10
|
+
maxItems: 32,
|
|
11
|
+
items: { type: "string", maxLength: 80 },
|
|
12
|
+
},
|
|
8
13
|
{ type: "boolean" },
|
|
9
14
|
],
|
|
10
15
|
});
|
|
@@ -16,85 +21,208 @@ const FreeformConfig = Type.Unsafe({
|
|
|
16
21
|
});
|
|
17
22
|
|
|
18
23
|
export const TeamToolParams = Type.Object({
|
|
19
|
-
action: Type.Optional(
|
|
20
|
-
Type.
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
Type.
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
24
|
+
action: Type.Optional(
|
|
25
|
+
Type.Union(
|
|
26
|
+
[
|
|
27
|
+
Type.Literal("run"),
|
|
28
|
+
Type.Literal("parallel"),
|
|
29
|
+
Type.Literal("plan"),
|
|
30
|
+
Type.Literal("status"),
|
|
31
|
+
Type.Literal("wait"),
|
|
32
|
+
Type.Literal("list"),
|
|
33
|
+
Type.Literal("get"),
|
|
34
|
+
Type.Literal("cancel"),
|
|
35
|
+
Type.Literal("retry"),
|
|
36
|
+
Type.Literal("resume"),
|
|
37
|
+
Type.Literal("respond"),
|
|
38
|
+
Type.Literal("create"),
|
|
39
|
+
Type.Literal("update"),
|
|
40
|
+
Type.Literal("delete"),
|
|
41
|
+
Type.Literal("doctor"),
|
|
42
|
+
Type.Literal("cleanup"),
|
|
43
|
+
Type.Literal("events"),
|
|
44
|
+
Type.Literal("artifacts"),
|
|
45
|
+
Type.Literal("worktrees"),
|
|
46
|
+
Type.Literal("forget"),
|
|
47
|
+
Type.Literal("summary"),
|
|
48
|
+
Type.Literal("prune"),
|
|
49
|
+
Type.Literal("export"),
|
|
50
|
+
Type.Literal("import"),
|
|
51
|
+
Type.Literal("imports"),
|
|
52
|
+
Type.Literal("help"),
|
|
53
|
+
Type.Literal("validate"),
|
|
54
|
+
Type.Literal("config"),
|
|
55
|
+
Type.Literal("init"),
|
|
56
|
+
Type.Literal("recommend"),
|
|
57
|
+
Type.Literal("autonomy"),
|
|
58
|
+
Type.Literal("api"),
|
|
59
|
+
Type.Literal("settings"),
|
|
60
|
+
Type.Literal("steer"),
|
|
61
|
+
Type.Literal("health"),
|
|
62
|
+
],
|
|
63
|
+
{ description: "Team action. Defaults to 'list' when omitted." },
|
|
64
|
+
),
|
|
65
|
+
),
|
|
66
|
+
resource: Type.Optional(
|
|
67
|
+
Type.Union(
|
|
68
|
+
[
|
|
69
|
+
Type.Literal("agent"),
|
|
70
|
+
Type.Literal("team"),
|
|
71
|
+
Type.Literal("workflow"),
|
|
72
|
+
],
|
|
73
|
+
{
|
|
74
|
+
description:
|
|
75
|
+
"Resource kind for get/create/update/delete/list. Defaults to all for list.",
|
|
76
|
+
},
|
|
77
|
+
),
|
|
78
|
+
),
|
|
79
|
+
team: Type.Optional(
|
|
80
|
+
Type.String({
|
|
81
|
+
description: "Team name, e.g. default or implementation.",
|
|
82
|
+
}),
|
|
83
|
+
),
|
|
84
|
+
workflow: Type.Optional(
|
|
85
|
+
Type.String({ description: "Workflow name, e.g. default or review." }),
|
|
86
|
+
),
|
|
87
|
+
role: Type.Optional(
|
|
88
|
+
Type.String({
|
|
89
|
+
description: "Role name to run directly within a team.",
|
|
90
|
+
}),
|
|
91
|
+
),
|
|
92
|
+
agent: Type.Optional(
|
|
93
|
+
Type.String({ description: "Agent name to inspect or run directly." }),
|
|
94
|
+
),
|
|
95
|
+
goal: Type.Optional(
|
|
96
|
+
Type.String({ description: "High-level objective for a team run." }),
|
|
97
|
+
),
|
|
98
|
+
task: Type.Optional(
|
|
99
|
+
Type.String({
|
|
100
|
+
description: "Concrete task text for direct role/agent execution.",
|
|
101
|
+
}),
|
|
102
|
+
),
|
|
103
|
+
runId: Type.Optional(
|
|
104
|
+
Type.String({ description: "Run ID for status, cancel, or resume." }),
|
|
105
|
+
),
|
|
106
|
+
taskId: Type.Optional(
|
|
107
|
+
Type.String({ description: "Task ID for respond action." }),
|
|
108
|
+
),
|
|
109
|
+
message: Type.Optional(
|
|
110
|
+
Type.String({ description: "Message for respond action." }),
|
|
111
|
+
),
|
|
112
|
+
async: Type.Optional(
|
|
113
|
+
Type.Boolean({
|
|
114
|
+
description: "Run in background when execution support is enabled.",
|
|
115
|
+
}),
|
|
116
|
+
),
|
|
117
|
+
workspaceMode: Type.Optional(
|
|
118
|
+
Type.Union([Type.Literal("single"), Type.Literal("worktree")], {
|
|
119
|
+
description:
|
|
120
|
+
"Workspace isolation mode. Worktree mode is planned after MVP.",
|
|
121
|
+
}),
|
|
122
|
+
),
|
|
123
|
+
context: Type.Optional(
|
|
124
|
+
Type.Union([Type.Literal("fresh"), Type.Literal("fork")], {
|
|
125
|
+
description: "Child context mode for workers.",
|
|
126
|
+
}),
|
|
127
|
+
),
|
|
128
|
+
cwd: Type.Optional(
|
|
129
|
+
Type.String({ description: "Working directory override." }),
|
|
130
|
+
),
|
|
131
|
+
model: Type.Optional(
|
|
132
|
+
Type.String({ description: "Model override for direct runs." }),
|
|
133
|
+
),
|
|
79
134
|
skill: Type.Optional(SkillOverride),
|
|
80
|
-
scope: Type.Optional(
|
|
81
|
-
Type.
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
135
|
+
scope: Type.Optional(
|
|
136
|
+
Type.Union(
|
|
137
|
+
[
|
|
138
|
+
Type.Literal("user"),
|
|
139
|
+
Type.Literal("project"),
|
|
140
|
+
Type.Literal("both"),
|
|
141
|
+
],
|
|
142
|
+
{ description: "Resource scope for discovery or management." },
|
|
143
|
+
),
|
|
144
|
+
),
|
|
85
145
|
config: Type.Optional(FreeformConfig),
|
|
86
|
-
dryRun: Type.Optional(
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
146
|
+
dryRun: Type.Optional(
|
|
147
|
+
Type.Boolean({
|
|
148
|
+
description: "Preview a management mutation without writing files.",
|
|
149
|
+
}),
|
|
150
|
+
),
|
|
151
|
+
confirm: Type.Optional(
|
|
152
|
+
Type.Boolean({
|
|
153
|
+
description: "Required for destructive management actions.",
|
|
154
|
+
}),
|
|
155
|
+
),
|
|
156
|
+
force: Type.Optional(
|
|
157
|
+
Type.Boolean({
|
|
158
|
+
description:
|
|
159
|
+
"Override reference checks for destructive management actions.",
|
|
160
|
+
}),
|
|
161
|
+
),
|
|
162
|
+
keep: Type.Optional(
|
|
163
|
+
Type.Integer({
|
|
164
|
+
minimum: 0,
|
|
165
|
+
description: "Number of finished runs to keep for prune.",
|
|
166
|
+
}),
|
|
167
|
+
),
|
|
168
|
+
updateReferences: Type.Optional(
|
|
169
|
+
Type.Boolean({
|
|
170
|
+
description:
|
|
171
|
+
"When renaming agents or workflows, update team references in the same project/user scope.",
|
|
172
|
+
}),
|
|
173
|
+
),
|
|
174
|
+
replyTo: Type.Optional(
|
|
175
|
+
Type.String({
|
|
176
|
+
description:
|
|
177
|
+
"ID of the original mailbox message this is a reply to.",
|
|
178
|
+
}),
|
|
179
|
+
),
|
|
180
|
+
replyFrom: Type.Optional(
|
|
181
|
+
Type.String({ description: "Task ID sending the reply." }),
|
|
182
|
+
),
|
|
183
|
+
replyDeadline: Type.Optional(
|
|
184
|
+
Type.Integer({ description: "Ms epoch deadline for a reply." }),
|
|
185
|
+
),
|
|
94
186
|
});
|
|
95
187
|
|
|
96
188
|
export interface TeamToolParamsValue {
|
|
97
|
-
action?:
|
|
189
|
+
action?:
|
|
190
|
+
| "run"
|
|
191
|
+
| "parallel"
|
|
192
|
+
| "plan"
|
|
193
|
+
| "status"
|
|
194
|
+
| "wait"
|
|
195
|
+
| "list"
|
|
196
|
+
| "get"
|
|
197
|
+
| "cancel"
|
|
198
|
+
| "retry"
|
|
199
|
+
| "resume"
|
|
200
|
+
| "respond"
|
|
201
|
+
| "create"
|
|
202
|
+
| "update"
|
|
203
|
+
| "delete"
|
|
204
|
+
| "doctor"
|
|
205
|
+
| "cleanup"
|
|
206
|
+
| "events"
|
|
207
|
+
| "artifacts"
|
|
208
|
+
| "worktrees"
|
|
209
|
+
| "forget"
|
|
210
|
+
| "summary"
|
|
211
|
+
| "prune"
|
|
212
|
+
| "export"
|
|
213
|
+
| "import"
|
|
214
|
+
| "imports"
|
|
215
|
+
| "help"
|
|
216
|
+
| "validate"
|
|
217
|
+
| "config"
|
|
218
|
+
| "init"
|
|
219
|
+
| "recommend"
|
|
220
|
+
| "autonomy"
|
|
221
|
+
| "api"
|
|
222
|
+
| "settings"
|
|
223
|
+
| "steer"
|
|
224
|
+
| "invalidate"
|
|
225
|
+
| "health";
|
|
98
226
|
resource?: "agent" | "team" | "workflow";
|
|
99
227
|
team?: string;
|
|
100
228
|
workflow?: string;
|
|
@@ -102,7 +102,7 @@ export function atomicWriteFile(filePath: string, content: string): void {
|
|
|
102
102
|
// Write temp with restrictive permissions
|
|
103
103
|
const O_NOFOLLOW = typeof fs.constants.O_NOFOLLOW === "number" ? fs.constants.O_NOFOLLOW : 0;
|
|
104
104
|
try {
|
|
105
|
-
const fd = fs.openSync(tempPath, fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_EXCL | O_NOFOLLOW,
|
|
105
|
+
const fd = fs.openSync(tempPath, fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_EXCL | O_NOFOLLOW, 0o600);
|
|
106
106
|
// Post-open verification: on Windows O_NOFOLLOW is 0, so verify FD is a regular file
|
|
107
107
|
const openedStat = fs.fstatSync(fd);
|
|
108
108
|
if (!openedStat.isFile()) {
|
|
@@ -168,7 +168,7 @@ export async function atomicWriteFileAsync(filePath: string, content: string): P
|
|
|
168
168
|
const tempPath = `${filePath}.${process.pid}.${Date.now()}.${Math.random().toString(36).slice(2)}.tmp`;
|
|
169
169
|
try {
|
|
170
170
|
const O_NOFOLLOW = typeof fs.constants.O_NOFOLLOW === "number" ? fs.constants.O_NOFOLLOW : 0;
|
|
171
|
-
const fd = await fs.promises.open(tempPath, fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_EXCL | O_NOFOLLOW,
|
|
171
|
+
const fd = await fs.promises.open(tempPath, fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_EXCL | O_NOFOLLOW, 0o600);
|
|
172
172
|
// Post-open verification: on Windows O_NOFOLLOW is 0, so verify FD is a regular file
|
|
173
173
|
const openedStat = await fd.stat();
|
|
174
174
|
if (!openedStat.isFile()) {
|
package/src/state/locks.ts
CHANGED
|
@@ -113,6 +113,25 @@ async function acquireLockWithRetryAsync(filePath: string, staleMs: number): Pro
|
|
|
113
113
|
}
|
|
114
114
|
}
|
|
115
115
|
|
|
116
|
+
/**
|
|
117
|
+
* General-purpose file lock for arbitrary file paths.
|
|
118
|
+
* Uses the same O_EXCL atomic create strategy as run locks.
|
|
119
|
+
*/
|
|
120
|
+
export function withFileLockSync<T>(filePath: string, fn: () => T, options: RunLockOptions = {}): T {
|
|
121
|
+
const staleMs = options.staleMs ?? DEFAULT_STALE_MS;
|
|
122
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
123
|
+
acquireLockWithRetry(filePath, staleMs);
|
|
124
|
+
try {
|
|
125
|
+
return fn();
|
|
126
|
+
} finally {
|
|
127
|
+
try {
|
|
128
|
+
fs.rmSync(filePath, { force: true });
|
|
129
|
+
} catch {
|
|
130
|
+
// Best-effort lock cleanup.
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
116
135
|
export function withRunLockSync<T>(manifest: TeamRunManifest, fn: () => T, options: RunLockOptions = {}): T {
|
|
117
136
|
const filePath = lockPath(manifest);
|
|
118
137
|
const staleMs = options.staleMs ?? DEFAULT_STALE_MS;
|
package/src/state/mailbox.ts
CHANGED
|
@@ -68,7 +68,13 @@ function mailboxDir(manifest: TeamRunManifest): string {
|
|
|
68
68
|
function safeMailboxDir(manifest: TeamRunManifest, create = false): string {
|
|
69
69
|
const dir = mailboxDir(manifest);
|
|
70
70
|
if (create) fs.mkdirSync(dir, { recursive: true });
|
|
71
|
-
|
|
71
|
+
// SECURITY: When create=true, dir now exists and must be validated via
|
|
72
|
+
// resolveRealContainedPath. When create=false, missing dir must throw —
|
|
73
|
+
// never return an unvalidated bare path (bypasses containment checks).
|
|
74
|
+
if (!fs.existsSync(dir)) {
|
|
75
|
+
if (create) throw new Error(`Mailbox directory creation failed: ${dir}`);
|
|
76
|
+
return path.join(dir); // will throw in callers via resolveRealContainedPath on read
|
|
77
|
+
}
|
|
72
78
|
if (fs.lstatSync(dir).isSymbolicLink()) throw new Error(`Invalid mailbox directory: ${dir}`);
|
|
73
79
|
return resolveRealContainedPath(manifest.stateRoot, "mailbox");
|
|
74
80
|
}
|
|
@@ -93,8 +99,6 @@ function taskMailboxDir(manifest: TeamRunManifest, taskId: string, create = fals
|
|
|
93
99
|
const relative = path.relative(tasksRoot, resolved);
|
|
94
100
|
if (relative.startsWith("..") || path.isAbsolute(relative)) throw new Error(`Invalid mailbox task id: ${taskId}`);
|
|
95
101
|
if (create) fs.mkdirSync(resolved, { recursive: true });
|
|
96
|
-
if (!fs.existsSync(resolved)) return resolved;
|
|
97
|
-
if (fs.lstatSync(resolved).isSymbolicLink()) throw new Error(`Invalid mailbox task directory: ${resolved}`);
|
|
98
102
|
return resolveRealContainedPath(tasksRoot, normalizedTaskId);
|
|
99
103
|
}
|
|
100
104
|
|
|
@@ -118,8 +122,21 @@ function mailboxFile(manifest: TeamRunManifest, direction: MailboxDirection, tas
|
|
|
118
122
|
}
|
|
119
123
|
|
|
120
124
|
function deliveryFile(manifest: TeamRunManifest, create = false): string {
|
|
121
|
-
|
|
122
|
-
|
|
125
|
+
// Pass create=true to ensure mailbox dir exists before computing delivery.json path.
|
|
126
|
+
// This mirrors ensureRunMailbox() pattern — always create before computing nested paths.
|
|
127
|
+
// When create=false, a missing directory is tolerated (callers like readDeliveryState
|
|
128
|
+
// handle missing file via try/catch; but missing directory must not throw here).
|
|
129
|
+
try {
|
|
130
|
+
const parent = safeMailboxDir(manifest, create);
|
|
131
|
+
return safeMailboxFile(path.join(parent, "delivery.json"), parent);
|
|
132
|
+
} catch (err) {
|
|
133
|
+
if ((err as NodeJS.ErrnoException).code === "ENOENT") {
|
|
134
|
+
// Directory missing and create=false: return unvalidated path so callers
|
|
135
|
+
// (readDeliveryState) that have their own try/catch can handle gracefully.
|
|
136
|
+
return path.join(mailboxDir(manifest), "delivery.json");
|
|
137
|
+
}
|
|
138
|
+
throw err;
|
|
139
|
+
}
|
|
123
140
|
}
|
|
124
141
|
|
|
125
142
|
function ensureRunMailbox(manifest: TeamRunManifest): void {
|
package/src/state/state-store.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
2
|
import * as path from "node:path";
|
|
3
3
|
import type { TeamRunManifest, TeamTaskState } from "./types.ts";
|
|
4
|
-
import { canTransitionRunStatus } from "./contracts.ts";
|
|
4
|
+
import { canTransitionRunStatus, isTerminalRunStatus } from "./contracts.ts";
|
|
5
|
+
import { unregisterActiveRun } from "./active-run-registry.ts";
|
|
5
6
|
import { atomicWriteJson, atomicWriteJsonAsync, atomicWriteJsonCoalesced, readJsonFile } from "./atomic-write.ts";
|
|
6
7
|
import { appendEvent } from "./event-log.ts";
|
|
7
8
|
import { DEFAULT_CACHE, DEFAULT_PATHS } from "../config/defaults.ts";
|
|
@@ -61,9 +62,11 @@ function resolveRunStateRoot(cwd: string, runId: string): string | undefined {
|
|
|
61
62
|
assertSafePathId("runId", runId);
|
|
62
63
|
const runsRoot = path.join(scopeBaseRoot(cwd), DEFAULT_PATHS.state.runsSubdir);
|
|
63
64
|
const scopedPath = resolveContainedRelativePath(runsRoot, runId, "runId");
|
|
64
|
-
if (!fs.existsSync(scopedPath)) return undefined;
|
|
65
65
|
try {
|
|
66
|
-
|
|
66
|
+
// Single atomic validation: resolves through symlinks via realpath,
|
|
67
|
+
// verifies containment within runsRoot, and throws ENOENT if missing.
|
|
68
|
+
// Eliminates the TOCTOU window from the previous existsSync + lstatSync
|
|
69
|
+
// + resolveRealContainedPath sequence.
|
|
67
70
|
resolveRealContainedPath(runsRoot, runId);
|
|
68
71
|
} catch {
|
|
69
72
|
return undefined;
|
|
@@ -242,6 +245,13 @@ export function updateRunStatus(manifest: TeamRunManifest, status: TeamRunManife
|
|
|
242
245
|
}
|
|
243
246
|
const updated: TeamRunManifest = { ...manifest, status, updatedAt: new Date().toISOString(), summary: summary ?? manifest.summary };
|
|
244
247
|
saveRunManifest(updated);
|
|
248
|
+
// Unregister from active-run-index when run reaches a terminal status.
|
|
249
|
+
// Without this, stale entries accumulate (e.g. integration tests in /tmp) and
|
|
250
|
+
// Pi UI shows ghost "queued" runs that are actually completed/failed/cancelled.
|
|
251
|
+
// Note: "blocked" is excluded because blocked runs can be unblocked later.
|
|
252
|
+
if (status === "completed" || status === "failed" || status === "cancelled") {
|
|
253
|
+
try { unregisterActiveRun(updated.runId); } catch { /* non-critical */ }
|
|
254
|
+
}
|
|
245
255
|
appendEvent(updated.eventsPath, {
|
|
246
256
|
type: `run.${status}`,
|
|
247
257
|
runId: updated.runId,
|
|
@@ -107,7 +107,8 @@ export function discoverTeams(cwd: string): TeamDiscoveryResult {
|
|
|
107
107
|
};
|
|
108
108
|
}
|
|
109
109
|
|
|
110
|
-
export function allTeams(discovery: TeamDiscoveryResult): TeamConfig[] {
|
|
110
|
+
export function allTeams(discovery: TeamDiscoveryResult | undefined): TeamConfig[] {
|
|
111
|
+
if (!discovery) return [];
|
|
111
112
|
const byName = new Map<string, TeamConfig>();
|
|
112
113
|
for (const team of [...discovery.project, ...discovery.builtin, ...discovery.user]) {
|
|
113
114
|
byName.set(team.name, team);
|
package/src/ui/run-event-bus.ts
CHANGED
|
@@ -11,7 +11,8 @@ export type RunEventType =
|
|
|
11
11
|
| "run_started"
|
|
12
12
|
| "run_completed"
|
|
13
13
|
| "run_blocked"
|
|
14
|
-
| "run_cancelled"
|
|
14
|
+
| "run_cancelled"
|
|
15
|
+
| "run.cache_invalidated";
|
|
15
16
|
|
|
16
17
|
/** Typed channel names for category-based event subscription. */
|
|
17
18
|
export type EventChannel =
|
|
@@ -85,6 +85,7 @@ const SETTINGS: SettingDef[] = [
|
|
|
85
85
|
{ id: "notifierIntervalMs", label: "Notifier Interval", type: "number", tab: "advanced", description: "Async run notifier check interval in ms." },
|
|
86
86
|
{ id: "reliability.autoRetry", label: "Auto Retry", type: "boolean", tab: "advanced", description: "Automatically retry failed tasks." },
|
|
87
87
|
{ id: "reliability.autoRecover", label: "Auto Recover", type: "boolean", tab: "advanced", description: "Automatically recover from crashes." },
|
|
88
|
+
{ id: "reliability.cleanupOrphanedTempDirs", label: "Cleanup Orphaned Temp Dirs", type: "boolean", tab: "advanced", description: "Remove /tmp/pi-crew-* directories after reconciliation (1h age threshold)." },
|
|
88
89
|
{ id: "telemetry.enabled", label: "Telemetry", type: "boolean", tab: "advanced", description: "Enable telemetry collection." },
|
|
89
90
|
{ id: "notifications.enabled", label: "Notifications", type: "boolean", tab: "advanced", description: "Enable run notifications." },
|
|
90
91
|
];
|
|
@@ -124,6 +125,7 @@ const EFFECTIVE_DEFAULTS: Record<string, unknown> = {
|
|
|
124
125
|
"notifierIntervalMs": 5000,
|
|
125
126
|
"reliability.autoRetry": false,
|
|
126
127
|
"reliability.autoRecover": false,
|
|
128
|
+
"reliability.cleanupOrphanedTempDirs": true,
|
|
127
129
|
"telemetry.enabled": false,
|
|
128
130
|
"notifications.enabled": false,
|
|
129
131
|
};
|
|
@@ -123,6 +123,9 @@ function readWorkflowDir(dir: string, source: ResourceSource): WorkflowConfig[]
|
|
|
123
123
|
}
|
|
124
124
|
|
|
125
125
|
export function discoverWorkflows(cwd: string): WorkflowDiscoveryResult {
|
|
126
|
+
if (!cwd || typeof cwd !== "string") {
|
|
127
|
+
return { builtin: [], user: [], project: [] };
|
|
128
|
+
}
|
|
126
129
|
return {
|
|
127
130
|
builtin: readWorkflowDir(path.join(packageRoot(), "workflows"), "builtin"),
|
|
128
131
|
user: readWorkflowDir(path.join(userPiRoot(), "workflows"), "user"),
|
|
@@ -130,7 +133,8 @@ export function discoverWorkflows(cwd: string): WorkflowDiscoveryResult {
|
|
|
130
133
|
};
|
|
131
134
|
}
|
|
132
135
|
|
|
133
|
-
export function allWorkflows(discovery: WorkflowDiscoveryResult): WorkflowConfig[] {
|
|
136
|
+
export function allWorkflows(discovery: WorkflowDiscoveryResult | undefined): WorkflowConfig[] {
|
|
137
|
+
if (!discovery) return [];
|
|
134
138
|
const byName = new Map<string, WorkflowConfig>();
|
|
135
139
|
for (const workflow of [...discovery.project, ...discovery.builtin, ...discovery.user]) {
|
|
136
140
|
byName.set(workflow.name, workflow);
|