pi-crew 0.9.33 → 0.9.35

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.
@@ -10,6 +10,11 @@ const CREW_SCHEDULER_KEY = Symbol.for("pi-crew:scheduler");
10
10
  type SchedulerRef = {
11
11
  add(job: import("../../runtime/scheduler.ts").ScheduledJob): void;
12
12
  list(): import("../../runtime/scheduler.ts").ScheduledJob[];
13
+ remove(id: string): boolean;
14
+ update(
15
+ id: string,
16
+ patch: Partial<import("../../runtime/scheduler.ts").ScheduledJob>,
17
+ ): import("../../runtime/scheduler.ts").ScheduledJob | undefined;
13
18
  };
14
19
 
15
20
  function getCrewScheduler(): SchedulerRef | undefined {
@@ -65,7 +70,27 @@ function buildScheduleSpec(params: ScheduleParams): {
65
70
  throw new Error("schedule requires one of: cron, interval, or once.");
66
71
  }
67
72
 
73
+ function getSubAction(params: TeamToolParamsValue): string | undefined {
74
+ const raw = params.subAction ?? (params.config as { subAction?: unknown } | undefined)?.subAction;
75
+ return typeof raw === "string" ? raw.toLowerCase() : undefined;
76
+ }
77
+
78
+ function getJobIdParam(params: TeamToolParamsValue): string {
79
+ const fromTop = typeof params.jobId === "string" ? params.jobId : "";
80
+ const fromConfig = ((params.config as { jobId?: unknown } | undefined)?.jobId as string | undefined) ?? "";
81
+ return (fromTop || fromConfig).trim();
82
+ }
83
+
68
84
  export function handleSchedule(params: TeamToolParamsValue, ctx: TeamContext): PiTeamsToolResult {
85
+ // Route subactions FIRST so remove/disable/enable don't require a goal.
86
+ const subAction = getSubAction(params);
87
+ if (subAction === "remove" || subAction === "delete") {
88
+ return handleRemoveScheduled(params, ctx);
89
+ }
90
+ if (subAction === "disable" || subAction === "enable" || subAction === "update") {
91
+ return handleUpdateScheduled(params, ctx);
92
+ }
93
+
69
94
  const team = params.team ?? "default";
70
95
  const goal = params.goal ?? params.task ?? "";
71
96
  if (!goal) return result("Schedule requires goal or task.", { action: "schedule", status: "error" }, true);
@@ -239,3 +264,80 @@ export function handleListScheduled(_params: TeamToolParamsValue, ctx: TeamConte
239
264
  }
240
265
  return result(lines.join("\n"), { action: "scheduled", status: "ok" });
241
266
  }
267
+
268
+ /**
269
+ * Remove a scheduled job. Mirrors the `cancel` action for runs.
270
+ *
271
+ * Usage:
272
+ * team action='schedule' subAction='remove' jobId='<uuid>'
273
+ * team action='schedule' subAction='delete' jobId='<uuid>'
274
+ */
275
+ export function handleRemoveScheduled(params: TeamToolParamsValue, ctx: TeamContext): PiTeamsToolResult {
276
+ const jobId = getJobIdParam(params);
277
+ if (!jobId) {
278
+ return result(
279
+ "subAction=remove requires jobId. Usage: team action='schedule' subAction='remove' jobId='<uuid>'",
280
+ { action: "schedule", status: "error" },
281
+ true,
282
+ );
283
+ }
284
+ const scheduler = getCrewScheduler();
285
+ if (!scheduler) {
286
+ return result("Scheduler not running.", { action: "schedule", status: "error" }, true);
287
+ }
288
+ const removed = scheduler.remove(jobId);
289
+ persistScheduledJobRemove(ctx.cwd, jobId);
290
+ if (!removed) {
291
+ return result(`No scheduled job with id '${jobId}'.`, { action: "schedule", status: "error" }, true);
292
+ }
293
+ return result([`Scheduled job removed.`, ` Job ID: ${jobId}`].join("\n"), {
294
+ action: "schedule",
295
+ status: "ok",
296
+ data: { jobId, removed: true },
297
+ });
298
+ }
299
+
300
+ /**
301
+ * Update a scheduled job — toggle enabled flag or apply patches.
302
+ *
303
+ * Usage:
304
+ * team action='schedule' subAction='disable' jobId='<uuid>'
305
+ * team action='schedule' subAction='enable' jobId='<uuid>'
306
+ * team action='schedule' subAction='update' jobId='<uuid>' cron='0 9 * * *'
307
+ */
308
+ export function handleUpdateScheduled(params: TeamToolParamsValue, ctx: TeamContext): PiTeamsToolResult {
309
+ const jobId = getJobIdParam(params);
310
+ if (!jobId) {
311
+ return result(
312
+ "subAction=update requires jobId. Usage: team action='schedule' subAction='update|enable|disable' jobId='<uuid>'",
313
+ { action: "schedule", status: "error" },
314
+ true,
315
+ );
316
+ }
317
+ const subAction = typeof params.subAction === "string" ? params.subAction.toLowerCase() : "update";
318
+ const scheduler = getCrewScheduler();
319
+ if (!scheduler) {
320
+ return result("Scheduler not running.", { action: "schedule", status: "error" }, true);
321
+ }
322
+
323
+ const patch: Partial<import("../../runtime/scheduler.ts").ScheduledJob> = {};
324
+ if (subAction === "disable") patch.enabled = false;
325
+ if (subAction === "enable") patch.enabled = true;
326
+ // For generic "update", allow patch of cron / interval / goal.
327
+ if (subAction === "update") {
328
+ if (typeof params.cron === "string") patch.schedule = params.cron;
329
+ if (typeof params.goal === "string" || typeof params.task === "string") {
330
+ patch.description = typeof params.task === "string" ? params.task : params.goal;
331
+ }
332
+ }
333
+
334
+ const updated = scheduler.update(jobId, patch);
335
+ if (!updated) {
336
+ return result(`No scheduled job with id '${jobId}'.`, { action: "schedule", status: "error" }, true);
337
+ }
338
+ persistScheduledJobUpdate(ctx.cwd, updated);
339
+ return result(
340
+ [`Scheduled job updated.`, ` Job ID: ${jobId}`, ` Enabled: ${updated.enabled}`, ` Schedule: ${updated.schedule}`].join("\n"),
341
+ { action: "schedule", status: "ok", data: { jobId, enabled: updated.enabled, schedule: updated.schedule } },
342
+ );
343
+ }
@@ -1,6 +1,5 @@
1
1
  import { allAgents, discoverAgents } from "../../agents/discover-agents.ts";
2
2
  import { loadConfig } from "../../config/config.ts";
3
- import { PipelineRunner, type PipelineWorkflow } from "../../runtime/pipeline-runner.ts";
4
3
  import { sanitizeTaskText } from "../../runtime/task-packet.ts";
5
4
  // Heavy runtime — lazy-loaded to avoid 1.4s import cost at extension registration.
6
5
  import type { executeTeamRun as ExecuteTeamRunFn } from "../../runtime/team-runner.ts";
@@ -374,47 +373,6 @@ export async function handleRun(params: TeamToolParamsValue, ctx: TeamContext):
374
373
  }
