pi-crew 0.6.0 → 0.6.3
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 +225 -0
- package/README.md +70 -31
- package/docs/issue-29-analysis.md +189 -0
- package/docs/superpowers/plans/2026-06-09-fallow-patterns-adoption.md +1268 -0
- package/package.json +2 -2
- package/src/agents/agent-config.ts +2 -1
- package/src/benchmark/feedback-loop.ts +4 -2
- package/src/config/config.ts +106 -15
- package/src/errors.ts +107 -0
- package/src/extension/async-notifier.ts +6 -2
- package/src/extension/crew-cleanup.ts +8 -5
- package/src/extension/cross-extension-rpc.ts +48 -0
- package/src/extension/management.ts +464 -109
- package/src/extension/register.ts +194 -34
- package/src/extension/registration/commands.ts +4 -3
- package/src/extension/registration/subagent-helpers.ts +2 -2
- package/src/extension/registration/subagent-tools.ts +3 -1
- package/src/extension/registration/team-tool.ts +3 -1
- package/src/extension/registration/viewers.ts +3 -2
- package/src/extension/run-export.ts +16 -1
- package/src/extension/run-import.ts +16 -0
- package/src/extension/team-tool/anchor.ts +5 -1
- package/src/extension/team-tool/api.ts +12 -7
- package/src/extension/team-tool/cancel.ts +3 -3
- package/src/extension/team-tool/config-patch.ts +15 -1
- package/src/extension/team-tool/explain.ts +1 -1
- package/src/extension/team-tool/handle-schedule.ts +40 -0
- package/src/extension/team-tool/inspect.ts +3 -3
- package/src/extension/team-tool/lifecycle-actions.ts +4 -4
- package/src/extension/team-tool/respond.ts +2 -2
- package/src/extension/team-tool/run.ts +60 -11
- package/src/extension/team-tool/status.ts +1 -1
- package/src/extension/team-tool.ts +175 -47
- package/src/hooks/registry.ts +84 -12
- package/src/hooks/types.ts +12 -3
- package/src/i18n.ts +15 -2
- package/src/observability/exporters/otlp-exporter.ts +73 -0
- package/src/plugins/plugin-define.ts +6 -0
- package/src/plugins/plugin-registry.ts +32 -0
- package/src/plugins/plugins/index.ts +3 -0
- package/src/plugins/plugins/nextjs.ts +19 -0
- package/src/plugins/plugins/vite.ts +14 -0
- package/src/plugins/plugins/vitest.ts +14 -0
- package/src/runtime/adaptive-plan.ts +24 -0
- package/src/runtime/agent-control.ts +6 -3
- package/src/runtime/async-runner.ts +86 -3
- package/src/runtime/background-runner.ts +529 -144
- package/src/runtime/chain-parser.ts +5 -1
- package/src/runtime/chain-runner.ts +67 -3
- package/src/runtime/checkpoint.ts +262 -180
- package/src/runtime/child-pi.ts +165 -38
- package/src/runtime/crash-recovery.ts +25 -14
- package/src/runtime/crew-agent-records.ts +6 -3
- package/src/runtime/cross-extension-rpc.ts +34 -8
- package/src/runtime/diagnostic-export.ts +4 -5
- package/src/runtime/dynamic-script-runner.ts +9 -6
- package/src/runtime/foreground-watchdog.ts +3 -3
- package/src/runtime/heartbeat-watcher.ts +19 -2
- package/src/runtime/intercom-bridge.ts +7 -0
- package/src/runtime/iteration-hooks.ts +1 -1
- package/src/runtime/live-agent-manager.ts +6 -3
- package/src/runtime/live-irc.ts +4 -2
- package/src/runtime/manifest-cache.ts +4 -0
- package/src/runtime/orphan-worker-registry.ts +444 -0
- package/src/runtime/parallel-utils.ts +2 -1
- package/src/runtime/parent-guard.ts +70 -16
- package/src/runtime/pi-args.ts +437 -14
- package/src/runtime/pi-spawn.ts +1 -0
- package/src/runtime/post-checks.ts +11 -4
- package/src/runtime/retry-runner.ts +9 -3
- package/src/runtime/{drift-detectors.ts → run-drift.ts} +1 -1
- package/src/runtime/run-tracker.ts +38 -10
- package/src/runtime/sandbox.ts +42 -21
- package/src/runtime/scheduler.ts +25 -2
- package/src/runtime/semaphore.ts +2 -1
- package/src/runtime/settings-store.ts +14 -2
- package/src/runtime/skill-effectiveness.ts +109 -64
- package/src/runtime/skill-instructions.ts +114 -33
- package/src/runtime/stale-reconciler.ts +310 -69
- package/src/runtime/subagent-manager.ts +265 -53
- package/src/runtime/subprocess-tool-registry.ts +2 -2
- package/src/runtime/task-health.ts +76 -0
- package/src/runtime/task-id.ts +20 -0
- package/src/runtime/task-packet.ts +13 -1
- package/src/runtime/task-runner/live-executor.ts +1 -1
- package/src/runtime/task-runner/progress.ts +1 -1
- package/src/runtime/task-runner/state-helpers.ts +110 -6
- package/src/runtime/task-runner.ts +98 -67
- package/src/runtime/team-runner.ts +186 -20
- package/src/runtime/usage-tracker.ts +4 -2
- package/src/runtime/verification-gates.ts +36 -9
- package/src/schema/team-tool-schema.ts +27 -9
- package/src/skills/discover-skills.ts +9 -3
- package/src/state/active-run-registry.ts +170 -38
- package/src/state/artifact-store.ts +25 -13
- package/src/state/atomic-write-v2.ts +86 -0
- package/src/state/atomic-write.ts +346 -55
- package/src/state/blob-store.ts +178 -10
- package/src/state/contracts.ts +2 -1
- package/src/state/crew-init.ts +161 -28
- package/src/state/decision-ledger.ts +172 -111
- package/src/state/event-log-rotation.ts +82 -52
- package/src/state/event-log.ts +270 -75
- package/src/state/health-store.ts +71 -0
- package/src/state/hook-instinct-bridge.ts +2 -1
- package/src/state/locks.ts +109 -20
- package/src/state/mailbox.ts +45 -7
- package/src/state/observation-store.ts +4 -1
- package/src/state/run-graph.ts +141 -130
- package/src/state/run-metrics.ts +24 -8
- package/src/state/state-store.ts +333 -44
- package/src/state/task-claims.ts +9 -2
- package/src/tools/safe-bash.ts +69 -20
- package/src/types/new-api-types.ts +10 -5
- package/src/ui/keybinding-map.ts +2 -1
- package/src/ui/live-run-sidebar.ts +1 -1
- package/src/ui/loaders.ts +4 -0
- package/src/ui/overlays/agent-picker-overlay.ts +1 -1
- package/src/ui/overlays/mailbox-detail-overlay.ts +1 -1
- package/src/ui/run-action-dispatcher.ts +4 -3
- package/src/ui/run-snapshot-cache.ts +1 -1
- package/src/ui/status-colors.ts +2 -1
- package/src/ui/syntax-highlight.ts +2 -1
- package/src/ui/tool-render.ts +13 -3
- package/src/utils/env-filter.ts +66 -0
- package/src/utils/file-coalescer.ts +9 -1
- package/src/utils/fs-watch.ts +4 -2
- package/src/utils/gh-protocol.ts +2 -1
- package/src/utils/paths.ts +80 -5
- package/src/utils/redaction.ts +6 -1
- package/src/utils/safe-paths.ts +287 -10
- package/src/worktree/cleanup.ts +117 -26
- package/src/worktree/worktree-manager.ts +128 -15
- package/test-bugs-all.mjs +10 -6
- package/test-integration-check.ts +4 -0
|
@@ -72,10 +72,16 @@ export class RetryRunner {
|
|
|
72
72
|
private _disposed = false;
|
|
73
73
|
private _handoffs: HandoffSummary[] = [];
|
|
74
74
|
|
|
75
|
+
private taskRunner: TaskRunnerLike;
|
|
76
|
+
private handoffManager: HandoffManager;
|
|
77
|
+
|
|
75
78
|
constructor(
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
) {
|
|
79
|
+
taskRunner: TaskRunnerLike,
|
|
80
|
+
handoffManager: HandoffManager,
|
|
81
|
+
) {
|
|
82
|
+
this.taskRunner = taskRunner;
|
|
83
|
+
this.handoffManager = handoffManager;
|
|
84
|
+
}
|
|
79
85
|
|
|
80
86
|
/**
|
|
81
87
|
* Check if this runner has been disposed.
|
|
@@ -208,7 +208,7 @@ export function runDriftDetection(ctx: DriftContext, maxPasses = 2): DriftReport
|
|
|
208
208
|
newFindings++;
|
|
209
209
|
}
|
|
210
210
|
} catch (error) {
|
|
211
|
-
logInternalError("drift
|
|
211
|
+
logInternalError("run-drift", error, `detector=${detector.name} runId=${ctx.manifest?.runId}`);
|
|
212
212
|
}
|
|
213
213
|
}
|
|
214
214
|
|
|
@@ -1,21 +1,31 @@
|
|
|
1
|
-
import type { TeamRunManifest, TeamTaskState } from "../state/types.ts";
|
|
2
1
|
import * as fs from "node:fs";
|
|
3
2
|
import * as path from "node:path";
|
|
4
3
|
import { loadRunManifestById } from "../state/state-store.ts";
|
|
4
|
+
import type { TeamRunManifest, TeamTaskState } from "../state/types.ts";
|
|
5
|
+
import { projectCrewRoot } from "../utils/paths.ts";
|
|
5
6
|
import { isFinishedRunStatus } from "./process-status.ts";
|
|
6
7
|
|
|
7
8
|
export interface ActiveRunPromise {
|
|
8
9
|
promise: Promise<{ manifest: TeamRunManifest; tasks: TeamTaskState[] }>;
|
|
9
|
-
resolve: (value: {
|
|
10
|
+
resolve: (value: {
|
|
11
|
+
manifest: TeamRunManifest;
|
|
12
|
+
tasks: TeamTaskState[];
|
|
13
|
+
}) => void;
|
|
10
14
|
reject: (reason: unknown) => void;
|
|
11
15
|
}
|
|
12
16
|
|
|
13
17
|
const activeRunPromises = new Map<string, ActiveRunPromise>();
|
|
14
18
|
|
|
15
19
|
export function registerRunPromise(runId: string): ActiveRunPromise {
|
|
16
|
-
let resolve!: (value: {
|
|
20
|
+
let resolve!: (value: {
|
|
21
|
+
manifest: TeamRunManifest;
|
|
22
|
+
tasks: TeamTaskState[];
|
|
23
|
+
}) => void;
|
|
17
24
|
let reject!: (reason: unknown) => void;
|
|
18
|
-
const promise = new Promise<{
|
|
25
|
+
const promise = new Promise<{
|
|
26
|
+
manifest: TeamRunManifest;
|
|
27
|
+
tasks: TeamTaskState[];
|
|
28
|
+
}>((res, rej) => {
|
|
19
29
|
resolve = res;
|
|
20
30
|
reject = rej;
|
|
21
31
|
});
|
|
@@ -24,7 +34,10 @@ export function registerRunPromise(runId: string): ActiveRunPromise {
|
|
|
24
34
|
return entry;
|
|
25
35
|
}
|
|
26
36
|
|
|
27
|
-
export function resolveRunPromise(
|
|
37
|
+
export function resolveRunPromise(
|
|
38
|
+
runId: string,
|
|
39
|
+
result: { manifest: TeamRunManifest; tasks: TeamTaskState[] },
|
|
40
|
+
): void {
|
|
28
41
|
const entry = activeRunPromises.get(runId);
|
|
29
42
|
if (entry) {
|
|
30
43
|
entry.resolve(result);
|
|
@@ -55,7 +68,7 @@ export async function waitForRun(
|
|
|
55
68
|
const deadline = Date.now() + timeoutMs;
|
|
56
69
|
|
|
57
70
|
// Fast path: already terminal on disk
|
|
58
|
-
const loaded = loadRunManifestById(cwd, runId);
|
|
71
|
+
const loaded = loadRunManifestById(cwd, runId); // NOTE: no withRunLock - best-effort only; concurrent writes may cause inconsistency;
|
|
59
72
|
if (loaded && isFinishedRunStatus(loaded.manifest.status)) {
|
|
60
73
|
return loaded;
|
|
61
74
|
}
|
|
@@ -65,7 +78,13 @@ export async function waitForRun(
|
|
|
65
78
|
if (entry) {
|
|
66
79
|
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
67
80
|
const timeoutPromise = new Promise<never>((_, reject) => {
|
|
68
|
-
timer = setTimeout(
|
|
81
|
+
timer = setTimeout(
|
|
82
|
+
() =>
|
|
83
|
+
reject(
|
|
84
|
+
new Error(`waitForRun timed out after ${timeoutMs}ms`),
|
|
85
|
+
),
|
|
86
|
+
timeoutMs,
|
|
87
|
+
);
|
|
69
88
|
});
|
|
70
89
|
try {
|
|
71
90
|
return await Promise.race([entry.promise, timeoutPromise]);
|
|
@@ -78,15 +97,24 @@ export async function waitForRun(
|
|
|
78
97
|
let attempt = 0;
|
|
79
98
|
while (Date.now() < deadline) {
|
|
80
99
|
if (attempt === 0) {
|
|
81
|
-
// Early exit: if the run directory doesn't exist, don't waste time polling
|
|
82
|
-
|
|
100
|
+
// Early exit: if the run directory doesn't exist, don't waste time polling.
|
|
101
|
+
// Use projectCrewRoot() to honour the .pi/teams/ fallback for .pi-based
|
|
102
|
+
// projects (see issue #29). Without this, the hardcoded `.crew/state/runs/`
|
|
103
|
+
// path never resolves in projects that use the `.pi/` layout, the throw
|
|
104
|
+
// escapes via subagent-manager.ts:281, and pi crashes with uncaughtException.
|
|
105
|
+
const runDir = path.join(
|
|
106
|
+
projectCrewRoot(cwd),
|
|
107
|
+
"state",
|
|
108
|
+
"runs",
|
|
109
|
+
runId,
|
|
110
|
+
);
|
|
83
111
|
if (!fs.existsSync(runDir)) {
|
|
84
112
|
throw new Error(
|
|
85
113
|
`Run ${runId} not found. No run directory at ${runDir}`,
|
|
86
114
|
);
|
|
87
115
|
}
|
|
88
116
|
}
|
|
89
|
-
const fresh = loadRunManifestById(cwd, runId);
|
|
117
|
+
const fresh = loadRunManifestById(cwd, runId); // NOTE: no withRunLock - best-effort only; concurrent writes may cause inconsistency;
|
|
90
118
|
if (fresh && isFinishedRunStatus(fresh.manifest.status)) {
|
|
91
119
|
return fresh;
|
|
92
120
|
}
|
package/src/runtime/sandbox.ts
CHANGED
|
@@ -21,28 +21,38 @@ const FORBIDDEN_PATTERNS = [
|
|
|
21
21
|
// Global escape vectors
|
|
22
22
|
/\bglobalThis\b/, // globalThis reference
|
|
23
23
|
/\bglobal\b/, // global reference (Node.js)
|
|
24
|
+
// Block constructor chain escape vectors:
|
|
25
|
+
// - `.constructor` followed by `(`, `.`, `;`, or end-of-line/string
|
|
26
|
+
// - `[constructor]` or `["constructor"]` bracket access on any value
|
|
27
|
+
// The bare `constructor` keyword in a class body is safe and allowed.
|
|
28
|
+
/\.constructor\s*\(/, // Block obj.constructor() chain calls
|
|
29
|
+
/\.constructor\s*(?:\.|$|;|,|\s)/, // Block obj.constructor at end, semicolon, comma, or whitespace
|
|
30
|
+
/\[\s*['"]?constructor['"]?\s*\]/, // Block ["constructor"] or ['constructor'] or [constructor]
|
|
24
31
|
] as const;
|
|
25
32
|
|
|
33
|
+
Object.freeze(FORBIDDEN_PATTERNS);
|
|
34
|
+
|
|
26
35
|
/**
|
|
27
|
-
*
|
|
28
|
-
*
|
|
36
|
+
* SECURITY (HIGH #3 fix): Normalize source code before forbidden-pattern checks
|
|
37
|
+
* to prevent unicode-escape bypasses.
|
|
38
|
+
*
|
|
39
|
+
* Attackers can write `import\u0028"fs"\u0029` which compiles as
|
|
40
|
+
* `import("fs")` but does not match the regex `/import\s*\(/`.
|
|
41
|
+
*
|
|
42
|
+
* This function:
|
|
43
|
+
* 1. Strips null bytes (used to split keywords across boundaries)
|
|
44
|
+
* 2. Decodes \uXXXX escape sequences so regexes see the actual characters
|
|
29
45
|
*/
|
|
30
|
-
|
|
31
|
-
//
|
|
32
|
-
|
|
33
|
-
//
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
"console",
|
|
41
|
-
// Process (limited)
|
|
42
|
-
"process",
|
|
43
|
-
]);
|
|
44
|
-
|
|
45
|
-
Object.freeze(FORBIDDEN_PATTERNS);
|
|
46
|
+
export function normalizeCodeForValidation(code: string): string {
|
|
47
|
+
// Strip null bytes
|
|
48
|
+
let normalized = code.replace(/\0/g, "");
|
|
49
|
+
// Decode common unicode escapes: \u0028 → (
|
|
50
|
+
normalized = normalized.replace(
|
|
51
|
+
/\\u([0-9a-fA-F]{4})/g,
|
|
52
|
+
(_, hex) => String.fromCharCode(Number.parseInt(hex, 16)),
|
|
53
|
+
);
|
|
54
|
+
return normalized;
|
|
55
|
+
}
|
|
46
56
|
|
|
47
57
|
export interface SandboxOptions {
|
|
48
58
|
timeout?: number;
|
|
@@ -76,7 +86,16 @@ export class WorkflowSandbox {
|
|
|
76
86
|
const safeEnv = Object.freeze(sanitizeEnvSecrets(process.env, {
|
|
77
87
|
allowList: [
|
|
78
88
|
"NODE_ENV",
|
|
79
|
-
|
|
89
|
+
// Note: PI_CREW_* globs are not used here because isDangerousGlob
|
|
90
|
+
// flags them as potentially matching secret env vars (PI_CREW_token,
|
|
91
|
+
// PI_CREW_api_key, etc.). Instead, list the specific PI_CREW env vars
|
|
92
|
+
// that sandboxed code legitimately needs.
|
|
93
|
+
"PI_CREW_DEPTH",
|
|
94
|
+
"PI_CREW_INHERIT_PROJECT_CONTEXT",
|
|
95
|
+
"PI_CREW_INHERIT_SKILLS",
|
|
96
|
+
"PI_CREW_MOCK_LIVE_SESSION",
|
|
97
|
+
"PI_CREW_SKIP_HOME_CHECK",
|
|
98
|
+
"PI_CREW_WARM_POOL_SIZE",
|
|
80
99
|
"PATH",
|
|
81
100
|
"PATH_SEPARATOR",
|
|
82
101
|
"USERPROFILE",
|
|
@@ -204,15 +223,17 @@ export class WorkflowSandbox {
|
|
|
204
223
|
* ensure compilation is safe.
|
|
205
224
|
*/
|
|
206
225
|
private validateScript(code: string): void {
|
|
226
|
+
// SECURITY (HIGH #3 fix): Normalize unicode escapes before pattern matching
|
|
227
|
+
const normalized = normalizeCodeForValidation(code);
|
|
207
228
|
// Check for ESM/module patterns
|
|
208
229
|
for (const pattern of FORBIDDEN_PATTERNS) {
|
|
209
|
-
if (pattern.test(
|
|
230
|
+
if (pattern.test(normalized)) {
|
|
210
231
|
throw new Error(`Forbidden pattern detected: ${pattern.source}`);
|
|
211
232
|
}
|
|
212
233
|
}
|
|
213
234
|
|
|
214
235
|
// Check for import.meta specifically (C4)
|
|
215
|
-
if (/import\.meta/.test(
|
|
236
|
+
if (/import\.meta/.test(normalized)) {
|
|
216
237
|
throw new Error("import.meta is not allowed in sandboxed code");
|
|
217
238
|
}
|
|
218
239
|
|
package/src/runtime/scheduler.ts
CHANGED
|
@@ -15,11 +15,13 @@ export interface ScheduledJob {
|
|
|
15
15
|
lastStatus?: "success" | "error" | "running";
|
|
16
16
|
nextRun?: string;
|
|
17
17
|
runCount: number;
|
|
18
|
+
/** Run IDs spawned by this job. Used to cancel runs when job is removed. */
|
|
19
|
+
spawnedRunIds?: string[];
|
|
18
20
|
}
|
|
19
21
|
|
|
20
22
|
export type ScheduleChangeEvent =
|
|
21
23
|
| { type: "added"; job: ScheduledJob }
|
|
22
|
-
| { type: "removed"; jobId: string }
|
|
24
|
+
| { type: "removed"; jobId: string; spawnedRunIds?: string[] }
|
|
23
25
|
| { type: "updated"; job: ScheduledJob }
|
|
24
26
|
| { type: "fired"; jobId: string; agentId: string; name: string }
|
|
25
27
|
| { type: "error"; jobId: string; error: string };
|
|
@@ -30,17 +32,21 @@ export class CrewScheduler {
|
|
|
30
32
|
private emit?: (event: ScheduleChangeEvent) => void;
|
|
31
33
|
private executor?: (job: ScheduledJob) => string;
|
|
32
34
|
private finalizer?: (jobId: string, agentId: string) => void;
|
|
35
|
+
private runCancelFn?: (runId: string) => void;
|
|
33
36
|
|
|
34
37
|
start(
|
|
35
38
|
options: {
|
|
36
39
|
emit: (event: ScheduleChangeEvent) => void;
|
|
37
40
|
executor: (job: ScheduledJob) => string;
|
|
38
41
|
finalizer: (jobId: string, agentId: string) => void;
|
|
42
|
+
/** Optional callback to cancel a spawned run by runId. */
|
|
43
|
+
runCancelFn?: (runId: string) => void;
|
|
39
44
|
},
|
|
40
45
|
): void {
|
|
41
46
|
this.emit = options.emit;
|
|
42
47
|
this.executor = options.executor;
|
|
43
48
|
this.finalizer = options.finalizer;
|
|
49
|
+
this.runCancelFn = options.runCancelFn;
|
|
44
50
|
}
|
|
45
51
|
|
|
46
52
|
stop(): void {
|
|
@@ -52,6 +58,7 @@ export class CrewScheduler {
|
|
|
52
58
|
this.emit = undefined;
|
|
53
59
|
this.executor = undefined;
|
|
54
60
|
this.finalizer = undefined;
|
|
61
|
+
this.runCancelFn = undefined;
|
|
55
62
|
}
|
|
56
63
|
|
|
57
64
|
add(job: ScheduledJob): void {
|
|
@@ -61,9 +68,17 @@ export class CrewScheduler {
|
|
|
61
68
|
}
|
|
62
69
|
|
|
63
70
|
remove(id: string): boolean {
|
|
71
|
+
const job = this.jobs.get(id);
|
|
72
|
+
const spawnedRunIds = job?.spawnedRunIds;
|
|
64
73
|
this.disarm(id);
|
|
74
|
+
// Cancel all spawned runs that are still active
|
|
75
|
+
if (spawnedRunIds && this.runCancelFn) {
|
|
76
|
+
for (const runId of spawnedRunIds) {
|
|
77
|
+
try { this.runCancelFn(runId); } catch { /* best-effort */ }
|
|
78
|
+
}
|
|
79
|
+
}
|
|
65
80
|
const ok = this.jobs.delete(id);
|
|
66
|
-
if (ok) this.emit?.({ type: "removed", jobId: id });
|
|
81
|
+
if (ok) this.emit?.({ type: "removed", jobId: id, spawnedRunIds });
|
|
67
82
|
return ok;
|
|
68
83
|
}
|
|
69
84
|
|
|
@@ -82,6 +97,14 @@ export class CrewScheduler {
|
|
|
82
97
|
return [...this.jobs.values()];
|
|
83
98
|
}
|
|
84
99
|
|
|
100
|
+
/** Record a runId spawned by a job. Call this after executor fires. */
|
|
101
|
+
recordSpawnedRun(jobId: string, runId: string): void {
|
|
102
|
+
const job = this.jobs.get(jobId);
|
|
103
|
+
if (!job) return;
|
|
104
|
+
const spawnedRunIds = [...(job.spawnedRunIds ?? []), runId];
|
|
105
|
+
this.jobs.set(jobId, { ...job, spawnedRunIds });
|
|
106
|
+
}
|
|
107
|
+
|
|
85
108
|
private arm(job: ScheduledJob): void {
|
|
86
109
|
if (this.timers.has(job.id)) return;
|
|
87
110
|
if (job.scheduleType === "interval" && job.intervalMs) {
|
package/src/runtime/semaphore.ts
CHANGED
|
@@ -88,7 +88,8 @@ export interface ParallelResult<R> {
|
|
|
88
88
|
*
|
|
89
89
|
* Adapted from oh-my-pi's `mapWithConcurrencyLimit`.
|
|
90
90
|
*/
|
|
91
|
-
|
|
91
|
+
/** @internal */
|
|
92
|
+
async function mapWithFailFast<T, R>(
|
|
92
93
|
items: T[],
|
|
93
94
|
concurrency: number,
|
|
94
95
|
fn: (item: T, index: number, signal: AbortSignal) => Promise<R>,
|
|
@@ -20,6 +20,18 @@ const MAX_TURNS_CEILING = 10_000;
|
|
|
20
20
|
const GRACE_TURNS_CEILING = 1_000;
|
|
21
21
|
const VALID_JOIN_MODES = new Set<JoinMode>(["async", "group", "smart"]);
|
|
22
22
|
|
|
23
|
+
/**
|
|
24
|
+
* M2: Validate that a scheduled job object has required fields before passing to scheduler.
|
|
25
|
+
* Prevents opaque unknown[] from reaching CrewScheduler.add() without validation.
|
|
26
|
+
*/
|
|
27
|
+
function validateScheduledJob(job: unknown): boolean {
|
|
28
|
+
if (!job || typeof job !== "object") return false;
|
|
29
|
+
const obj = job as Record<string, unknown>;
|
|
30
|
+
return typeof obj.id === "string" && obj.id.length > 0
|
|
31
|
+
&& typeof obj.scheduleType === "string"
|
|
32
|
+
&& typeof obj.enabled === "boolean";
|
|
33
|
+
}
|
|
34
|
+
|
|
23
35
|
function sanitizeSettings(raw: unknown): CrewSettings {
|
|
24
36
|
if (!raw || typeof raw !== "object") return {};
|
|
25
37
|
const r = raw as Record<string, unknown>;
|
|
@@ -57,9 +69,9 @@ function sanitizeSettings(raw: unknown): CrewSettings {
|
|
|
57
69
|
if (typeof r.notifierIntervalMs === "number" && r.notifierIntervalMs >= 1000) {
|
|
58
70
|
out.notifierIntervalMs = r.notifierIntervalMs;
|
|
59
71
|
}
|
|
60
|
-
// Pass through scheduledJobs
|
|
72
|
+
// Pass through scheduledJobs after basic validation
|
|
61
73
|
if (Array.isArray(r.scheduledJobs)) {
|
|
62
|
-
out.scheduledJobs = r.scheduledJobs;
|
|
74
|
+
out.scheduledJobs = (r.scheduledJobs as unknown[]).filter(validateScheduledJob);
|
|
63
75
|
}
|
|
64
76
|
return out;
|
|
65
77
|
}
|