pi-crew 0.1.45 → 0.1.46
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/README.md +5 -5
- package/agents/analyst.md +1 -1
- package/agents/critic.md +1 -1
- package/agents/executor.md +1 -1
- package/agents/explorer.md +1 -1
- package/agents/planner.md +1 -1
- package/agents/reviewer.md +1 -1
- package/agents/security-reviewer.md +1 -1
- package/agents/test-engineer.md +1 -1
- package/agents/verifier.md +1 -1
- package/agents/writer.md +1 -1
- package/docs/next-upgrade-roadmap.md +733 -0
- package/docs/refactor-tasks-phase3.md +394 -394
- package/docs/refactor-tasks-phase4.md +564 -564
- package/docs/refactor-tasks-phase5.md +402 -402
- package/docs/refactor-tasks-phase6.md +662 -662
- package/docs/research-awesome-agent-skills-distillation.md +100 -0
- package/docs/research-extension-examples.md +297 -297
- package/docs/research-extension-system.md +324 -324
- package/docs/research-oh-my-pi-distillation.md +322 -0
- package/docs/research-optimization-plan.md +548 -548
- package/docs/research-phase10-distillation.md +198 -198
- package/docs/research-phase11-distillation.md +201 -201
- package/docs/research-pi-coding-agent.md +357 -357
- package/docs/research-source-pi-crew-reference.md +174 -174
- package/docs/runtime-flow.md +148 -148
- package/docs/source-runtime-refactor-map.md +107 -83
- package/docs/usage.md +3 -3
- package/index.ts +6 -6
- package/install.mjs +52 -8
- package/package.json +1 -1
- package/schema.json +2 -1
- package/skills/async-worker-recovery/SKILL.md +42 -0
- package/skills/context-artifact-hygiene/SKILL.md +52 -0
- package/skills/delegation-patterns/SKILL.md +54 -0
- package/skills/mailbox-interactive/SKILL.md +40 -0
- package/skills/model-routing-context/SKILL.md +39 -0
- package/skills/multi-perspective-review/SKILL.md +58 -0
- package/skills/observability-reliability/SKILL.md +41 -0
- package/skills/ownership-session-security/SKILL.md +41 -0
- package/skills/pi-extension-lifecycle/SKILL.md +39 -0
- package/skills/requirements-to-task-packet/SKILL.md +63 -0
- package/skills/resource-discovery-config/SKILL.md +41 -0
- package/skills/runtime-state-reader/SKILL.md +44 -0
- package/skills/secure-agent-orchestration-review/SKILL.md +45 -0
- package/skills/state-mutation-locking/SKILL.md +42 -0
- package/skills/systematic-debugging/SKILL.md +67 -0
- package/skills/ui-render-performance/SKILL.md +39 -0
- package/skills/verification-before-done/SKILL.md +57 -0
- package/skills/worktree-isolation/SKILL.md +39 -0
- package/src/agents/agent-serializer.ts +34 -34
- package/src/agents/discover-agents.ts +12 -11
- package/src/config/config.ts +48 -24
- package/src/config/defaults.ts +14 -0
- package/src/extension/cross-extension-rpc.ts +82 -82
- package/src/extension/project-init.ts +62 -2
- package/src/extension/register.ts +11 -9
- package/src/extension/registration/commands.ts +32 -25
- package/src/extension/registration/compaction-guard.ts +125 -125
- package/src/extension/registration/subagent-helpers.ts +8 -0
- package/src/extension/registration/subagent-tools.ts +149 -148
- package/src/extension/registration/team-tool.ts +8 -6
- package/src/extension/run-bundle-schema.ts +89 -89
- package/src/extension/run-index.ts +13 -5
- package/src/extension/run-maintenance.ts +62 -43
- package/src/extension/team-tool/api.ts +25 -8
- package/src/extension/team-tool/cancel.ts +33 -4
- package/src/extension/team-tool/context.ts +5 -0
- package/src/extension/team-tool/handle-settings.ts +188 -188
- package/src/extension/team-tool/inspect.ts +41 -41
- package/src/extension/team-tool/lifecycle-actions.ts +91 -79
- package/src/extension/team-tool/plan.ts +19 -19
- package/src/extension/team-tool/respond.ts +37 -17
- package/src/extension/team-tool/run.ts +52 -10
- package/src/extension/team-tool/status.ts +12 -1
- package/src/extension/team-tool-types.ts +2 -0
- package/src/extension/team-tool.ts +32 -11
- package/src/i18n.ts +184 -184
- package/src/observability/event-to-metric.ts +8 -1
- package/src/observability/exporters/otlp-exporter.ts +77 -77
- package/src/prompt/prompt-runtime.ts +72 -72
- package/src/runtime/agent-control.ts +63 -63
- 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/background-runner.ts +59 -53
- package/src/runtime/cancellation.ts +51 -0
- package/src/runtime/child-pi.ts +457 -444
- package/src/runtime/completion-guard.ts +190 -190
- package/src/runtime/crash-recovery.ts +1 -0
- package/src/runtime/crew-agent-records.ts +38 -6
- package/src/runtime/deadletter.ts +1 -0
- package/src/runtime/delivery-coordinator.ts +46 -25
- package/src/runtime/direct-run.ts +35 -35
- package/src/runtime/effectiveness.ts +76 -0
- 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/live-agent-control.ts +88 -87
- package/src/runtime/live-agent-manager.ts +103 -85
- package/src/runtime/live-control-realtime.ts +36 -36
- package/src/runtime/live-session-runtime.ts +309 -305
- package/src/runtime/manifest-cache.ts +17 -2
- package/src/runtime/model-fallback.ts +6 -4
- package/src/runtime/parallel-research.ts +44 -44
- package/src/runtime/pi-args.ts +18 -3
- package/src/runtime/pi-json-output.ts +111 -111
- package/src/runtime/policy-engine.ts +79 -79
- package/src/runtime/process-status.ts +5 -1
- package/src/runtime/progress-event-coalescer.ts +43 -43
- package/src/runtime/recovery-recipes.ts +74 -74
- package/src/runtime/retry-executor.ts +81 -64
- package/src/runtime/role-permission.ts +39 -39
- package/src/runtime/runtime-resolver.ts +22 -6
- 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/skill-instructions.ts +222 -0
- package/src/runtime/stale-reconciler.ts +4 -14
- package/src/runtime/subagent-manager.ts +3 -0
- package/src/runtime/supervisor-contact.ts +59 -59
- package/src/runtime/task-display.ts +38 -38
- package/src/runtime/task-output-context.ts +127 -127
- package/src/runtime/task-runner/capabilities.ts +78 -0
- package/src/runtime/task-runner/live-executor.ts +105 -101
- package/src/runtime/task-runner/progress.ts +119 -119
- package/src/runtime/task-runner/prompt-builder.ts +3 -1
- package/src/runtime/task-runner/prompt-pipeline.ts +64 -0
- package/src/runtime/task-runner/result-utils.ts +14 -14
- package/src/runtime/task-runner/state-helpers.ts +22 -22
- package/src/runtime/task-runner.ts +44 -5
- package/src/runtime/team-runner.ts +78 -15
- package/src/runtime/worker-heartbeat.ts +21 -21
- package/src/runtime/worker-startup.ts +57 -57
- package/src/schema/config-schema.ts +1 -0
- package/src/schema/team-tool-schema.ts +3 -3
- package/src/state/active-run-registry.ts +165 -0
- package/src/state/contracts.ts +1 -1
- package/src/state/mailbox.ts +44 -4
- package/src/state/state-store.ts +8 -1
- package/src/state/task-claims.ts +44 -44
- package/src/state/types.ts +44 -2
- 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-config.ts +1 -0
- 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 +4 -3
- package/src/ui/dashboard-panes/metrics-pane.ts +34 -34
- package/src/ui/dashboard-panes/progress-pane.ts +2 -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-diff.ts +119 -119
- package/src/ui/render-scheduler.ts +143 -143
- package/src/ui/run-snapshot-cache.ts +10 -2
- package/src/ui/snapshot-types.ts +2 -0
- 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/utils/atomic-write.ts +33 -33
- 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 +12 -12
- package/src/utils/names.ts +27 -27
- package/src/utils/paths.ts +4 -2
- package/src/utils/redaction.ts +44 -44
- package/src/utils/safe-paths.ts +47 -47
- package/src/utils/sleep.ts +32 -32
- package/src/workflows/validate-workflow.ts +40 -40
- package/src/workflows/workflow-config.ts +1 -0
- 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 +29 -29
- package/workflows/fast-fix.workflow.md +22 -22
- package/workflows/implementation.workflow.md +38 -38
- package/workflows/parallel-research.workflow.md +46 -46
- package/workflows/research.workflow.md +22 -22
- package/workflows/review.workflow.md +30 -30
|
@@ -1,59 +1,59 @@
|
|
|
1
|
-
import type { TeamRunManifest, TeamTaskState } from "../state/types.ts";
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Creates a lightweight snapshot of task state for event emission.
|
|
5
|
-
* Prevents mutation-during-callback issues by copying relevant fields.
|
|
6
|
-
*/
|
|
7
|
-
export function snapshotTaskState(task: TeamTaskState): Readonly<TeamTaskState> {
|
|
8
|
-
return {
|
|
9
|
-
...task,
|
|
10
|
-
dependsOn: [...task.dependsOn],
|
|
11
|
-
usage: task.usage ? { ...task.usage } : undefined,
|
|
12
|
-
agentProgress: task.agentProgress ? { ...task.agentProgress } : undefined,
|
|
13
|
-
heartbeat: task.heartbeat ? { ...task.heartbeat } : undefined,
|
|
14
|
-
modelAttempts: task.modelAttempts?.map((a) => ({ ...a })),
|
|
15
|
-
modelRouting: task.modelRouting ? { ...task.modelRouting } : undefined,
|
|
16
|
-
claim: task.claim ? { ...task.claim } : undefined,
|
|
17
|
-
checkpoint: task.checkpoint ? { ...task.checkpoint } : undefined,
|
|
18
|
-
attempts: task.attempts?.map((a) => ({ ...a })),
|
|
19
|
-
worktree: task.worktree ? { ...task.worktree } : undefined,
|
|
20
|
-
};
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* Session state snapshot for persistence before session switches.
|
|
25
|
-
* Captures the minimal set of data needed to resume operations.
|
|
26
|
-
*/
|
|
27
|
-
export interface SessionStateSnapshot {
|
|
28
|
-
/** ISO timestamp of the snapshot */
|
|
29
|
-
capturedAt: string;
|
|
30
|
-
/** Active run IDs at time of snapshot */
|
|
31
|
-
activeRunIds: string[];
|
|
32
|
-
/** Number of pending deliveries */
|
|
33
|
-
pendingDeliveryCount: number;
|
|
34
|
-
/** Session generation counter */
|
|
35
|
-
sessionGeneration: number;
|
|
36
|
-
/** Summary of active tasks by status */
|
|
37
|
-
taskSummary: Record<string, number>;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
* Create a session state snapshot for pre-switch persistence.
|
|
42
|
-
*/
|
|
43
|
-
export function createSessionSnapshot(
|
|
44
|
-
activeRuns: TeamRunManifest[],
|
|
45
|
-
pendingDeliveryCount: number,
|
|
46
|
-
sessionGeneration: number,
|
|
47
|
-
): SessionStateSnapshot {
|
|
48
|
-
const taskSummary: Record<string, number> = {};
|
|
49
|
-
for (const run of activeRuns) {
|
|
50
|
-
taskSummary[run.status] = (taskSummary[run.status] ?? 0) + 1;
|
|
51
|
-
}
|
|
52
|
-
return {
|
|
53
|
-
capturedAt: new Date().toISOString(),
|
|
54
|
-
activeRunIds: activeRuns.map((r) => r.runId),
|
|
55
|
-
pendingDeliveryCount,
|
|
56
|
-
sessionGeneration,
|
|
57
|
-
taskSummary,
|
|
58
|
-
};
|
|
59
|
-
}
|
|
1
|
+
import type { TeamRunManifest, TeamTaskState } from "../state/types.ts";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Creates a lightweight snapshot of task state for event emission.
|
|
5
|
+
* Prevents mutation-during-callback issues by copying relevant fields.
|
|
6
|
+
*/
|
|
7
|
+
export function snapshotTaskState(task: TeamTaskState): Readonly<TeamTaskState> {
|
|
8
|
+
return {
|
|
9
|
+
...task,
|
|
10
|
+
dependsOn: [...task.dependsOn],
|
|
11
|
+
usage: task.usage ? { ...task.usage } : undefined,
|
|
12
|
+
agentProgress: task.agentProgress ? { ...task.agentProgress } : undefined,
|
|
13
|
+
heartbeat: task.heartbeat ? { ...task.heartbeat } : undefined,
|
|
14
|
+
modelAttempts: task.modelAttempts?.map((a) => ({ ...a })),
|
|
15
|
+
modelRouting: task.modelRouting ? { ...task.modelRouting } : undefined,
|
|
16
|
+
claim: task.claim ? { ...task.claim } : undefined,
|
|
17
|
+
checkpoint: task.checkpoint ? { ...task.checkpoint } : undefined,
|
|
18
|
+
attempts: task.attempts?.map((a) => ({ ...a })),
|
|
19
|
+
worktree: task.worktree ? { ...task.worktree } : undefined,
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Session state snapshot for persistence before session switches.
|
|
25
|
+
* Captures the minimal set of data needed to resume operations.
|
|
26
|
+
*/
|
|
27
|
+
export interface SessionStateSnapshot {
|
|
28
|
+
/** ISO timestamp of the snapshot */
|
|
29
|
+
capturedAt: string;
|
|
30
|
+
/** Active run IDs at time of snapshot */
|
|
31
|
+
activeRunIds: string[];
|
|
32
|
+
/** Number of pending deliveries */
|
|
33
|
+
pendingDeliveryCount: number;
|
|
34
|
+
/** Session generation counter */
|
|
35
|
+
sessionGeneration: number;
|
|
36
|
+
/** Summary of active tasks by status */
|
|
37
|
+
taskSummary: Record<string, number>;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Create a session state snapshot for pre-switch persistence.
|
|
42
|
+
*/
|
|
43
|
+
export function createSessionSnapshot(
|
|
44
|
+
activeRuns: TeamRunManifest[],
|
|
45
|
+
pendingDeliveryCount: number,
|
|
46
|
+
sessionGeneration: number,
|
|
47
|
+
): SessionStateSnapshot {
|
|
48
|
+
const taskSummary: Record<string, number> = {};
|
|
49
|
+
for (const run of activeRuns) {
|
|
50
|
+
taskSummary[run.status] = (taskSummary[run.status] ?? 0) + 1;
|
|
51
|
+
}
|
|
52
|
+
return {
|
|
53
|
+
capturedAt: new Date().toISOString(),
|
|
54
|
+
activeRunIds: activeRuns.map((r) => r.runId),
|
|
55
|
+
pendingDeliveryCount,
|
|
56
|
+
sessionGeneration,
|
|
57
|
+
taskSummary,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
@@ -1,79 +1,79 @@
|
|
|
1
|
-
import * as fs from "node:fs";
|
|
2
|
-
import type { UsageState } from "../state/types.ts";
|
|
3
|
-
|
|
4
|
-
function asRecord(value: unknown): Record<string, unknown> | undefined {
|
|
5
|
-
return value && typeof value === "object" && !Array.isArray(value) ? value as Record<string, unknown> : undefined;
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
function numberField(obj: Record<string, unknown>, keys: string[]): number | undefined {
|
|
9
|
-
for (const key of keys) {
|
|
10
|
-
const value = obj[key];
|
|
11
|
-
if (typeof value === "number" && Number.isFinite(value)) return value;
|
|
12
|
-
}
|
|
13
|
-
return undefined;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
function usageFromValue(value: unknown): UsageState | undefined {
|
|
17
|
-
const obj = asRecord(value);
|
|
18
|
-
if (!obj) return undefined;
|
|
19
|
-
const direct: UsageState = {
|
|
20
|
-
input: numberField(obj, ["input", "inputTokens", "input_tokens"]),
|
|
21
|
-
output: numberField(obj, ["output", "outputTokens", "output_tokens"]),
|
|
22
|
-
cacheRead: numberField(obj, ["cacheRead", "cache_read", "cacheReadTokens", "cache_read_tokens"]),
|
|
23
|
-
cacheWrite: numberField(obj, ["cacheWrite", "cache_write", "cacheWriteTokens", "cache_write_tokens"]),
|
|
24
|
-
cost: numberField(obj, ["cost", "costUsd", "cost_usd"]),
|
|
25
|
-
turns: numberField(obj, ["turns", "turnCount", "turn_count"]),
|
|
26
|
-
};
|
|
27
|
-
if (Object.values(direct).some((entry) => entry !== undefined)) return direct;
|
|
28
|
-
for (const key of ["usage", "tokenUsage", "tokens", "stats"]) {
|
|
29
|
-
const nested = usageFromValue(obj[key]);
|
|
30
|
-
if (nested) return nested;
|
|
31
|
-
}
|
|
32
|
-
const message = asRecord(obj.message);
|
|
33
|
-
return message ? usageFromValue(message.usage) : undefined;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
function addUsage(total: UsageState, usage: UsageState): UsageState {
|
|
37
|
-
return {
|
|
38
|
-
input: (total.input ?? 0) + (usage.input ?? 0),
|
|
39
|
-
output: (total.output ?? 0) + (usage.output ?? 0),
|
|
40
|
-
cacheRead: (total.cacheRead ?? 0) + (usage.cacheRead ?? 0),
|
|
41
|
-
cacheWrite: (total.cacheWrite ?? 0) + (usage.cacheWrite ?? 0),
|
|
42
|
-
cost: (total.cost ?? 0) + (usage.cost ?? 0),
|
|
43
|
-
turns: (total.turns ?? 0) + (usage.turns ?? 0),
|
|
44
|
-
};
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
function compactUsage(total: UsageState, foundKeys: Set<keyof UsageState>): UsageState | undefined {
|
|
48
|
-
if (foundKeys.size === 0) return undefined;
|
|
49
|
-
const compact: UsageState = {};
|
|
50
|
-
for (const key of foundKeys) compact[key] = total[key];
|
|
51
|
-
return compact;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
export function parseSessionUsageFromJsonlText(text: string): UsageState | undefined {
|
|
55
|
-
let total: UsageState = {};
|
|
56
|
-
const foundKeys = new Set<keyof UsageState>();
|
|
57
|
-
for (const line of text.split(/\r?\n/)) {
|
|
58
|
-
const trimmed = line.trim();
|
|
59
|
-
if (!trimmed) continue;
|
|
60
|
-
try {
|
|
61
|
-
const usage = usageFromValue(JSON.parse(trimmed) as unknown);
|
|
62
|
-
if (!usage) continue;
|
|
63
|
-
for (const key of Object.keys(usage) as Array<keyof UsageState>) foundKeys.add(key);
|
|
64
|
-
total = addUsage(total, usage);
|
|
65
|
-
} catch {
|
|
66
|
-
// Session JSONL can contain partial/corrupt lines after interrupted workers.
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
return compactUsage(total, foundKeys);
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
export function parseSessionUsage(filePath: string): UsageState | undefined {
|
|
73
|
-
try {
|
|
74
|
-
if (!fs.existsSync(filePath)) return undefined;
|
|
75
|
-
return parseSessionUsageFromJsonlText(fs.readFileSync(filePath, "utf-8"));
|
|
76
|
-
} catch {
|
|
77
|
-
return undefined;
|
|
78
|
-
}
|
|
79
|
-
}
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import type { UsageState } from "../state/types.ts";
|
|
3
|
+
|
|
4
|
+
function asRecord(value: unknown): Record<string, unknown> | undefined {
|
|
5
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value as Record<string, unknown> : undefined;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function numberField(obj: Record<string, unknown>, keys: string[]): number | undefined {
|
|
9
|
+
for (const key of keys) {
|
|
10
|
+
const value = obj[key];
|
|
11
|
+
if (typeof value === "number" && Number.isFinite(value)) return value;
|
|
12
|
+
}
|
|
13
|
+
return undefined;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function usageFromValue(value: unknown): UsageState | undefined {
|
|
17
|
+
const obj = asRecord(value);
|
|
18
|
+
if (!obj) return undefined;
|
|
19
|
+
const direct: UsageState = {
|
|
20
|
+
input: numberField(obj, ["input", "inputTokens", "input_tokens"]),
|
|
21
|
+
output: numberField(obj, ["output", "outputTokens", "output_tokens"]),
|
|
22
|
+
cacheRead: numberField(obj, ["cacheRead", "cache_read", "cacheReadTokens", "cache_read_tokens"]),
|
|
23
|
+
cacheWrite: numberField(obj, ["cacheWrite", "cache_write", "cacheWriteTokens", "cache_write_tokens"]),
|
|
24
|
+
cost: numberField(obj, ["cost", "costUsd", "cost_usd"]),
|
|
25
|
+
turns: numberField(obj, ["turns", "turnCount", "turn_count"]),
|
|
26
|
+
};
|
|
27
|
+
if (Object.values(direct).some((entry) => entry !== undefined)) return direct;
|
|
28
|
+
for (const key of ["usage", "tokenUsage", "tokens", "stats"]) {
|
|
29
|
+
const nested = usageFromValue(obj[key]);
|
|
30
|
+
if (nested) return nested;
|
|
31
|
+
}
|
|
32
|
+
const message = asRecord(obj.message);
|
|
33
|
+
return message ? usageFromValue(message.usage) : undefined;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function addUsage(total: UsageState, usage: UsageState): UsageState {
|
|
37
|
+
return {
|
|
38
|
+
input: (total.input ?? 0) + (usage.input ?? 0),
|
|
39
|
+
output: (total.output ?? 0) + (usage.output ?? 0),
|
|
40
|
+
cacheRead: (total.cacheRead ?? 0) + (usage.cacheRead ?? 0),
|
|
41
|
+
cacheWrite: (total.cacheWrite ?? 0) + (usage.cacheWrite ?? 0),
|
|
42
|
+
cost: (total.cost ?? 0) + (usage.cost ?? 0),
|
|
43
|
+
turns: (total.turns ?? 0) + (usage.turns ?? 0),
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function compactUsage(total: UsageState, foundKeys: Set<keyof UsageState>): UsageState | undefined {
|
|
48
|
+
if (foundKeys.size === 0) return undefined;
|
|
49
|
+
const compact: UsageState = {};
|
|
50
|
+
for (const key of foundKeys) compact[key] = total[key];
|
|
51
|
+
return compact;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function parseSessionUsageFromJsonlText(text: string): UsageState | undefined {
|
|
55
|
+
let total: UsageState = {};
|
|
56
|
+
const foundKeys = new Set<keyof UsageState>();
|
|
57
|
+
for (const line of text.split(/\r?\n/)) {
|
|
58
|
+
const trimmed = line.trim();
|
|
59
|
+
if (!trimmed) continue;
|
|
60
|
+
try {
|
|
61
|
+
const usage = usageFromValue(JSON.parse(trimmed) as unknown);
|
|
62
|
+
if (!usage) continue;
|
|
63
|
+
for (const key of Object.keys(usage) as Array<keyof UsageState>) foundKeys.add(key);
|
|
64
|
+
total = addUsage(total, usage);
|
|
65
|
+
} catch {
|
|
66
|
+
// Session JSONL can contain partial/corrupt lines after interrupted workers.
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return compactUsage(total, foundKeys);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function parseSessionUsage(filePath: string): UsageState | undefined {
|
|
73
|
+
try {
|
|
74
|
+
if (!fs.existsSync(filePath)) return undefined;
|
|
75
|
+
return parseSessionUsageFromJsonlText(fs.readFileSync(filePath, "utf-8"));
|
|
76
|
+
} catch {
|
|
77
|
+
return undefined;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
@@ -1,29 +1,29 @@
|
|
|
1
|
-
import * as fs from "node:fs";
|
|
2
|
-
import * as path from "node:path";
|
|
3
|
-
import { redactSecrets } from "../utils/redaction.ts";
|
|
4
|
-
|
|
5
|
-
export interface SidechainEntry {
|
|
6
|
-
isSidechain: true;
|
|
7
|
-
agentId: string;
|
|
8
|
-
type: string;
|
|
9
|
-
message: unknown;
|
|
10
|
-
timestamp: string;
|
|
11
|
-
cwd: string;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export function writeSidechainEntry(filePath: string, entry: Omit<SidechainEntry, "isSidechain" | "timestamp">): void {
|
|
15
|
-
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
16
|
-
fs.appendFileSync(filePath, `${JSON.stringify(redactSecrets({ isSidechain: true, timestamp: new Date().toISOString(), ...entry }))}\n`, "utf-8");
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export function sidechainOutputPath(stateRoot: string, taskId: string): string {
|
|
20
|
-
return path.join(stateRoot, "agents", taskId, "sidechain.output.jsonl");
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export function eventToSidechainType(event: unknown): string | undefined {
|
|
24
|
-
if (!event || typeof event !== "object" || Array.isArray(event)) return undefined;
|
|
25
|
-
const type = (event as { type?: unknown }).type;
|
|
26
|
-
if (type === "message_start" || type === "message_update" || type === "message_end") return "message";
|
|
27
|
-
if (type === "tool_execution_start" || type === "tool_execution_update" || type === "tool_execution_end") return "tool";
|
|
28
|
-
return typeof type === "string" ? type : undefined;
|
|
29
|
-
}
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import { redactSecrets } from "../utils/redaction.ts";
|
|
4
|
+
|
|
5
|
+
export interface SidechainEntry {
|
|
6
|
+
isSidechain: true;
|
|
7
|
+
agentId: string;
|
|
8
|
+
type: string;
|
|
9
|
+
message: unknown;
|
|
10
|
+
timestamp: string;
|
|
11
|
+
cwd: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function writeSidechainEntry(filePath: string, entry: Omit<SidechainEntry, "isSidechain" | "timestamp">): void {
|
|
15
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
16
|
+
fs.appendFileSync(filePath, `${JSON.stringify(redactSecrets({ isSidechain: true, timestamp: new Date().toISOString(), ...entry }))}\n`, "utf-8");
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function sidechainOutputPath(stateRoot: string, taskId: string): string {
|
|
20
|
+
return path.join(stateRoot, "agents", taskId, "sidechain.output.jsonl");
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function eventToSidechainType(event: unknown): string | undefined {
|
|
24
|
+
if (!event || typeof event !== "object" || Array.isArray(event)) return undefined;
|
|
25
|
+
const type = (event as { type?: unknown }).type;
|
|
26
|
+
if (type === "message_start" || type === "message_update" || type === "message_end") return "message";
|
|
27
|
+
if (type === "tool_execution_start" || type === "tool_execution_update" || type === "tool_execution_end") return "tool";
|
|
28
|
+
return typeof type === "string" ? type : undefined;
|
|
29
|
+
}
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import type { AgentConfig } from "../agents/agent-config.ts";
|
|
5
|
+
import type { TeamRole } from "../teams/team-config.ts";
|
|
6
|
+
import type { WorkflowStep } from "../workflows/workflow-config.ts";
|
|
7
|
+
import { isSafePathId, resolveContainedPath, resolveRealContainedPath } from "../utils/safe-paths.ts";
|
|
8
|
+
|
|
9
|
+
const PACKAGE_SKILLS_DIR = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "..", "skills");
|
|
10
|
+
const MAX_SKILL_CHARS = 1500;
|
|
11
|
+
const MAX_TOTAL_CHARS = 6000;
|
|
12
|
+
const MAX_SKILL_NAME_CHARS = 80;
|
|
13
|
+
const MAX_SELECTED_SKILLS = 32;
|
|
14
|
+
const SKILL_CACHE_MAX_ENTRIES = 128;
|
|
15
|
+
|
|
16
|
+
const DEFAULT_ROLE_SKILLS: Record<string, string[]> = {
|
|
17
|
+
explorer: ["read-only-explorer", "context-artifact-hygiene"],
|
|
18
|
+
analyst: ["read-only-explorer", "requirements-to-task-packet"],
|
|
19
|
+
planner: ["delegation-patterns", "requirements-to-task-packet"],
|
|
20
|
+
critic: ["read-only-explorer", "multi-perspective-review"],
|
|
21
|
+
executor: ["state-mutation-locking", "safe-bash", "verification-before-done"],
|
|
22
|
+
reviewer: ["read-only-explorer", "multi-perspective-review"],
|
|
23
|
+
"security-reviewer": ["secure-agent-orchestration-review", "ownership-session-security"],
|
|
24
|
+
"test-engineer": ["verification-before-done", "safe-bash"],
|
|
25
|
+
verifier: ["verification-before-done", "runtime-state-reader"],
|
|
26
|
+
writer: ["context-artifact-hygiene", "verify-evidence"],
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export interface ResolveTaskSkillsInput {
|
|
30
|
+
role: string;
|
|
31
|
+
agent?: Pick<AgentConfig, "skills">;
|
|
32
|
+
teamRole?: Pick<TeamRole, "skills">;
|
|
33
|
+
step?: Pick<WorkflowStep, "skills">;
|
|
34
|
+
override?: string[] | false;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface RenderSkillInstructionsInput extends ResolveTaskSkillsInput {
|
|
38
|
+
cwd: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function isValidSkillName(name: string): boolean {
|
|
42
|
+
return name.length > 0 && name.length <= MAX_SKILL_NAME_CHARS && isSafePathId(name);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function sanitizeSkillName(name: string): string {
|
|
46
|
+
return name.replace(/[^A-Za-z0-9_-]/g, "_").slice(0, MAX_SKILL_NAME_CHARS) || "invalid";
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function unique(items: string[]): string[] {
|
|
50
|
+
const seen = new Set<string>();
|
|
51
|
+
const result: string[] = [];
|
|
52
|
+
for (const item of items.map((entry) => entry.trim()).filter(Boolean)) {
|
|
53
|
+
if (!isValidSkillName(item)) continue;
|
|
54
|
+
if (seen.has(item)) continue;
|
|
55
|
+
seen.add(item);
|
|
56
|
+
result.push(item);
|
|
57
|
+
}
|
|
58
|
+
return result;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function normalizeSkillOverride(value: string | string[] | boolean | undefined): string[] | false | undefined {
|
|
62
|
+
if (value === false) return false;
|
|
63
|
+
if (typeof value === "string") return value.split(",").map((entry) => entry.trim()).filter(Boolean);
|
|
64
|
+
if (value === true) return undefined;
|
|
65
|
+
if (Array.isArray(value)) return value.map((entry) => entry.trim()).filter(Boolean);
|
|
66
|
+
return undefined;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function defaultSkillsForRole(role: string): string[] {
|
|
70
|
+
return DEFAULT_ROLE_SKILLS[role] ?? [];
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function collectTaskSkillNames(input: ResolveTaskSkillsInput): string[] {
|
|
74
|
+
if (input.override === false) return [];
|
|
75
|
+
const roleDefaultsDisabled = input.teamRole?.skills === false || input.step?.skills === false;
|
|
76
|
+
const names = roleDefaultsDisabled ? [] : defaultSkillsForRole(input.role);
|
|
77
|
+
if (input.agent?.skills?.length) names.push(...input.agent.skills);
|
|
78
|
+
if (Array.isArray(input.teamRole?.skills)) names.push(...input.teamRole.skills);
|
|
79
|
+
if (Array.isArray(input.step?.skills)) names.push(...input.step.skills);
|
|
80
|
+
if (Array.isArray(input.override)) names.push(...input.override);
|
|
81
|
+
return unique(names);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function resolveTaskSkillNames(input: ResolveTaskSkillsInput): string[] {
|
|
85
|
+
return collectTaskSkillNames(input).slice(0, MAX_SELECTED_SKILLS);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function candidateSkillDirs(cwd: string): Array<{ root: string; source: "project" | "package" }> {
|
|
89
|
+
return [
|
|
90
|
+
{ root: path.resolve(cwd, "skills"), source: "project" },
|
|
91
|
+
{ root: PACKAGE_SKILLS_DIR, source: "package" },
|
|
92
|
+
];
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
interface CachedSkillMarkdown {
|
|
96
|
+
path: string;
|
|
97
|
+
source: "project" | "package";
|
|
98
|
+
content: string;
|
|
99
|
+
mtimeMs: number;
|
|
100
|
+
size: number;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const skillReadCache = new Map<string, CachedSkillMarkdown>();
|
|
104
|
+
|
|
105
|
+
function rememberSkill(key: string, value: CachedSkillMarkdown): CachedSkillMarkdown {
|
|
106
|
+
if (skillReadCache.has(key)) skillReadCache.delete(key);
|
|
107
|
+
skillReadCache.set(key, value);
|
|
108
|
+
while (skillReadCache.size > SKILL_CACHE_MAX_ENTRIES) {
|
|
109
|
+
const oldest = skillReadCache.keys().next().value;
|
|
110
|
+
if (!oldest) break;
|
|
111
|
+
skillReadCache.delete(oldest);
|
|
112
|
+
}
|
|
113
|
+
return value;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function clearSkillInstructionCache(): void {
|
|
117
|
+
skillReadCache.clear();
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function cachedSkillFresh(value: CachedSkillMarkdown): boolean {
|
|
121
|
+
try {
|
|
122
|
+
const stat = fs.statSync(value.path);
|
|
123
|
+
return stat.mtimeMs === value.mtimeMs && stat.size === value.size;
|
|
124
|
+
} catch {
|
|
125
|
+
return false;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function readSkillMarkdown(cwd: string, name: string): { path: string; source: "project" | "package"; content: string } | undefined {
|
|
130
|
+
if (!isValidSkillName(name)) return undefined;
|
|
131
|
+
const cacheKey = `${path.resolve(cwd)}:${name}`;
|
|
132
|
+
const cached = skillReadCache.get(cacheKey);
|
|
133
|
+
if (cached && cachedSkillFresh(cached)) return cached;
|
|
134
|
+
if (cached) skillReadCache.delete(cacheKey);
|
|
135
|
+
for (const entry of candidateSkillDirs(cwd)) {
|
|
136
|
+
try {
|
|
137
|
+
const relative = path.join(name, "SKILL.md");
|
|
138
|
+
const contained = resolveContainedPath(entry.root, relative);
|
|
139
|
+
if (!fs.existsSync(contained)) continue;
|
|
140
|
+
if (fs.lstatSync(contained).isSymbolicLink()) continue;
|
|
141
|
+
const filePath = resolveRealContainedPath(entry.root, relative);
|
|
142
|
+
const stat = fs.statSync(filePath);
|
|
143
|
+
return rememberSkill(cacheKey, { path: filePath, source: entry.source, content: fs.readFileSync(filePath, "utf-8"), mtimeMs: stat.mtimeMs, size: stat.size });
|
|
144
|
+
} catch {
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return undefined;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function frontmatterDescription(content: string): string | undefined {
|
|
152
|
+
const match = /^---\r?\n([\s\S]*?)\r?\n---/.exec(content);
|
|
153
|
+
if (!match) return undefined;
|
|
154
|
+
const line = match[1].split(/\r?\n/).find((entry) => entry.startsWith("description:"));
|
|
155
|
+
return line?.slice("description:".length).trim();
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function stripFrontmatter(content: string): string {
|
|
159
|
+
return content.replace(/^---\r?\n[\s\S]*?\r?\n---\r?\n/, "").trim();
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function compactSkillContent(content: string): string {
|
|
163
|
+
const body = stripFrontmatter(content);
|
|
164
|
+
if (body.length <= MAX_SKILL_CHARS) return body;
|
|
165
|
+
const preferred = body.split(/\r?\n## Verification\r?\n/)[0]?.trim() ?? body;
|
|
166
|
+
const truncated = preferred.length > MAX_SKILL_CHARS ? preferred.slice(0, MAX_SKILL_CHARS - 40).trimEnd() : preferred;
|
|
167
|
+
return `${truncated}\n\n[skill instructions truncated]`;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export interface RenderedSkillInstructions {
|
|
171
|
+
names: string[];
|
|
172
|
+
paths: string[];
|
|
173
|
+
block: string;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export function renderSkillInstructions(input: RenderSkillInstructionsInput): RenderedSkillInstructions {
|
|
177
|
+
const allNames = collectTaskSkillNames(input);
|
|
178
|
+
const names = allNames.slice(0, MAX_SELECTED_SKILLS);
|
|
179
|
+
const overflowCount = Math.max(0, allNames.length - names.length);
|
|
180
|
+
if (names.length === 0) return { names, paths: [], block: "" };
|
|
181
|
+
const sections: string[] = [];
|
|
182
|
+
const skillPaths: string[] = [];
|
|
183
|
+
let total = 0;
|
|
184
|
+
let omittedCount = overflowCount;
|
|
185
|
+
const pushSection = (section: string): boolean => {
|
|
186
|
+
if (total + section.length > MAX_TOTAL_CHARS) return false;
|
|
187
|
+
sections.push(section);
|
|
188
|
+
total += section.length;
|
|
189
|
+
return true;
|
|
190
|
+
};
|
|
191
|
+
for (const name of names) {
|
|
192
|
+
const safeName = sanitizeSkillName(name);
|
|
193
|
+
const loaded = readSkillMarkdown(input.cwd, name);
|
|
194
|
+
if (!loaded) {
|
|
195
|
+
const missing = `## ${safeName}\n\nSkill '${safeName}' was selected but no SKILL.md file was found. Continue with the task packet and report this missing skill.`;
|
|
196
|
+
if (!pushSection(missing)) omittedCount += 1;
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
skillPaths.push(path.dirname(loaded.path));
|
|
200
|
+
const description = frontmatterDescription(loaded.content);
|
|
201
|
+
const source = loaded.source === "project" ? `project:skills/${safeName}` : `package:skills/${safeName}`;
|
|
202
|
+
const header = [`## ${safeName}`, description ? `Description: ${description}` : undefined, `Source: ${source}`].filter(Boolean).join("\n");
|
|
203
|
+
const section = `${header}\n\n${compactSkillContent(loaded.content)}`;
|
|
204
|
+
if (!pushSection(section)) omittedCount += 1;
|
|
205
|
+
}
|
|
206
|
+
if (omittedCount > 0) {
|
|
207
|
+
const summary = `## Omitted skills\n\n[omitted ${omittedCount} selected skill(s): skill instruction budget exceeded]`;
|
|
208
|
+
if (!pushSection(summary) && sections.length > 0) {
|
|
209
|
+
sections[sections.length - 1] = summary;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
return {
|
|
213
|
+
names,
|
|
214
|
+
paths: [...new Set(skillPaths)],
|
|
215
|
+
block: [
|
|
216
|
+
"# Applicable Skills",
|
|
217
|
+
"The following skills were selected for this worker. Follow them when they match the current task. If a selected skill conflicts with the explicit task packet, project AGENTS.md, or user request, follow the stricter/higher-priority instruction and report the conflict.",
|
|
218
|
+
"",
|
|
219
|
+
sections.join("\n\n---\n\n"),
|
|
220
|
+
].join("\n"),
|
|
221
|
+
};
|
|
222
|
+
}
|
|
@@ -1,9 +1,5 @@
|
|
|
1
|
-
import * as fs from "node:fs";
|
|
2
|
-
import * as path from "node:path";
|
|
3
1
|
import type { TeamRunManifest, TeamTaskState } from "../state/types.ts";
|
|
4
2
|
import { checkProcessLiveness } from "./process-status.ts";
|
|
5
|
-
import { logInternalError } from "../utils/internal-error.ts";
|
|
6
|
-
import { writeAtomicJson } from "../utils/atomic-write.ts";
|
|
7
3
|
|
|
8
4
|
/**
|
|
9
5
|
* Result of reconciling a single stale run.
|
|
@@ -16,6 +12,8 @@ export interface ReconcileResult {
|
|
|
16
12
|
repaired: boolean;
|
|
17
13
|
/** Human-readable detail */
|
|
18
14
|
detail: string;
|
|
15
|
+
/** Repaired task state, returned to a locked caller for persistence. */
|
|
16
|
+
repairedTasks?: TeamTaskState[];
|
|
19
17
|
}
|
|
20
18
|
|
|
21
19
|
const STALE_ALIVE_PID_MS = 24 * 60 * 60 * 1000; // 24 hours
|
|
@@ -106,16 +104,6 @@ function repairStaleRun(
|
|
|
106
104
|
return task;
|
|
107
105
|
});
|
|
108
106
|
|
|
109
|
-
// Write repaired tasks atomically
|
|
110
|
-
const tasksPath = manifest.tasksPath;
|
|
111
|
-
if (tasksPath) {
|
|
112
|
-
try {
|
|
113
|
-
writeAtomicJson(tasksPath, repairedTasks);
|
|
114
|
-
} catch (error) {
|
|
115
|
-
logInternalError("stale-reconciler.repair-tasks", error, `runId=${manifest.runId}`);
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
|
|
119
107
|
return repairedTasks;
|
|
120
108
|
}
|
|
121
109
|
|
|
@@ -167,6 +155,7 @@ export function reconcileStaleRun(
|
|
|
167
155
|
verdict: "no_status",
|
|
168
156
|
repaired: true,
|
|
169
157
|
detail: `No PID; stale ${Math.round((now - updatedAt) / 3600_000)}h; repaired ${repaired.filter((t) => t.status === "cancelled").length} tasks`,
|
|
158
|
+
repairedTasks: repaired,
|
|
170
159
|
};
|
|
171
160
|
}
|
|
172
161
|
return {
|
|
@@ -195,5 +184,6 @@ export function reconcileStaleRun(
|
|
|
195
184
|
verdict: pidStatus.alive ? "pid_alive_stale" : "pid_dead",
|
|
196
185
|
repaired: true,
|
|
197
186
|
detail: `PID ${pid}: ${pidStatus.detail}; ${staleness.reason}; repaired ${repaired.filter((t) => t.status === "cancelled").length} tasks`,
|
|
187
|
+
repairedTasks: repaired,
|
|
198
188
|
};
|
|
199
189
|
}
|
|
@@ -17,6 +17,7 @@ export interface SubagentSpawnOptions {
|
|
|
17
17
|
prompt: string;
|
|
18
18
|
background: boolean;
|
|
19
19
|
model?: string;
|
|
20
|
+
skill?: string | string[] | false;
|
|
20
21
|
maxTurns?: number;
|
|
21
22
|
ownerSessionGeneration?: number;
|
|
22
23
|
}
|
|
@@ -34,6 +35,7 @@ export interface SubagentRecord {
|
|
|
34
35
|
error?: string;
|
|
35
36
|
resultConsumed?: boolean;
|
|
36
37
|
model?: string;
|
|
38
|
+
skill?: string | string[] | false;
|
|
37
39
|
background: boolean;
|
|
38
40
|
ownerSessionGeneration?: number;
|
|
39
41
|
stuckNotified?: boolean;
|
|
@@ -138,6 +140,7 @@ export class SubagentManager {
|
|
|
138
140
|
status: options.background && this.runningBackground >= this.maxConcurrent ? "queued" : "running",
|
|
139
141
|
startedAt: Date.now(),
|
|
140
142
|
model: options.model,
|
|
143
|
+
skill: options.skill,
|
|
141
144
|
background: options.background,
|
|
142
145
|
ownerSessionGeneration: options.ownerSessionGeneration,
|
|
143
146
|
};
|