375
374
  }
376
375
 
377
- // Check if this is a pipeline workflow - special handling for multi-stage execution
378
- const isPipelineWorkflow = workflowName === "pipeline" && !directAgent;
379
- if (isPipelineWorkflow) {
380
- // For pipeline workflows, use PipelineRunner for execution
381
- const pipelineRunner = new PipelineRunner();
382
- const pipelineWorkflow: PipelineWorkflow = {
383
- name: workflow.name,
384
- description: workflow.description,
385
- goal,
386
- stages: workflow.steps.map((step) => ({
387
- name: step.id,
388
- team: step.role,
389
- inputs: step.task,
390
- usePreviousResults: step.dependsOn && step.dependsOn.length > 0,
391
- })),
392
- stopOnError: true,
393
- defaultMaxConcurrency: workflow.maxConcurrency ?? 5,
394
- };
395
-
396
- // For now, show pipeline workflow info - full integration would require
397
- // connecting PipelineRunner to the actual team execution system
398
- const stageInfo = pipelineWorkflow.stages.map((s) => `- ${s.name} (${s.team})`).join("\n");
399
- return result(
400
- [
401
- `Pipeline workflow '${workflow.name}' is not yet wired into the team execution system.`,
402
- `Goal: ${goal}`,
403
- `Defined stages (${pipelineWorkflow.stages.length}):`,
404
- stageInfo,
405
- "",
406
- "To actually run work right now, use a supported workflow instead:",
407
- " - action='run' workflow='default' (explore → plan → execute → verify)",
408
- " - action='run' workflow='implementation' (adaptive, parallel specialists)",
409
- " - action='run' workflow='research' (explore → analyze → write)",
410
- "",
411
- "Run action='list' resource='workflow' to see all available workflows.",
412
- ].join("\n"),
413
- { action: "run", status: "ok" },
414
- false,
415
- );
416
- }
417
-
418
376
  // LAZY: dodge the jiti ESM/CJS interop TDZ race on the static `import { validateWorkflowForTeam }` above (issue #28, RFC 17).
