pi-crew 0.1.51 → 0.2.1
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 +56 -1
- package/README.md +176 -781
- package/agents/analyst.md +11 -11
- package/agents/critic.md +11 -11
- package/agents/executor.md +11 -11
- package/agents/explorer.md +11 -11
- package/agents/planner.md +11 -11
- package/agents/reviewer.md +11 -11
- package/agents/security-reviewer.md +11 -11
- package/agents/test-engineer.md +11 -11
- package/agents/verifier.md +70 -11
- package/agents/writer.md +11 -11
- package/docs/actions-reference.md +595 -0
- package/docs/commands-reference.md +347 -0
- package/docs/runtime-flow.md +148 -148
- package/index.ts +6 -6
- package/package.json +99 -99
- package/skills/async-worker-recovery/SKILL.md +42 -42
- package/skills/context-artifact-hygiene/SKILL.md +52 -52
- package/skills/delegation-patterns/SKILL.md +54 -54
- package/skills/mailbox-interactive/SKILL.md +40 -40
- package/skills/model-routing-context/SKILL.md +39 -39
- package/skills/multi-perspective-review/SKILL.md +58 -58
- package/skills/observability-reliability/SKILL.md +41 -41
- package/skills/orchestration/SKILL.md +157 -157
- package/skills/ownership-session-security/SKILL.md +41 -41
- package/skills/pi-extension-lifecycle/SKILL.md +39 -39
- package/skills/requirements-to-task-packet/SKILL.md +63 -63
- package/skills/resource-discovery-config/SKILL.md +41 -41
- package/skills/runtime-state-reader/SKILL.md +44 -44
- package/skills/secure-agent-orchestration-review/SKILL.md +45 -45
- package/skills/state-mutation-locking/SKILL.md +42 -42
- package/skills/systematic-debugging/SKILL.md +67 -67
- package/skills/ui-render-performance/SKILL.md +39 -39
- package/skills/verification-before-done/SKILL.md +57 -57
- package/skills/worktree-isolation/SKILL.md +39 -39
- package/src/adapters/claude-adapter.ts +25 -0
- package/src/adapters/codex-adapter.ts +21 -0
- package/src/adapters/cursor-adapter.ts +17 -0
- package/src/adapters/export-util.ts +137 -0
- package/src/adapters/index.ts +15 -0
- package/src/adapters/registry.ts +18 -0
- package/src/adapters/types.ts +23 -0
- package/src/agents/agent-config.ts +2 -0
- package/src/agents/agent-search.ts +98 -98
- package/src/agents/discover-agents.ts +2 -1
- package/src/config/config.ts +13 -1
- package/src/config/drift-detector.ts +211 -0
- package/src/config/markers.ts +327 -0
- package/src/config/resilient-parser.ts +108 -0
- package/src/config/suggestions.ts +74 -0
- package/src/extension/cross-extension-rpc.ts +103 -94
- package/src/extension/project-init.ts +21 -1
- package/src/extension/register.ts +45 -14
- package/src/extension/registration/commands.ts +77 -8
- package/src/extension/registration/subagent-tools.ts +10 -1
- package/src/extension/registration/team-tool.ts +10 -1
- package/src/extension/registration/viewers.ts +48 -34
- package/src/extension/run-bundle-schema.ts +89 -89
- package/src/extension/run-import.ts +25 -1
- package/src/extension/run-index.ts +5 -1
- package/src/extension/run-maintenance.ts +142 -68
- package/src/extension/team-manager-command.ts +10 -1
- package/src/extension/team-tool/api.ts +441 -441
- package/src/extension/team-tool/doctor.ts +28 -3
- package/src/extension/team-tool/handle-settings.ts +195 -188
- package/src/extension/team-tool/inspect.ts +41 -41
- package/src/extension/team-tool/intent-policy.ts +42 -42
- package/src/extension/team-tool/lifecycle-actions.ts +27 -8
- package/src/extension/team-tool/plan.ts +19 -19
- package/src/extension/team-tool/run.ts +12 -1
- package/src/extension/team-tool.ts +332 -322
- package/src/i18n.ts +184 -184
- package/src/observability/exporters/otlp-exporter.ts +92 -77
- package/src/prompt/prompt-runtime.ts +72 -72
- package/src/runtime/agent-memory.ts +72 -72
- package/src/runtime/agent-observability.ts +114 -114
- package/src/runtime/async-marker.ts +26 -26
- package/src/runtime/attention-events.ts +28 -28
- package/src/runtime/auto-resume.ts +100 -0
- package/src/runtime/background-runner.ts +11 -1
- package/src/runtime/cancellation-token.ts +89 -89
- package/src/runtime/cancellation.ts +61 -61
- package/src/runtime/capability-inventory.ts +116 -116
- package/src/runtime/child-pi.ts +7 -2
- package/src/runtime/compaction-summary.ts +271 -0
- package/src/runtime/completion-guard.ts +190 -190
- package/src/runtime/crash-recovery.ts +33 -1
- package/src/runtime/delta-conflict.ts +360 -0
- package/src/runtime/direct-run.ts +35 -35
- package/src/runtime/foreground-control.ts +82 -82
- package/src/runtime/green-contract.ts +46 -46
- package/src/runtime/group-join.ts +106 -106
- package/src/runtime/heartbeat-gradient.ts +28 -28
- package/src/runtime/heartbeat-watcher.ts +124 -124
- package/src/runtime/iteration-hooks.ts +264 -0
- package/src/runtime/live-agent-control.ts +88 -88
- package/src/runtime/live-control-realtime.ts +36 -36
- package/src/runtime/live-extension-bridge.ts +150 -150
- package/src/runtime/live-irc.ts +92 -92
- package/src/runtime/live-session-health.ts +100 -100
- package/src/runtime/loop-gates.ts +129 -0
- package/src/runtime/metric-parser.ts +40 -0
- package/src/runtime/notebook-helpers.ts +90 -90
- package/src/runtime/orphan-sentinel.ts +7 -7
- package/src/runtime/parallel-research.ts +44 -44
- package/src/runtime/phase-progress.ts +217 -0
- package/src/runtime/pi-args.ts +38 -11
- package/src/runtime/pi-json-output.ts +111 -111
- package/src/runtime/pi-spawn.ts +57 -7
- package/src/runtime/policy-engine.ts +79 -79
- package/src/runtime/post-checks.ts +122 -0
- package/src/runtime/progress-event-coalescer.ts +43 -43
- package/src/runtime/prose-compressor.ts +164 -164
- package/src/runtime/recovery-recipes.ts +74 -74
- package/src/runtime/result-extractor.ts +121 -121
- package/src/runtime/role-permission.ts +39 -39
- package/src/runtime/sensitive-paths.ts +2 -2
- package/src/runtime/session-resources.ts +25 -25
- package/src/runtime/session-snapshot.ts +59 -59
- package/src/runtime/session-usage.ts +79 -79
- package/src/runtime/sidechain-output.ts +29 -29
- package/src/runtime/stream-preview.ts +177 -177
- package/src/runtime/supervisor-contact.ts +59 -59
- package/src/runtime/task-display.ts +38 -38
- package/src/runtime/task-graph.ts +207 -0
- package/src/runtime/task-quality.ts +207 -0
- package/src/runtime/task-runner/capabilities.ts +78 -78
- package/src/runtime/task-runner/live-executor.ts +7 -1
- package/src/runtime/task-runner/progress.ts +119 -119
- package/src/runtime/task-runner/prompt-pipeline.ts +64 -64
- package/src/runtime/task-runner/result-utils.ts +14 -14
- package/src/runtime/task-runner/run-projection.ts +103 -103
- package/src/runtime/task-runner/state-helpers.ts +22 -22
- package/src/runtime/team-runner.ts +117 -7
- package/src/runtime/worker-heartbeat.ts +21 -21
- package/src/runtime/worker-startup.ts +57 -57
- package/src/runtime/workflow-state.ts +187 -0
- package/src/runtime/workspace-tree.ts +298 -298
- package/src/schema/config-schema.ts +11 -0
- package/src/schema/validation-types.ts +148 -0
- package/src/skills/skill-templates.ts +374 -0
- package/src/state/active-run-registry.ts +35 -11
- package/src/state/atomic-write.ts +33 -26
- package/src/state/contracts.ts +1 -0
- package/src/state/event-reconstructor.ts +217 -0
- package/src/state/locks.ts +2 -13
- package/src/state/mailbox.ts +4 -3
- package/src/state/state-store.ts +16 -6
- package/src/state/task-claims.ts +44 -44
- package/src/state/types.ts +9 -0
- package/src/state/usage.ts +29 -29
- package/src/subagents/async-entry.ts +1 -1
- package/src/subagents/index.ts +3 -3
- package/src/subagents/live/control.ts +1 -1
- package/src/subagents/live/manager.ts +1 -1
- package/src/subagents/live/realtime.ts +1 -1
- package/src/subagents/live/session-runtime.ts +1 -1
- package/src/subagents/manager.ts +1 -1
- package/src/subagents/spawn.ts +1 -1
- package/src/teams/team-serializer.ts +38 -38
- package/src/types/diff.d.ts +18 -18
- package/src/ui/crew-footer.ts +101 -101
- package/src/ui/crew-select-list.ts +111 -111
- package/src/ui/crew-widget.ts +5 -2
- package/src/ui/dashboard-panes/cancellation-pane.ts +42 -42
- package/src/ui/dashboard-panes/capability-pane.ts +59 -59
- package/src/ui/dashboard-panes/mailbox-pane.ts +35 -35
- package/src/ui/dashboard-panes/metrics-pane.ts +34 -34
- package/src/ui/dashboard-panes/progress-pane.ts +11 -0
- package/src/ui/dynamic-border.ts +25 -25
- package/src/ui/layout-primitives.ts +106 -106
- package/src/ui/loaders.ts +158 -158
- package/src/ui/render-coalescer.ts +51 -51
- package/src/ui/render-diff.ts +119 -119
- package/src/ui/render-scheduler.ts +143 -143
- package/src/ui/run-action-dispatcher.ts +10 -1
- package/src/ui/spinner.ts +17 -17
- package/src/ui/status-colors.ts +58 -58
- package/src/ui/syntax-highlight.ts +116 -116
- package/src/ui/transcript-entries.ts +258 -258
- package/src/utils/completion-dedupe.ts +63 -63
- package/src/utils/frontmatter.ts +68 -68
- package/src/utils/git.ts +262 -262
- package/src/utils/ids.ts +17 -17
- package/src/utils/incremental-reader.ts +104 -104
- package/src/utils/names.ts +27 -27
- package/src/utils/redaction.ts +44 -44
- package/src/utils/safe-paths.ts +47 -47
- package/src/utils/scan-cache.ts +136 -136
- package/src/utils/sleep.ts +40 -26
- package/src/utils/task-name-generator.ts +337 -337
- package/src/workflows/validate-workflow.ts +40 -40
- package/src/worktree/branch-freshness.ts +45 -45
- package/teams/default.team.md +12 -12
- package/teams/fast-fix.team.md +11 -11
- package/teams/implementation.team.md +18 -18
- package/teams/parallel-research.team.md +14 -14
- package/teams/research.team.md +11 -11
- package/teams/review.team.md +12 -12
- package/workflows/default.workflow.md +30 -29
- package/workflows/fast-fix.workflow.md +23 -22
- package/workflows/implementation.workflow.md +43 -43
- package/workflows/parallel-research.workflow.md +46 -46
- package/workflows/research.workflow.md +22 -22
- package/workflows/review.workflow.md +30 -30
- package/docs/refactor-tasks-phase3.md +0 -394
- package/docs/refactor-tasks-phase4.md +0 -564
- package/docs/refactor-tasks-phase5.md +0 -402
- package/docs/refactor-tasks-phase6.md +0 -662
- package/docs/refactor-tasks.md +0 -1484
- package/docs/research/AGENT-EXECUTION-ARCHITECTURE.md +0 -261
- package/docs/research/AGENT-LIFECYCLE-COMPARISON.md +0 -111
- package/docs/research/AUDIT_OH_MY_PI.md +0 -261
- package/docs/research/AUDIT_PI_CREW.md +0 -457
- package/docs/research/CAVEMAN-DEEP-RESEARCH.md +0 -281
- package/docs/research/COMPARISON_OH_MY_PI_VS_PI_CREW.md +0 -264
- package/docs/research/DEEP-RESEARCH-PI-POWERBAR.md +0 -343
- package/docs/research/DEEP_RESEARCH_SUBAGENT_ARCHITECTURE.md +0 -480
- package/docs/research/GAP_CLOSURE_IMPLEMENTATION_PLAN.md +0 -354
- package/docs/research/IMPLEMENTATION_PLAN.md +0 -385
- package/docs/research/LIVE-SESSION-PRODUCTION-READY-PLAN.md +0 -502
- package/docs/research/OH-MY-PI-DEEP-RESEARCH-v14.7.6.md +0 -266
- package/docs/research/REMAINING-GAPS-PLAN.md +0 -363
- package/docs/research/SESSION-SUMMARY-2026-05-08.md +0 -146
- package/docs/research/UI-RESPONSIVENESS-AUDIT.md +0 -173
- package/docs/research-awesome-agent-skills-distillation.md +0 -100
- package/docs/research-extension-examples.md +0 -297
- package/docs/research-extension-system.md +0 -324
- package/docs/research-oh-my-pi-distillation.md +0 -369
- package/docs/research-optimization-plan.md +0 -548
- package/docs/research-phase10-distillation.md +0 -199
- package/docs/research-phase11-distillation.md +0 -201
- package/docs/research-phase8-operator-experience-plan.md +0 -819
- package/docs/research-phase9-observability-reliability-plan.md +0 -1190
- package/docs/research-pi-coding-agent.md +0 -357
- package/docs/research-source-pi-crew-reference.md +0 -174
- package/docs/research-ui-optimization-plan.md +0 -480
- package/docs/source-runtime-refactor-map.md +0 -107
- package/src/utils/atomic-write.ts +0 -33
|
@@ -1,59 +1,59 @@
|
|
|
1
|
-
import type { TeamRunManifest } from "../state/types.ts";
|
|
2
|
-
import { appendEvent } from "../state/event-log.ts";
|
|
3
|
-
import { logInternalError } from "../utils/internal-error.ts";
|
|
4
|
-
|
|
5
|
-
export interface SupervisorContactPayload {
|
|
6
|
-
runId: string;
|
|
7
|
-
taskId: string;
|
|
8
|
-
reason: "decision_needed" | "clarification" | "approval" | "error_escalation" | "custom";
|
|
9
|
-
message: string;
|
|
10
|
-
data?: Record<string, unknown>;
|
|
11
|
-
timestamp: string;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* Record a supervisor contact event from a child task.
|
|
16
|
-
* This represents a child→parent communication where the child needs
|
|
17
|
-
* a decision, clarification, or approval to continue.
|
|
18
|
-
*/
|
|
19
|
-
export function recordSupervisorContact(manifest: TeamRunManifest, payload: Omit<SupervisorContactPayload, "timestamp">): void {
|
|
20
|
-
const fullPayload: SupervisorContactPayload = {
|
|
21
|
-
...payload,
|
|
22
|
-
timestamp: new Date().toISOString(),
|
|
23
|
-
};
|
|
24
|
-
try {
|
|
25
|
-
appendEvent(manifest.eventsPath, {
|
|
26
|
-
type: "supervisor.contact",
|
|
27
|
-
runId: manifest.runId,
|
|
28
|
-
taskId: payload.taskId,
|
|
29
|
-
data: fullPayload as unknown as Record<string, unknown>,
|
|
30
|
-
});
|
|
31
|
-
} catch (error) {
|
|
32
|
-
logInternalError("supervisor-contact.record", error, `runId=${manifest.runId} taskId=${payload.taskId}`);
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* Parse a supervisor contact request from child Pi stdout.
|
|
38
|
-
* Detects structured JSON lines with type "supervisor_contact".
|
|
39
|
-
*/
|
|
40
|
-
export function parseSupervisorContactFromLine(line: string): Omit<SupervisorContactPayload, "timestamp" | "runId"> | undefined {
|
|
41
|
-
if (!line.trim()) return undefined;
|
|
42
|
-
let parsed: unknown;
|
|
43
|
-
try {
|
|
44
|
-
parsed = JSON.parse(line);
|
|
45
|
-
} catch {
|
|
46
|
-
return undefined;
|
|
47
|
-
}
|
|
48
|
-
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return undefined;
|
|
49
|
-
const record = parsed as Record<string, unknown>;
|
|
50
|
-
if (record.type !== "supervisor_contact" && record.type !== "crew_supervisor_contact") return undefined;
|
|
51
|
-
return {
|
|
52
|
-
taskId: typeof record.taskId === "string" ? record.taskId : "",
|
|
53
|
-
reason: typeof record.reason === "string" && ["decision_needed", "clarification", "approval", "error_escalation", "custom"].includes(record.reason)
|
|
54
|
-
? record.reason as SupervisorContactPayload["reason"]
|
|
55
|
-
: "custom",
|
|
56
|
-
message: typeof record.message === "string" ? record.message : String(record.message ?? ""),
|
|
57
|
-
data: record.data && typeof record.data === "object" && !Array.isArray(record.data) ? record.data as Record<string, unknown> : undefined,
|
|
58
|
-
};
|
|
59
|
-
}
|
|
1
|
+
import type { TeamRunManifest } from "../state/types.ts";
|
|
2
|
+
import { appendEvent } from "../state/event-log.ts";
|
|
3
|
+
import { logInternalError } from "../utils/internal-error.ts";
|
|
4
|
+
|
|
5
|
+
export interface SupervisorContactPayload {
|
|
6
|
+
runId: string;
|
|
7
|
+
taskId: string;
|
|
8
|
+
reason: "decision_needed" | "clarification" | "approval" | "error_escalation" | "custom";
|
|
9
|
+
message: string;
|
|
10
|
+
data?: Record<string, unknown>;
|
|
11
|
+
timestamp: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Record a supervisor contact event from a child task.
|
|
16
|
+
* This represents a child→parent communication where the child needs
|
|
17
|
+
* a decision, clarification, or approval to continue.
|
|
18
|
+
*/
|
|
19
|
+
export function recordSupervisorContact(manifest: TeamRunManifest, payload: Omit<SupervisorContactPayload, "timestamp">): void {
|
|
20
|
+
const fullPayload: SupervisorContactPayload = {
|
|
21
|
+
...payload,
|
|
22
|
+
timestamp: new Date().toISOString(),
|
|
23
|
+
};
|
|
24
|
+
try {
|
|
25
|
+
appendEvent(manifest.eventsPath, {
|
|
26
|
+
type: "supervisor.contact",
|
|
27
|
+
runId: manifest.runId,
|
|
28
|
+
taskId: payload.taskId,
|
|
29
|
+
data: fullPayload as unknown as Record<string, unknown>,
|
|
30
|
+
});
|
|
31
|
+
} catch (error) {
|
|
32
|
+
logInternalError("supervisor-contact.record", error, `runId=${manifest.runId} taskId=${payload.taskId}`);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Parse a supervisor contact request from child Pi stdout.
|
|
38
|
+
* Detects structured JSON lines with type "supervisor_contact".
|
|
39
|
+
*/
|
|
40
|
+
export function parseSupervisorContactFromLine(line: string): Omit<SupervisorContactPayload, "timestamp" | "runId"> | undefined {
|
|
41
|
+
if (!line.trim()) return undefined;
|
|
42
|
+
let parsed: unknown;
|
|
43
|
+
try {
|
|
44
|
+
parsed = JSON.parse(line);
|
|
45
|
+
} catch {
|
|
46
|
+
return undefined;
|
|
47
|
+
}
|
|
48
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return undefined;
|
|
49
|
+
const record = parsed as Record<string, unknown>;
|
|
50
|
+
if (record.type !== "supervisor_contact" && record.type !== "crew_supervisor_contact") return undefined;
|
|
51
|
+
return {
|
|
52
|
+
taskId: typeof record.taskId === "string" ? record.taskId : "",
|
|
53
|
+
reason: typeof record.reason === "string" && ["decision_needed", "clarification", "approval", "error_escalation", "custom"].includes(record.reason)
|
|
54
|
+
? record.reason as SupervisorContactPayload["reason"]
|
|
55
|
+
: "custom",
|
|
56
|
+
message: typeof record.message === "string" ? record.message : String(record.message ?? ""),
|
|
57
|
+
data: record.data && typeof record.data === "object" && !Array.isArray(record.data) ? record.data as Record<string, unknown> : undefined,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
@@ -1,38 +1,38 @@
|
|
|
1
|
-
import type { TeamTaskState } from "../state/types.ts";
|
|
2
|
-
import type { CrewAgentRecord, CrewRuntimeKind } from "./crew-agent-runtime.ts";
|
|
3
|
-
import { recordFromTask } from "./crew-agent-records.ts";
|
|
4
|
-
import type { TeamRunManifest } from "../state/types.ts";
|
|
5
|
-
|
|
6
|
-
export function shouldMaterializeAgent(task: TeamTaskState): boolean {
|
|
7
|
-
return task.status !== "queued" && task.status !== "skipped";
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
export function recordsForMaterializedTasks(manifest: TeamRunManifest, tasks: TeamTaskState[], runtime: CrewRuntimeKind): CrewAgentRecord[] {
|
|
11
|
-
return tasks.filter(shouldMaterializeAgent).map((task) => recordFromTask(manifest, task, runtime));
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export function taskById(tasks: TeamTaskState[]): Map<string, TeamTaskState> {
|
|
15
|
-
const map = new Map<string, TeamTaskState>();
|
|
16
|
-
for (const task of tasks) {
|
|
17
|
-
map.set(task.id, task);
|
|
18
|
-
if (task.stepId) map.set(task.stepId, task);
|
|
19
|
-
}
|
|
20
|
-
return map;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export function waitingReason(task: TeamTaskState, tasks: TeamTaskState[]): string | undefined {
|
|
24
|
-
if (task.status !== "queued") return undefined;
|
|
25
|
-
const byId = taskById(tasks);
|
|
26
|
-
const waiting = task.dependsOn.map((id) => byId.get(id)?.id ?? id).filter((id) => byId.get(id)?.status !== "completed");
|
|
27
|
-
if (waiting.length === 0) return "ready";
|
|
28
|
-
return `waiting for ${waiting.join(", ")}`;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
export function formatTaskGraphLines(tasks: TeamTaskState[]): string[] {
|
|
32
|
-
if (tasks.length === 0) return ["- (none)"];
|
|
33
|
-
return tasks.map((task) => {
|
|
34
|
-
const icon = task.status === "completed" ? "✓" : task.status === "running" ? "⠋" : task.status === "failed" ? "✗" : task.status === "cancelled" || task.status === "skipped" ? "■" : "◦";
|
|
35
|
-
const wait = waitingReason(task, tasks);
|
|
36
|
-
return `- ${icon} ${task.id} [${task.status}] ${task.role}->${task.agent}${wait && wait !== "ready" ? ` (${wait})` : ""}`;
|
|
37
|
-
});
|
|
38
|
-
}
|
|
1
|
+
import type { TeamTaskState } from "../state/types.ts";
|
|
2
|
+
import type { CrewAgentRecord, CrewRuntimeKind } from "./crew-agent-runtime.ts";
|
|
3
|
+
import { recordFromTask } from "./crew-agent-records.ts";
|
|
4
|
+
import type { TeamRunManifest } from "../state/types.ts";
|
|
5
|
+
|
|
6
|
+
export function shouldMaterializeAgent(task: TeamTaskState): boolean {
|
|
7
|
+
return task.status !== "queued" && task.status !== "skipped";
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function recordsForMaterializedTasks(manifest: TeamRunManifest, tasks: TeamTaskState[], runtime: CrewRuntimeKind): CrewAgentRecord[] {
|
|
11
|
+
return tasks.filter(shouldMaterializeAgent).map((task) => recordFromTask(manifest, task, runtime));
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function taskById(tasks: TeamTaskState[]): Map<string, TeamTaskState> {
|
|
15
|
+
const map = new Map<string, TeamTaskState>();
|
|
16
|
+
for (const task of tasks) {
|
|
17
|
+
map.set(task.id, task);
|
|
18
|
+
if (task.stepId) map.set(task.stepId, task);
|
|
19
|
+
}
|
|
20
|
+
return map;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function waitingReason(task: TeamTaskState, tasks: TeamTaskState[]): string | undefined {
|
|
24
|
+
if (task.status !== "queued") return undefined;
|
|
25
|
+
const byId = taskById(tasks);
|
|
26
|
+
const waiting = task.dependsOn.map((id) => byId.get(id)?.id ?? id).filter((id) => byId.get(id)?.status !== "completed");
|
|
27
|
+
if (waiting.length === 0) return "ready";
|
|
28
|
+
return `waiting for ${waiting.join(", ")}`;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function formatTaskGraphLines(tasks: TeamTaskState[]): string[] {
|
|
32
|
+
if (tasks.length === 0) return ["- (none)"];
|
|
33
|
+
return tasks.map((task) => {
|
|
34
|
+
const icon = task.status === "completed" ? "✓" : task.status === "running" ? "⠋" : task.status === "failed" ? "✗" : task.status === "cancelled" || task.status === "skipped" ? "■" : "◦";
|
|
35
|
+
const wait = waitingReason(task, tasks);
|
|
36
|
+
return `- ${icon} ${task.id} [${task.status}] ${task.role}->${task.agent}${wait && wait !== "ready" ? ` (${wait})` : ""}`;
|
|
37
|
+
});
|
|
38
|
+
}
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DAG-based task execution order calculator.
|
|
3
|
+
*
|
|
4
|
+
* Uses Kahn's algorithm for topological sort and DFS for cycle detection.
|
|
5
|
+
* Groups tasks into parallel "waves" where all tasks in wave N can run
|
|
6
|
+
* concurrently and wave N+1 depends on at least one task in wave N.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/** A lightweight node representation for the execution DAG. */
|
|
10
|
+
export interface TaskNode {
|
|
11
|
+
id: string;
|
|
12
|
+
dependsOn: string[];
|
|
13
|
+
phase?: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** A group of tasks that can all run in parallel. */
|
|
17
|
+
export interface ExecutionWave {
|
|
18
|
+
index: number;
|
|
19
|
+
taskIds: string[];
|
|
20
|
+
label?: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** The full execution plan produced by topological sort. */
|
|
24
|
+
export interface ExecutionPlan {
|
|
25
|
+
waves: ExecutionWave[];
|
|
26
|
+
hasCycle: boolean;
|
|
27
|
+
cycleNodes?: string[];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Build an execution plan from a flat list of task nodes using Kahn's algorithm.
|
|
32
|
+
*
|
|
33
|
+
* - Tasks with empty `dependsOn` go into wave 0.
|
|
34
|
+
* - Each subsequent wave contains tasks whose dependencies are all in earlier waves.
|
|
35
|
+
* - If all tasks have empty `dependsOn`, they all go into wave 0 (backward compatible).
|
|
36
|
+
* - If a cycle is detected, `hasCycle` is true and `cycleNodes` lists the involved IDs.
|
|
37
|
+
*/
|
|
38
|
+
export function buildExecutionPlan(tasks: TaskNode[]): ExecutionPlan {
|
|
39
|
+
if (tasks.length === 0) {
|
|
40
|
+
return { waves: [], hasCycle: false };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const idSet = new Set<string>(tasks.map((t) => t.id));
|
|
44
|
+
const adjacency = new Map<string, Set<string>>(); // id -> ids that depend on it
|
|
45
|
+
const inDegree = new Map<string, number>();
|
|
46
|
+
|
|
47
|
+
for (const task of tasks) {
|
|
48
|
+
adjacency.set(task.id, new Set<string>());
|
|
49
|
+
inDegree.set(task.id, 0);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
for (const task of tasks) {
|
|
53
|
+
let degree = 0;
|
|
54
|
+
for (const dep of task.dependsOn) {
|
|
55
|
+
if (!idSet.has(dep)) continue; // ignore unknown deps
|
|
56
|
+
adjacency.get(dep)!.add(task.id);
|
|
57
|
+
degree++;
|
|
58
|
+
}
|
|
59
|
+
inDegree.set(task.id, degree);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Kahn's algorithm with wave grouping
|
|
63
|
+
const waves: ExecutionWave[] = [];
|
|
64
|
+
const assigned = new Set<string>();
|
|
65
|
+
let currentWaveIds = tasks
|
|
66
|
+
.filter((t) => inDegree.get(t.id) === 0)
|
|
67
|
+
.map((t) => t.id);
|
|
68
|
+
|
|
69
|
+
let waveIndex = 0;
|
|
70
|
+
while (currentWaveIds.length > 0) {
|
|
71
|
+
for (const id of currentWaveIds) assigned.add(id);
|
|
72
|
+
|
|
73
|
+
const wave = buildWave(tasks, currentWaveIds, waveIndex);
|
|
74
|
+
waves.push(wave);
|
|
75
|
+
|
|
76
|
+
// Decrement in-degrees for dependents
|
|
77
|
+
const nextWaveCandidates = new Set<string>();
|
|
78
|
+
for (const id of currentWaveIds) {
|
|
79
|
+
for (const dependent of adjacency.get(id) ?? []) {
|
|
80
|
+
const current = inDegree.get(dependent)!;
|
|
81
|
+
inDegree.set(dependent, current - 1);
|
|
82
|
+
if (current - 1 === 0) nextWaveCandidates.add(dependent);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
currentWaveIds = [...nextWaveCandidates];
|
|
87
|
+
waveIndex++;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Detect cycle: if not all tasks were assigned, remaining ones form cycles
|
|
91
|
+
if (assigned.size < tasks.length) {
|
|
92
|
+
const cycleNodes = tasks
|
|
93
|
+
.filter((t) => !assigned.has(t.id))
|
|
94
|
+
.map((t) => t.id);
|
|
95
|
+
return {
|
|
96
|
+
waves,
|
|
97
|
+
hasCycle: true,
|
|
98
|
+
cycleNodes,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return { waves, hasCycle: false };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Derive the phase label for a wave. If all tasks in the wave share the same
|
|
107
|
+
* `phase` value, use it as the wave label; otherwise leave it undefined.
|
|
108
|
+
*/
|
|
109
|
+
function buildWave(tasks: TaskNode[], ids: string[], index: number): ExecutionWave {
|
|
110
|
+
const taskMap = new Map(tasks.map((t) => [t.id, t]));
|
|
111
|
+
const waveTasks = ids.map((id) => taskMap.get(id)!).filter(Boolean);
|
|
112
|
+
|
|
113
|
+
let label: string | undefined;
|
|
114
|
+
if (waveTasks.length > 0 && waveTasks.every((t) => t.phase !== undefined)) {
|
|
115
|
+
const phases = new Set(waveTasks.map((t) => t.phase));
|
|
116
|
+
if (phases.size === 1) label = [...phases][0];
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return { index, taskIds: ids, label };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Return the IDs of tasks that are ready to run given a set of completed tasks.
|
|
124
|
+
*
|
|
125
|
+
* A task is "ready" when all its dependencies are in `completedTaskIds` AND
|
|
126
|
+
* it has not already been completed itself. Returns tasks from the earliest
|
|
127
|
+
* wave that still has uncompleted tasks.
|
|
128
|
+
*/
|
|
129
|
+
export function getReadyTasks(plan: ExecutionPlan, completedTaskIds: Set<string>): string[] {
|
|
130
|
+
if (plan.hasCycle || plan.waves.length === 0) return [];
|
|
131
|
+
|
|
132
|
+
const completed = completedTaskIds;
|
|
133
|
+
|
|
134
|
+
for (const wave of plan.waves) {
|
|
135
|
+
// All tasks in prior waves must be completed for this wave to be ready
|
|
136
|
+
const priorWavesComplete = plan.waves
|
|
137
|
+
.slice(0, wave.index)
|
|
138
|
+
.every((w) => w.taskIds.every((id) => completed.has(id)));
|
|
139
|
+
|
|
140
|
+
if (!priorWavesComplete) continue;
|
|
141
|
+
|
|
142
|
+
// Filter to tasks not already completed
|
|
143
|
+
const ready = wave.taskIds.filter((id) => !completed.has(id));
|
|
144
|
+
if (ready.length > 0) return ready;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return [];
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Detect all cycles in the task graph using DFS.
|
|
152
|
+
*
|
|
153
|
+
* Returns an array of cycles, where each cycle is represented as an array of
|
|
154
|
+
* task IDs forming a path from a node back to itself.
|
|
155
|
+
*/
|
|
156
|
+
export function detectCycles(tasks: TaskNode[]): string[][] {
|
|
157
|
+
if (tasks.length === 0) return [];
|
|
158
|
+
|
|
159
|
+
const idSet = new Set<string>(tasks.map((t) => t.id));
|
|
160
|
+
const adjacency = new Map<string, string[]>();
|
|
161
|
+
for (const task of tasks) {
|
|
162
|
+
adjacency.set(
|
|
163
|
+
task.id,
|
|
164
|
+
task.dependsOn.filter((dep) => idSet.has(dep)),
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const WHITE = 0;
|
|
169
|
+
const GRAY = 1;
|
|
170
|
+
const BLACK = 2;
|
|
171
|
+
|
|
172
|
+
const color = new Map<string, number>();
|
|
173
|
+
for (const task of tasks) color.set(task.id, WHITE);
|
|
174
|
+
|
|
175
|
+
const cycles: string[][] = [];
|
|
176
|
+
const path: string[] = [];
|
|
177
|
+
|
|
178
|
+
function dfs(nodeId: string): void {
|
|
179
|
+
color.set(nodeId, GRAY);
|
|
180
|
+
path.push(nodeId);
|
|
181
|
+
|
|
182
|
+
const deps = adjacency.get(nodeId) ?? [];
|
|
183
|
+
for (const dep of deps) {
|
|
184
|
+
const depColor = color.get(dep);
|
|
185
|
+
if (depColor === GRAY) {
|
|
186
|
+
// Found a cycle: extract the path from dep to current node
|
|
187
|
+
const cycleStart = path.indexOf(dep);
|
|
188
|
+
if (cycleStart >= 0) {
|
|
189
|
+
cycles.push(path.slice(cycleStart));
|
|
190
|
+
}
|
|
191
|
+
} else if (depColor === WHITE) {
|
|
192
|
+
dfs(dep);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
path.pop();
|
|
197
|
+
color.set(nodeId, BLACK);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
for (const task of tasks) {
|
|
201
|
+
if (color.get(task.id) === WHITE) {
|
|
202
|
+
dfs(task.id);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
return cycles;
|
|
207
|
+
}
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Task quality scoring — simple additive heuristic for evaluating task
|
|
3
|
+
* completion quality based on diagnostics, metrics, artifacts, and duration.
|
|
4
|
+
*
|
|
5
|
+
* Distilled from pi-autoresearch's quality scoring pattern.
|
|
6
|
+
*/
|
|
7
|
+
import * as fs from "node:fs";
|
|
8
|
+
import * as path from "node:path";
|
|
9
|
+
import type { TeamTaskState } from "../state/types.ts";
|
|
10
|
+
|
|
11
|
+
/** Letter grade for task quality. */
|
|
12
|
+
export type QualityGrade = "A" | "B" | "C" | "D";
|
|
13
|
+
|
|
14
|
+
/** Breakdown of individual quality criteria. */
|
|
15
|
+
export interface QualityBreakdown {
|
|
16
|
+
/** Task has a non-empty diagnostics object. */
|
|
17
|
+
hasDiagnostics: boolean;
|
|
18
|
+
/** Task has a non-empty metrics object. */
|
|
19
|
+
hasMetrics: boolean;
|
|
20
|
+
/** Task produced files in the artifacts directory. */
|
|
21
|
+
producedArtifacts: boolean;
|
|
22
|
+
/** Task has a non-empty result/description. */
|
|
23
|
+
hasDescription: boolean;
|
|
24
|
+
/** Task duration is reasonable (> 0 and < 1 hour). */
|
|
25
|
+
durationReasonable: boolean;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Scored quality result for a task. */
|
|
29
|
+
export interface TaskQualityScore {
|
|
30
|
+
/** Numeric score (0–5). */
|
|
31
|
+
score: number;
|
|
32
|
+
/** Individual criterion breakdown. */
|
|
33
|
+
breakdown: QualityBreakdown;
|
|
34
|
+
/** Letter grade based on score thresholds. */
|
|
35
|
+
grade: QualityGrade;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** One hour in milliseconds. */
|
|
39
|
+
const ONE_HOUR_MS = 3_600_000;
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Determine the letter grade for a given numeric score.
|
|
43
|
+
*
|
|
44
|
+
* A: 4–5, B: 3, C: 2, D: 0–1
|
|
45
|
+
*/
|
|
46
|
+
function scoreToGrade(score: number): QualityGrade {
|
|
47
|
+
if (score >= 4) return "A";
|
|
48
|
+
if (score === 3) return "B";
|
|
49
|
+
if (score === 2) return "C";
|
|
50
|
+
return "D";
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Check whether the artifacts directory contains files for the given task.
|
|
55
|
+
*
|
|
56
|
+
* Looks for a subdirectory named after the task ID, or files containing
|
|
57
|
+
* the task ID prefix in the artifacts directory.
|
|
58
|
+
*/
|
|
59
|
+
function hasTaskArtifacts(taskId: string, artifactsDir: string): boolean {
|
|
60
|
+
try {
|
|
61
|
+
if (!fs.existsSync(artifactsDir)) return false;
|
|
62
|
+
|
|
63
|
+
// Check for a task-specific subdirectory
|
|
64
|
+
const taskDir = path.join(artifactsDir, taskId);
|
|
65
|
+
if (fs.existsSync(taskDir)) {
|
|
66
|
+
const stat = fs.statSync(taskDir);
|
|
67
|
+
if (stat.isDirectory()) {
|
|
68
|
+
const entries = fs.readdirSync(taskDir);
|
|
69
|
+
return entries.length > 0;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Check for files containing the task ID prefix
|
|
74
|
+
const entries = fs.readdirSync(artifactsDir);
|
|
75
|
+
const safePrefix = taskId.replace(/[^a-zA-Z0-9_-]/g, "");
|
|
76
|
+
return entries.some((entry) => entry.includes(safePrefix));
|
|
77
|
+
} catch {
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Check if a task result string is a non-empty description.
|
|
84
|
+
*
|
|
85
|
+
* A result is considered descriptive if any of these sources have non-empty content:
|
|
86
|
+
* - task.resultArtifact exists with a path
|
|
87
|
+
* - task.error is a non-empty string (workers often set this with result info)
|
|
88
|
+
* - task.verification.satisfied is true
|
|
89
|
+
* - task.diagnostics contains a 'result' string
|
|
90
|
+
*/
|
|
91
|
+
function isResultDescriptive(task: TeamTaskState): boolean {
|
|
92
|
+
// Check resultArtifact — presence of a result artifact indicates output was produced
|
|
93
|
+
if (task.resultArtifact?.path) return true;
|
|
94
|
+
|
|
95
|
+
// Check error field — workers often put result info here
|
|
96
|
+
if (typeof task.error === "string" && task.error.trim().length > 0) return true;
|
|
97
|
+
|
|
98
|
+
// Check verification — satisfied verification indicates meaningful output
|
|
99
|
+
if (task.verification?.satisfied) return true;
|
|
100
|
+
|
|
101
|
+
// Check diagnostics for an explicit result string
|
|
102
|
+
if (
|
|
103
|
+
task.diagnostics &&
|
|
104
|
+
typeof task.diagnostics === "object" &&
|
|
105
|
+
typeof task.diagnostics.result === "string" &&
|
|
106
|
+
(task.diagnostics.result as string).trim().length > 0
|
|
107
|
+
) return true;
|
|
108
|
+
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Check if the task duration is reasonable (started, finished, > 0, < 1 hour).
|
|
114
|
+
*/
|
|
115
|
+
function isDurationReasonable(task: TeamTaskState): boolean {
|
|
116
|
+
if (!task.startedAt || !task.finishedAt) return false;
|
|
117
|
+
|
|
118
|
+
const started = new Date(task.startedAt).getTime();
|
|
119
|
+
const finished = new Date(task.finishedAt).getTime();
|
|
120
|
+
|
|
121
|
+
if (Number.isNaN(started) || Number.isNaN(finished)) return false;
|
|
122
|
+
|
|
123
|
+
const duration = finished - started;
|
|
124
|
+
return duration > 0 && duration < ONE_HOUR_MS;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Compute the quality score for a completed task.
|
|
129
|
+
*
|
|
130
|
+
* Uses simple additive scoring across 5 criteria:
|
|
131
|
+
* - hasDiagnostics: +1 if task.diagnostics exists and has keys
|
|
132
|
+
* - hasMetrics: +1 if task.metrics exists and has keys
|
|
133
|
+
* - producedArtifacts: +1 if artifactsDir has files for this task
|
|
134
|
+
* - hasDescription: +1 if task has a non-empty result/description
|
|
135
|
+
* - durationReasonable: +1 if task has both startedAt and finishedAt, duration > 0 and < 1 hour
|
|
136
|
+
*
|
|
137
|
+
* @param task - The task state to evaluate
|
|
138
|
+
* @param artifactsDir - Optional path to the run artifacts directory
|
|
139
|
+
* @returns TaskQualityScore with numeric score, breakdown, and letter grade
|
|
140
|
+
*/
|
|
141
|
+
export function computeTaskQuality(
|
|
142
|
+
task: TeamTaskState,
|
|
143
|
+
artifactsDir?: string,
|
|
144
|
+
): TaskQualityScore {
|
|
145
|
+
const hasDiagnostics =
|
|
146
|
+
task.diagnostics !== undefined &&
|
|
147
|
+
typeof task.diagnostics === "object" &&
|
|
148
|
+
Object.keys(task.diagnostics).length > 0;
|
|
149
|
+
|
|
150
|
+
const hasMetrics =
|
|
151
|
+
task.metrics !== undefined &&
|
|
152
|
+
typeof task.metrics === "object" &&
|
|
153
|
+
Object.keys(task.metrics).length > 0;
|
|
154
|
+
|
|
155
|
+
const producedArtifacts =
|
|
156
|
+
artifactsDir !== undefined && hasTaskArtifacts(task.id, artifactsDir);
|
|
157
|
+
|
|
158
|
+
const hasDescription = isResultDescriptive(task);
|
|
159
|
+
|
|
160
|
+
const durationReasonable = isDurationReasonable(task);
|
|
161
|
+
|
|
162
|
+
const breakdown: QualityBreakdown = {
|
|
163
|
+
hasDiagnostics,
|
|
164
|
+
hasMetrics,
|
|
165
|
+
producedArtifacts,
|
|
166
|
+
hasDescription,
|
|
167
|
+
durationReasonable,
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
const score =
|
|
171
|
+
(hasDiagnostics ? 1 : 0) +
|
|
172
|
+
(hasMetrics ? 1 : 0) +
|
|
173
|
+
(producedArtifacts ? 1 : 0) +
|
|
174
|
+
(hasDescription ? 1 : 0) +
|
|
175
|
+
(durationReasonable ? 1 : 0);
|
|
176
|
+
|
|
177
|
+
return {
|
|
178
|
+
score,
|
|
179
|
+
breakdown,
|
|
180
|
+
grade: scoreToGrade(score),
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/** Human-readable labels for each quality criterion. */
|
|
185
|
+
const CRITERION_LABELS: Record<keyof QualityBreakdown, string> = {
|
|
186
|
+
hasDiagnostics: "diagnostics",
|
|
187
|
+
hasMetrics: "metrics",
|
|
188
|
+
producedArtifacts: "artifacts",
|
|
189
|
+
hasDescription: "description",
|
|
190
|
+
durationReasonable: "duration",
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Format a quality score as a human-readable one-line string.
|
|
195
|
+
*
|
|
196
|
+
* Format: "Quality: B (3/5: diagnostics, metrics, description)"
|
|
197
|
+
*
|
|
198
|
+
* @param score - The quality score to format
|
|
199
|
+
* @returns Formatted string
|
|
200
|
+
*/
|
|
201
|
+
export function formatQualityScore(score: TaskQualityScore): string {
|
|
202
|
+
const metCriteria = Object.entries(score.breakdown)
|
|
203
|
+
.filter(([, met]) => met)
|
|
204
|
+
.map(([key]) => CRITERION_LABELS[key as keyof QualityBreakdown]);
|
|
205
|
+
|
|
206
|
+
return `Quality: ${score.grade} (${score.score}/5${metCriteria.length > 0 ? `: ${metCriteria.join(", ")}` : ""})`;
|
|
207
|
+
}
|