419
377
  const { validateWorkflowForTeam: validateWorkflow } = await import("../../workflows/validate-workflow.ts");
420
378
  const validationErrors = validateWorkflow(workflow, team);
@@ -11,7 +11,7 @@ import { writeArtifact } from "../state/artifact-store.ts";
11
11
  import { appendEvent, appendEventFireAndForget } from "../state/event-log.ts";
12
12
  import { withRunLock } from "../state/locks.ts";
13
13
  import { replayPendingMailboxMessages } from "../state/mailbox.ts";
14
- import { loadRunManifestById, saveRunManifest, saveRunTasks, updateRunStatus } from "../state/state-store.ts";
14
+ import { loadRunManifestById, saveRunManifestAsync, saveRunTasks, updateRunStatus } from "../state/state-store.ts";
15
15
  import type { ArtifactDescriptor, TeamRunManifest, TeamTaskState } from "../state/types.ts";
16
16
  import { allTeams, discoverTeams } from "../teams/discover-teams.ts";
17
17
  import { assertSafePathId } from "../utils/safe-paths.ts";
@@ -59,7 +59,7 @@ async function handleRun(...args: Parameters<HandleRunFn>): Promise<Awaited<Retu
59
59
 
60
60
  import { FileCheckpointStore } from "../runtime/checkpoint.ts";
61
61
  import { waitForRun } from "../runtime/run-tracker.ts";
62
- import { normalizeSkillOverride } from "../runtime/skill-instructions.ts";
62
+ import { getSkillCacheStats, normalizeSkillOverride } from "../runtime/skill-instructions.ts";
63
63
  import { computeRunCacheKey, getCachedRun, getCacheStats } from "../state/run-cache.ts";
64
64
  import { listRunGraphs, loadRunGraph } from "../state/run-graph.ts";
65
65
  import { searchAgents, searchTeams } from "../utils/bm25-search.ts";
@@ -223,10 +223,10 @@ function artifactKey(artifact: ArtifactDescriptor): string {
223
223
  return `${artifact.kind}:${artifact.path}`;
224
224
  }
225
225
 
226
- function recoverCheckpointedTasks(
226
+ async function recoverCheckpointedTasks(
227
227
  manifest: TeamRunManifest,
228
228
  tasks: TeamTaskState[],
229
- ): { manifest: TeamRunManifest; tasks: TeamTaskState[]; recovered: string[] } {
229
+ ): Promise<{ manifest: TeamRunManifest; tasks: TeamTaskState[]; recovered: string[] }> {
230
230
  const recovered: string[] = [];
231
231
  let nextManifest = manifest;
232
232
  const nextTasks = tasks.map((task) => {
@@ -302,7 +302,7 @@ function recoverCheckpointedTasks(
302
302
  artifacts: [...artifacts.values()],
303
303
  updatedAt: new Date().toISOString(),
304
304
  };
305
- saveRunManifest(nextManifest);
305
+ await saveRunManifestAsync(nextManifest);
306
306
  saveRunTasks(nextManifest, nextTasks);
307
307
  }
308
308
  return { manifest: nextManifest, tasks: nextTasks, recovered };
@@ -346,7 +346,7 @@ export async function handleResume(params: TeamToolParamsValue, ctx: TeamContext
346
346
  const lockedManifest = fresh?.manifest ?? loaded.manifest;
347
347
  const lockedTasks = fresh?.tasks ?? loaded.tasks;
348
348
  const loadedConfig = loadConfig(ctx.cwd);
349
- const recovered = recoverCheckpointedTasks(lockedManifest, lockedTasks);
349
+ const recovered = await recoverCheckpointedTasks(lockedManifest, lockedTasks);
350
350
  const resumeManifest = recovered.manifest;
351
351
  const executedConfig = {
352
352
  ...effectiveRunConfig(loadedConfig.config, params.config),
@@ -372,7 +372,7 @@ export async function handleResume(params: TeamToolParamsValue, ctx: TeamContext
372
372
  runtimeResolution,
373
373
  updatedAt: new Date().toISOString(),
374
374
  };
375
- saveRunManifest(runtimeManifest);
375
+ await saveRunManifestAsync(runtimeManifest);
376
376
  appendEvent(runtimeManifest.eventsPath, {
377
377
  type: "runtime.resolved",
378
378
  runId: runtimeManifest.runId,
@@ -974,7 +974,12 @@ export async function handleTeamTool(params: TeamToolParamsValue, ctx: TeamConte
974
974
  });
975
975
  }
976
976
  const stats = getCacheStats(ctx.cwd);
977
- return result(`Cache stats: ${stats.entries} entries, ${stats.sizeBytes} bytes`, { action: "cache", status: "ok" });
977
+ const skillStats = getSkillCacheStats();
978
+ return result(
979
+ `Run cache: ${stats.entries} entries, ${stats.sizeBytes} bytes\n` +
980
+ `Skill cache: ${skillStats.hits} hits, ${skillStats.misses} misses (${(skillStats.hitRate * 100).toFixed(1)}% hit rate), ${skillStats.currentSize}/${skillStats.maxEntries} entries, ${skillStats.evictions} evictions`,
981
+ { action: "cache", status: "ok" },
982
+ );
978
983
  }
979
984
  case "checkpoint": {
980
985
  if (!params.runId || !params.taskId) {
@@ -38,12 +38,34 @@ export function validateEndpoint(endpoint: string): void {
38
38
  // Reject IPv6 loopback and private
39
39
  if (hostname.startsWith("[")) {
40
40
  const bare = hostname.replace(/^\[|\]$/g, "");
41
- if (bare === "::1") {
41
+ const lower = bare.toLowerCase();
42
+ if (lower === "::1") {
42
43
  throw new Error(`OTLP endpoint must not target loopback address: ${endpoint}`);
43
44
  }
44
- if (bare.toLowerCase().startsWith("fd") || bare.toLowerCase().startsWith("fc")) {
45
+ // Unique Local Addresses (fc00::/7) — fd and fc prefixes
46
+ if (lower.startsWith("fd") || lower.startsWith("fc")) {
45
47
  throw new Error(`OTLP endpoint must not target private IPv6 address: ${endpoint}`);
46
48
  }
49
+ // Link-Local (fe80::/10) — fe8x, fe9x, feax, febx prefixes
50
+ if (lower.startsWith("fe8") || lower.startsWith("fe9") || lower.startsWith("fea") || lower.startsWith("feb")) {
51
+ throw new Error(`OTLP endpoint must not target IPv6 link-local address: ${endpoint}`);
52
+ }
53
+ // Site-Local (fec0::/10) — deprecated but still routable on misconfigured networks
54
+ if (lower.startsWith("fec") || lower.startsWith("fed") || lower.startsWith("fee") || lower.startsWith("fef")) {
55
+ throw new Error(`OTLP endpoint must not target IPv6 site-local address: ${endpoint}`);
56
+ }
57
+ // Multicast (ff00::/8) — prefix ff
58
+ if (lower.startsWith("ff")) {
59
+ throw new Error(`OTLP endpoint must not target IPv6 multicast address: ${endpoint}`);
60
+ }
61
+ // Unspecified address ::
62
+ if (lower === "::") {
63
+ throw new Error(`OTLP endpoint must not target IPv6 unspecified address: ${endpoint}`);
64
+ }
65
+ // IPv4-mapped IPv6 (::ffff:x.x.x.x)
66
+ if (lower.startsWith("::ffff:")) {
67
+ throw new Error(`OTLP endpoint must not target IPv4-mapped IPv6 address: ${endpoint}`);
68
+ }
47
69
  }
48
70
 
49
71
  // Reject IPv4 private/reserved ranges
@@ -17,7 +17,7 @@
17
17
  import * as fs from "node:fs";
18
18
  import { writeArtifact } from "../state/artifact-store.ts";
19
19
  import { appendEvent } from "../state/event-log.ts";
20
- import { saveRunManifest } from "../state/state-store.ts";
20
+ import { saveRunManifestAsync } from "../state/state-store.ts";
21
21
  import type { TeamRunManifest, TeamTaskState } from "../state/types.ts";
22
22
  import type { TeamConfig } from "../teams/team-config.ts";
23
23
  import type { WorkflowConfig, WorkflowStep } from "../workflows/workflow-config.ts";
@@ -369,7 +369,7 @@ export interface InjectAdaptivePlanResult {
369
369
  missingPlan: boolean;
370
370
  }
371
371
 
372
- export function injectAdaptivePlanIfReady(input: InjectAdaptivePlanInput): InjectAdaptivePlanResult {
372
+ export async function injectAdaptivePlanIfReady(input: InjectAdaptivePlanInput): Promise<InjectAdaptivePlanResult> {
373
373
  if (input.workflow.name !== "implementation")
374
374
  return {
375
375
  tasks: input.tasks,
@@ -442,7 +442,7 @@ export function injectAdaptivePlanIfReady(input: InjectAdaptivePlanInput): Injec
442
442
  producer: assessTask.id,
443
443
  content: `${JSON.stringify({ reason: repair.reason, phases: repair.plan.phases.map((phase) => ({ name: phase.name, count: phase.tasks.length, roles: phase.tasks.map((task) => task.role) })) }, null, 2)}\n`,
444
444
  });
445
- saveRunManifest({
445
+ await saveRunManifestAsync({
446
446
  ...input.manifest,
447
447
  updatedAt: new Date().toISOString(),
448
448
  artifacts: [...input.manifest.artifacts, repairArtifact],
@@ -4,7 +4,7 @@ import { allAgents, discoverAgents } from "../agents/discover-agents.ts";
4
4
  import { loadConfig } from "../config/config.ts";
5
5
  import { appendEvent } from "../state/event-log.ts";
6
6
  import { withRunLockSync } from "../state/locks.ts";
7
- import { createRunPaths, loadRunManifestById, saveRunManifest, updateRunStatus } from "../state/state-store.ts";
7
+ import { createRunPaths, loadRunManifestById, saveRunManifestAsync, updateRunStatus } from "../state/state-store.ts";
8
8
  import type { TeamRunManifest, TeamTaskState } from "../state/types.ts";
9
9
  import { allTeams, discoverTeams } from "../teams/discover-teams.ts";
10
10
  import { projectCrewRoot } from "../utils/paths.ts";
@@ -621,7 +621,7 @@ async function main(): Promise<void> {
621
621
  status: runStatus,
622
622
  updatedAt: new Date().toISOString(),
623
623
  };
624
- saveRunManifest(finalGoalManifest);
624
+ await saveRunManifestAsync(finalGoalManifest);
625
625
  earlyResult = {
626
626
  manifest: finalGoalManifest,
627
627
  tasks: goalResult.tasks,
@@ -642,7 +642,7 @@ async function main(): Promise<void> {
642
642
  signal: abortController.signal,
643
643
  tokenBudget: wf.maxTokenBudget,
644
644
  });
645
- saveRunManifest(dwfResult.manifest);
645
+ await saveRunManifestAsync(dwfResult.manifest);
646
646
  earlyResult = dwfResult;
647
647
  }
648
648
  console.log(`[background-runner] ${manifest.runKind} returned, status=${earlyResult.manifest.status}`);
@@ -687,7 +687,7 @@ async function main(): Promise<void> {
687
687
  runConfig,
688
688
  updatedAt: new Date().toISOString(),
689
689
  };
690
- saveRunManifest(manifest);
690
+ await saveRunManifestAsync(manifest);
691
691
  appendEvent(manifest.eventsPath, {
692
692
  type: "runtime.resolved",
693
693
  runId: manifest.runId,