pi-subagents 0.40.0 → 0.41.0
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 +76 -0
- package/README.md +246 -525
- package/agents/oracle.md +1 -0
- package/package.json +12 -4
- package/prompts/parallel-context-build.md +1 -1
- package/prompts/parallel-handoff-plan.md +1 -1
- package/prompts/review-loop.md +1 -1
- package/skills/pi-subagents/SKILL.md +6 -6
- package/skills/pi-subagents/references/constraints-and-recipes.md +19 -26
- package/skills/pi-subagents/references/execution-controls.md +98 -97
- package/skills/pi-subagents/references/management-authoring-rpc.md +2 -2
- package/skills/pi-subagents/references/prompting-and-roles.md +18 -27
- package/src/agents/agent-management.ts +155 -65
- package/src/agents/agent-serializer.ts +19 -0
- package/src/agents/agents.ts +154 -71
- package/src/agents/chain-serializer.ts +10 -7
- package/src/agents/frontmatter.ts +5 -3
- package/src/agents/identity.ts +1 -1
- package/src/agents/proactive-skills.ts +13 -10
- package/src/agents/skills.ts +23 -6
- package/src/api/control-channel.ts +4 -0
- package/src/api/delegation.ts +26 -194
- package/src/api/external-runs.ts +129 -0
- package/src/api/intercom-bridge.ts +3 -0
- package/src/api/pi-args.ts +5 -0
- package/src/api/preflight.ts +3 -3
- package/src/api/shared-types.ts +19 -0
- package/src/extension/config.ts +10 -0
- package/src/extension/control-notices.ts +5 -39
- package/src/extension/doctor.ts +10 -9
- package/src/extension/fanout-child.ts +7 -4
- package/src/extension/index.ts +232 -68
- package/src/extension/rpc.ts +18 -12
- package/src/extension/schemas.ts +48 -37
- package/src/extension/tool-description.ts +36 -86
- package/src/inspectors/herdr/actions.ts +229 -0
- package/src/inspectors/herdr/client.ts +130 -0
- package/src/inspectors/herdr/inspector-runner.ts +141 -0
- package/src/inspectors/herdr/project-panes.ts +154 -0
- package/src/integrations/herdr-status.ts +330 -0
- package/src/intercom/intercom-bridge.ts +3 -2
- package/src/intercom/result-intercom.ts +5 -1
- package/src/missions/actions.ts +372 -0
- package/src/missions/lifecycle.ts +314 -0
- package/src/missions/store.ts +442 -0
- package/src/missions/types.ts +135 -0
- package/src/policy/authority.ts +46 -0
- package/src/profiles/profiles.ts +29 -3
- package/src/runs/background/async-execution.ts +98 -49
- package/src/runs/background/async-job-tracker.ts +10 -2
- package/src/runs/background/async-resume.ts +6 -6
- package/src/runs/background/async-status.ts +29 -1
- package/src/runs/background/auto-drain.ts +3 -3
- package/src/runs/background/chain-append.ts +3 -2
- package/src/runs/background/control-channel.ts +9 -7
- package/src/runs/background/fleet-view.ts +3 -4
- package/src/runs/background/notify.ts +2 -1
- package/src/runs/background/process-terminal.ts +5 -5
- package/src/runs/background/result-watcher.ts +13 -5
- package/src/runs/background/run-id-resolver.ts +3 -3
- package/src/runs/background/run-status.ts +35 -8
- package/src/runs/background/scheduled-runs.ts +602 -375
- package/src/runs/background/stale-run-reconciler.ts +3 -3
- package/src/runs/background/subagent-runner.ts +608 -445
- package/src/runs/background/subagent-wait.ts +50 -9
- package/src/runs/background/wait-subscriptions.ts +253 -0
- package/src/runs/background/wait-tool.ts +12 -4
- package/src/runs/foreground/async-steering-action.ts +3 -3
- package/src/runs/foreground/chain-clarify.ts +8 -4
- package/src/runs/foreground/chain-execution.ts +56 -30
- package/src/runs/foreground/execution.ts +15 -2
- package/src/runs/foreground/subagent-executor.ts +1023 -273
- package/src/runs/shared/acceptance.ts +28 -6
- package/src/runs/shared/child-protocol.ts +302 -22
- package/src/runs/shared/dynamic-fanout.ts +1 -1
- package/src/runs/shared/external-cli-runner.ts +130 -0
- package/src/runs/shared/long-running-guard.ts +42 -1
- package/src/runs/shared/nested-events.ts +59 -5
- package/src/runs/shared/nested-render.ts +9 -4
- package/src/runs/shared/parallel-handoff.ts +86 -2
- package/src/runs/shared/parallel-utils.ts +11 -2
- package/src/runs/shared/permissions.ts +95 -0
- package/src/runs/shared/pi-args.ts +11 -1
- package/src/runs/shared/pi-spawn.ts +11 -1
- package/src/runs/shared/run-history.ts +1 -1
- package/src/runs/shared/subagent-prompt-runtime.ts +36 -5
- package/src/runs/shared/subagent-startup-retry.ts +5 -2
- package/src/runs/shared/turn-budget.ts +6 -6
- package/src/runs/shared/worktree.ts +122 -12
- package/src/shared/accessible-dir.ts +29 -7
- package/src/shared/artifacts.ts +18 -1
- package/src/shared/fork-context.ts +3 -2
- package/src/shared/launch-contract.ts +1 -0
- package/src/shared/settings.ts +10 -0
- package/src/shared/types.ts +156 -14
- package/src/shared/utils.ts +8 -6
- package/src/slash/delegation-adapters.ts +32 -194
- package/src/slash/delegation-request.ts +43 -126
- package/src/slash/prompt-template-bridge.ts +158 -205
- package/src/slash/prompt-workflows.ts +21 -57
- package/src/slash/slash-bridge.ts +14 -0
- package/src/slash/slash-commands.ts +31 -632
- package/src/slash/subagents-admin.ts +18 -14
- package/src/tui/fleet-status.ts +156 -21
- package/src/tui/fleet-transcript.ts +110 -5
- package/src/tui/fleet.ts +56 -24
- package/src/tui/render.ts +291 -109
- package/src/types/pi-runtime-compat.d.ts +14 -0
- package/src/watchdog/lsp-diagnostics.ts +12 -7
- package/src/watchdog/model-selection.ts +2 -2
- package/src/watchdog/permission-arbiter.ts +145 -0
- package/src/watchdog/register-child.ts +1 -1
- package/src/watchdog/register-main.ts +1 -1
- package/src/watchdog/review.ts +4 -1
- package/src/watchdog/runtime.ts +3 -2
- package/src/workflows/chat-progress.ts +140 -0
- package/src/workflows/scripted-workflow.ts +415 -0
- package/agents/advisor.md +0 -73
- package/src/extension/chain-validation.ts +0 -181
|
@@ -0,0 +1,442 @@
|
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
+
import * as fs from "node:fs";
|
|
3
|
+
import * as os from "node:os";
|
|
4
|
+
import * as path from "node:path";
|
|
5
|
+
import { writePrivateAtomicJson } from "../shared/atomic-json.ts";
|
|
6
|
+
import { getAgentDir } from "../shared/utils.ts";
|
|
7
|
+
import {
|
|
8
|
+
MISSION_STATUSES,
|
|
9
|
+
type GlobalMissionIndexRecord,
|
|
10
|
+
type GlobalMissionListResult,
|
|
11
|
+
type MissionArtifact,
|
|
12
|
+
type MissionArtifactKind,
|
|
13
|
+
type MissionCreateInput,
|
|
14
|
+
type MissionDecision,
|
|
15
|
+
type MissionIndexEntry,
|
|
16
|
+
type MissionListResult,
|
|
17
|
+
type MissionReceipt,
|
|
18
|
+
type MissionReceiptKind,
|
|
19
|
+
type MissionReceiptStatus,
|
|
20
|
+
type MissionRecord,
|
|
21
|
+
type MissionRunLink,
|
|
22
|
+
type MissionRunMode,
|
|
23
|
+
type MissionStatus,
|
|
24
|
+
type MissionStoreConfig,
|
|
25
|
+
type MissionStoreLocation,
|
|
26
|
+
type MissionUpdateInput,
|
|
27
|
+
} from "./types.ts";
|
|
28
|
+
|
|
29
|
+
const MISSION_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
|
|
30
|
+
const MISSION_RUN_MODES = new Set<MissionRunMode>(["single", "parallel", "chain", "workflow", "scheduled", "external"]);
|
|
31
|
+
const MISSION_ARTIFACT_KINDS = new Set<MissionArtifactKind>(["status", "output", "patch", "manifest", "review", "note", "other"]);
|
|
32
|
+
const MISSION_RECEIPT_KINDS = new Set<MissionReceiptKind>(["pull_request", "ci", "deployment", "release"]);
|
|
33
|
+
const MISSION_RECEIPT_STATUSES = new Set<MissionReceiptStatus>(["pending", "ready", "succeeded", "failed"]);
|
|
34
|
+
const MISSION_STATUS_SET = new Set<MissionStatus>(MISSION_STATUSES);
|
|
35
|
+
const TERMINAL_MISSION_STATUSES = new Set<MissionStatus>(["completed", "failed", "cancelled"]);
|
|
36
|
+
const DEFAULT_TERMINAL_MISSION_RETENTION = 200;
|
|
37
|
+
|
|
38
|
+
function asObject(value: unknown, label: string): Record<string, unknown> {
|
|
39
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error(`${label} must be a JSON object`);
|
|
40
|
+
return value as Record<string, unknown>;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function requiredString(value: unknown, label: string): string {
|
|
44
|
+
if (typeof value !== "string" || !value.trim()) throw new Error(`${label} must be a non-empty string`);
|
|
45
|
+
return value;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function optionalString(value: unknown, label: string): string | undefined {
|
|
49
|
+
if (value === undefined) return undefined;
|
|
50
|
+
return requiredString(value, label);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function timestamp(value: unknown, label: string): string {
|
|
54
|
+
const result = requiredString(value, label);
|
|
55
|
+
if (Number.isNaN(Date.parse(result))) throw new Error(`${label} must be an ISO timestamp`);
|
|
56
|
+
return result;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function missionStatus(value: unknown, label: string): MissionStatus {
|
|
60
|
+
if (typeof value !== "string" || !MISSION_STATUS_SET.has(value as MissionStatus)) {
|
|
61
|
+
throw new Error(`${label} must be one of ${MISSION_STATUSES.join(", ")}`);
|
|
62
|
+
}
|
|
63
|
+
return value as MissionStatus;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function stringArray(value: unknown, label: string): string[] {
|
|
67
|
+
if (!Array.isArray(value)) throw new Error(`${label} must be an array of non-empty strings`);
|
|
68
|
+
const result = value.map((item, index) => requiredString(item, `${label}[${index}]`).trim());
|
|
69
|
+
return [...new Set(result)];
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function validateMissionId(value: unknown, label = "missionId"): string {
|
|
73
|
+
const id = requiredString(value, label);
|
|
74
|
+
if (!MISSION_ID_PATTERN.test(id) || id.includes("..")) {
|
|
75
|
+
throw new Error(`${label} must contain only letters, numbers, '.', '_', or '-' and cannot contain '..'`);
|
|
76
|
+
}
|
|
77
|
+
return id;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function parseRunLink(value: unknown, label: string): MissionRunLink {
|
|
81
|
+
const input = asObject(value, label);
|
|
82
|
+
const runId = requiredString(input.runId, `${label}.runId`);
|
|
83
|
+
const mode = requiredString(input.mode, `${label}.mode`) as MissionRunMode;
|
|
84
|
+
if (!MISSION_RUN_MODES.has(mode)) throw new Error(`${label}.mode is invalid`);
|
|
85
|
+
if (input.childIndex !== undefined && (!Number.isInteger(input.childIndex) || (input.childIndex as number) < 0)) {
|
|
86
|
+
throw new Error(`${label}.childIndex must be a non-negative integer`);
|
|
87
|
+
}
|
|
88
|
+
return {
|
|
89
|
+
runId,
|
|
90
|
+
mode,
|
|
91
|
+
...(optionalString(input.asyncDir, `${label}.asyncDir`) ? { asyncDir: input.asyncDir as string } : {}),
|
|
92
|
+
...(input.childIndex !== undefined ? { childIndex: input.childIndex as number } : {}),
|
|
93
|
+
...(optionalString(input.agent, `${label}.agent`) ? { agent: input.agent as string } : {}),
|
|
94
|
+
...(optionalString(input.status, `${label}.status`) ? { status: input.status as string } : {}),
|
|
95
|
+
...(input.startedAt !== undefined ? { startedAt: timestamp(input.startedAt, `${label}.startedAt`) } : {}),
|
|
96
|
+
...(input.completedAt !== undefined ? { completedAt: timestamp(input.completedAt, `${label}.completedAt`) } : {}),
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function parseDecision(value: unknown, label: string): MissionDecision {
|
|
101
|
+
const input = asObject(value, label);
|
|
102
|
+
const status = input.status;
|
|
103
|
+
if (status !== "open" && status !== "resolved") throw new Error(`${label}.status must be "open" or "resolved"`);
|
|
104
|
+
return {
|
|
105
|
+
id: validateMissionId(input.id, `${label}.id`),
|
|
106
|
+
status,
|
|
107
|
+
title: requiredString(input.title, `${label}.title`),
|
|
108
|
+
createdAt: timestamp(input.createdAt, `${label}.createdAt`),
|
|
109
|
+
...(optionalString(input.prompt, `${label}.prompt`) ? { prompt: input.prompt as string } : {}),
|
|
110
|
+
...(input.options !== undefined ? { options: stringArray(input.options, `${label}.options`) } : {}),
|
|
111
|
+
...(optionalString(input.recommendation, `${label}.recommendation`) ? { recommendation: input.recommendation as string } : {}),
|
|
112
|
+
...(input.resolvedAt !== undefined ? { resolvedAt: timestamp(input.resolvedAt, `${label}.resolvedAt`) } : {}),
|
|
113
|
+
...(optionalString(input.resolution, `${label}.resolution`) ? { resolution: input.resolution as string } : {}),
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function parseArtifact(value: unknown, label: string): MissionArtifact {
|
|
118
|
+
const input = asObject(value, label);
|
|
119
|
+
const kind = requiredString(input.kind, `${label}.kind`) as MissionArtifactKind;
|
|
120
|
+
if (!MISSION_ARTIFACT_KINDS.has(kind)) throw new Error(`${label}.kind is invalid`);
|
|
121
|
+
return {
|
|
122
|
+
kind,
|
|
123
|
+
path: requiredString(input.path, `${label}.path`),
|
|
124
|
+
...(optionalString(input.description, `${label}.description`) ? { description: input.description as string } : {}),
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function parseReceipt(value: unknown, label: string): MissionReceipt {
|
|
129
|
+
const input = asObject(value, label);
|
|
130
|
+
const kind = requiredString(input.kind, `${label}.kind`) as MissionReceiptKind;
|
|
131
|
+
const status = requiredString(input.status, `${label}.status`) as MissionReceiptStatus;
|
|
132
|
+
if (!MISSION_RECEIPT_KINDS.has(kind)) throw new Error(`${label}.kind is invalid`);
|
|
133
|
+
if (!MISSION_RECEIPT_STATUSES.has(status)) throw new Error(`${label}.status is invalid`);
|
|
134
|
+
const url = requiredString(input.url, `${label}.url`);
|
|
135
|
+
try {
|
|
136
|
+
new URL(url);
|
|
137
|
+
} catch {
|
|
138
|
+
throw new Error(`${label}.url must be an absolute URL`);
|
|
139
|
+
}
|
|
140
|
+
return {
|
|
141
|
+
kind,
|
|
142
|
+
status,
|
|
143
|
+
title: requiredString(input.title, `${label}.title`),
|
|
144
|
+
url,
|
|
145
|
+
createdAt: timestamp(input.createdAt, `${label}.createdAt`),
|
|
146
|
+
...(optionalString(input.description, `${label}.description`) ? { description: input.description as string } : {}),
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export function parseMissionRecord(value: unknown, source = "mission record"): MissionRecord {
|
|
151
|
+
const input = asObject(value, source);
|
|
152
|
+
if (input.schemaVersion !== 1) throw new Error(`${source}.schemaVersion must be 1`);
|
|
153
|
+
if (!Array.isArray(input.runs)) throw new Error(`${source}.runs must be an array`);
|
|
154
|
+
if (!Array.isArray(input.decisions)) throw new Error(`${source}.decisions must be an array`);
|
|
155
|
+
if (!Array.isArray(input.artifacts)) throw new Error(`${source}.artifacts must be an array`);
|
|
156
|
+
if (input.receipts !== undefined && !Array.isArray(input.receipts)) throw new Error(`${source}.receipts must be an array`);
|
|
157
|
+
const runs = input.runs as unknown[];
|
|
158
|
+
const decisions = input.decisions as unknown[];
|
|
159
|
+
const artifacts = input.artifacts as unknown[];
|
|
160
|
+
const receipts = (input.receipts ?? []) as unknown[];
|
|
161
|
+
return {
|
|
162
|
+
schemaVersion: 1,
|
|
163
|
+
id: validateMissionId(input.id, `${source}.id`),
|
|
164
|
+
title: requiredString(input.title, `${source}.title`),
|
|
165
|
+
goal: requiredString(input.goal, `${source}.goal`),
|
|
166
|
+
status: missionStatus(input.status, `${source}.status`),
|
|
167
|
+
createdAt: timestamp(input.createdAt, `${source}.createdAt`),
|
|
168
|
+
updatedAt: timestamp(input.updatedAt, `${source}.updatedAt`),
|
|
169
|
+
runs: runs.map((item, index) => parseRunLink(item, `${source}.runs[${index}]`)),
|
|
170
|
+
decisions: decisions.map((item, index) => parseDecision(item, `${source}.decisions[${index}]`)),
|
|
171
|
+
artifacts: artifacts.map((item, index) => parseArtifact(item, `${source}.artifacts[${index}]`)),
|
|
172
|
+
receipts: receipts.map((item, index) => parseReceipt(item, `${source}.receipts[${index}]`)),
|
|
173
|
+
...(optionalString(input.cwd, `${source}.cwd`) ? { cwd: input.cwd as string } : {}),
|
|
174
|
+
...(optionalString(input.ownerSessionId, `${source}.ownerSessionId`) ? { ownerSessionId: input.ownerSessionId as string } : {}),
|
|
175
|
+
...(optionalString(input.summary, `${source}.summary`) ? { summary: input.summary as string } : {}),
|
|
176
|
+
...(input.acceptance !== undefined ? { acceptance: input.acceptance } : {}),
|
|
177
|
+
...(input.labels !== undefined ? { labels: stringArray(input.labels, `${source}.labels`) } : {}),
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function expandConfiguredPath(value: string, projectRoot: string): string {
|
|
182
|
+
const expanded = value.startsWith("~/") ? path.join(os.homedir(), value.slice(2)) : value;
|
|
183
|
+
return path.isAbsolute(expanded) ? path.normalize(expanded) : path.resolve(projectRoot, expanded);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export function validateMissionStoreConfig(value: unknown, label = "config.missions"): MissionStoreConfig | undefined {
|
|
187
|
+
if (value === undefined) return undefined;
|
|
188
|
+
const input = asObject(value, label);
|
|
189
|
+
for (const key of Object.keys(input)) {
|
|
190
|
+
if (key !== "enabled" && key !== "directory" && key !== "globalIndex" && key !== "globalIndexDir" && key !== "retainTerminal") {
|
|
191
|
+
throw new Error(`${label}.${key} is unknown`);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
if (input.enabled !== undefined && typeof input.enabled !== "boolean") throw new Error(`${label}.enabled must be boolean`);
|
|
195
|
+
if (input.globalIndex !== undefined && typeof input.globalIndex !== "boolean") throw new Error(`${label}.globalIndex must be boolean`);
|
|
196
|
+
if (input.retainTerminal !== undefined && (!Number.isInteger(input.retainTerminal) || (input.retainTerminal as number) < 1)) {
|
|
197
|
+
throw new Error(`${label}.retainTerminal must be a positive integer`);
|
|
198
|
+
}
|
|
199
|
+
const directory = optionalString(input.directory, `${label}.directory`);
|
|
200
|
+
const globalIndexDir = optionalString(input.globalIndexDir, `${label}.globalIndexDir`);
|
|
201
|
+
return {
|
|
202
|
+
...(typeof input.enabled === "boolean" ? { enabled: input.enabled } : {}),
|
|
203
|
+
...(directory ? { directory } : {}),
|
|
204
|
+
...(typeof input.globalIndex === "boolean" ? { globalIndex: input.globalIndex } : {}),
|
|
205
|
+
...(globalIndexDir ? { globalIndexDir } : {}),
|
|
206
|
+
...(input.retainTerminal !== undefined ? { retainTerminal: input.retainTerminal as number } : {}),
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
export function resolveMissionStoreLocation(input: {
|
|
211
|
+
projectRoot: string;
|
|
212
|
+
config?: MissionStoreConfig;
|
|
213
|
+
agentDir?: string;
|
|
214
|
+
}): MissionStoreLocation {
|
|
215
|
+
const projectRoot = path.resolve(input.projectRoot);
|
|
216
|
+
const missionDir = input.config?.directory
|
|
217
|
+
? expandConfiguredPath(input.config.directory, projectRoot)
|
|
218
|
+
: path.join(projectRoot, ".pi-subagents", "missions");
|
|
219
|
+
const globalIndexDir = input.config?.globalIndexDir
|
|
220
|
+
? expandConfiguredPath(input.config.globalIndexDir, projectRoot)
|
|
221
|
+
: path.join(input.agentDir ?? getAgentDir(), "missions", "index");
|
|
222
|
+
return {
|
|
223
|
+
projectRoot,
|
|
224
|
+
missionDir,
|
|
225
|
+
globalIndexDir,
|
|
226
|
+
writeGlobalIndex: input.config?.globalIndex !== false,
|
|
227
|
+
...(input.config?.retainTerminal !== undefined ? { retainTerminal: input.config.retainTerminal } : {}),
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
export function missionRecordPath(location: MissionStoreLocation, missionId: string): string {
|
|
232
|
+
return path.join(location.missionDir, `${validateMissionId(missionId)}.json`);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function parseIndexEntry(value: unknown, source: string): MissionIndexEntry {
|
|
236
|
+
const input = asObject(value, source);
|
|
237
|
+
if (input.schemaVersion !== 1) throw new Error(`${source}.schemaVersion must be 1`);
|
|
238
|
+
return {
|
|
239
|
+
schemaVersion: 1,
|
|
240
|
+
missionId: validateMissionId(input.missionId, `${source}.missionId`),
|
|
241
|
+
projectRoot: requiredString(input.projectRoot, `${source}.projectRoot`),
|
|
242
|
+
recordPath: requiredString(input.recordPath, `${source}.recordPath`),
|
|
243
|
+
title: requiredString(input.title, `${source}.title`),
|
|
244
|
+
status: missionStatus(input.status, `${source}.status`),
|
|
245
|
+
updatedAt: timestamp(input.updatedAt, `${source}.updatedAt`),
|
|
246
|
+
...(optionalString(input.lastRunId, `${source}.lastRunId`) ? { lastRunId: input.lastRunId as string } : {}),
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function indexPath(location: MissionStoreLocation, record: MissionRecord): string {
|
|
251
|
+
const key = createHash("sha256").update(`${location.projectRoot}\0${record.id}`).digest("hex");
|
|
252
|
+
return path.join(location.globalIndexDir, `${key}.json`);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function writeMission(location: MissionStoreLocation, record: MissionRecord): MissionRecord {
|
|
256
|
+
const validated = parseMissionRecord(record);
|
|
257
|
+
writePrivateAtomicJson(missionRecordPath(location, validated.id), validated);
|
|
258
|
+
if (location.writeGlobalIndex) {
|
|
259
|
+
const lastRunId = validated.runs.at(-1)?.runId;
|
|
260
|
+
const entry: MissionIndexEntry = {
|
|
261
|
+
schemaVersion: 1,
|
|
262
|
+
missionId: validated.id,
|
|
263
|
+
projectRoot: location.projectRoot,
|
|
264
|
+
recordPath: missionRecordPath(location, validated.id),
|
|
265
|
+
title: validated.title,
|
|
266
|
+
status: validated.status,
|
|
267
|
+
updatedAt: validated.updatedAt,
|
|
268
|
+
...(lastRunId ? { lastRunId } : {}),
|
|
269
|
+
};
|
|
270
|
+
writePrivateAtomicJson(indexPath(location, validated), entry);
|
|
271
|
+
}
|
|
272
|
+
return validated;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function pruneTerminalMissions(location: MissionStoreLocation, maxTerminal: number): void {
|
|
276
|
+
const terminal = listMissions(location).records
|
|
277
|
+
.filter((record) => TERMINAL_MISSION_STATUSES.has(record.status))
|
|
278
|
+
.sort((left, right) => right.updatedAt.localeCompare(left.updatedAt));
|
|
279
|
+
for (const record of terminal.slice(maxTerminal)) {
|
|
280
|
+
try {
|
|
281
|
+
fs.rmSync(missionRecordPath(location, record.id), { force: true });
|
|
282
|
+
if (location.writeGlobalIndex) fs.rmSync(indexPath(location, record), { force: true });
|
|
283
|
+
} catch {
|
|
284
|
+
// Retention is best-effort and must never block a launch.
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
export function createMission(location: MissionStoreLocation, input: MissionCreateInput, now = new Date(), retainTerminal = location.retainTerminal ?? DEFAULT_TERMINAL_MISSION_RETENTION): MissionRecord {
|
|
290
|
+
const createdAt = now.toISOString();
|
|
291
|
+
const record: MissionRecord = {
|
|
292
|
+
schemaVersion: 1,
|
|
293
|
+
id: randomUUID(),
|
|
294
|
+
title: requiredString(input.title, "mission.title").trim(),
|
|
295
|
+
goal: requiredString(input.goal, "mission.goal").trim(),
|
|
296
|
+
status: input.status ?? "planned",
|
|
297
|
+
createdAt,
|
|
298
|
+
updatedAt: createdAt,
|
|
299
|
+
cwd: location.projectRoot,
|
|
300
|
+
runs: [],
|
|
301
|
+
decisions: [],
|
|
302
|
+
artifacts: [],
|
|
303
|
+
receipts: [],
|
|
304
|
+
...(input.ownerSessionId ? { ownerSessionId: requiredString(input.ownerSessionId, "mission.ownerSessionId") } : {}),
|
|
305
|
+
...(input.labels ? { labels: stringArray(input.labels, "mission.labels") } : {}),
|
|
306
|
+
};
|
|
307
|
+
const created = writeMission(location, record);
|
|
308
|
+
pruneTerminalMissions(location, retainTerminal);
|
|
309
|
+
return created;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
export class MissionNotFoundError extends Error {
|
|
313
|
+
readonly code = "MISSION_NOT_FOUND";
|
|
314
|
+
readonly missionId: string;
|
|
315
|
+
readonly missionDir: string;
|
|
316
|
+
|
|
317
|
+
constructor(missionId: string, missionDir: string) {
|
|
318
|
+
super(`Mission '${missionId}' was not found in ${missionDir}`);
|
|
319
|
+
this.name = "MissionNotFoundError";
|
|
320
|
+
this.missionId = missionId;
|
|
321
|
+
this.missionDir = missionDir;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
export function readMission(location: MissionStoreLocation, missionId: string): MissionRecord {
|
|
326
|
+
const filePath = missionRecordPath(location, missionId);
|
|
327
|
+
let raw: string;
|
|
328
|
+
try {
|
|
329
|
+
raw = fs.readFileSync(filePath, "utf-8");
|
|
330
|
+
} catch (error) {
|
|
331
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") throw new MissionNotFoundError(missionId, location.missionDir);
|
|
332
|
+
throw error;
|
|
333
|
+
}
|
|
334
|
+
try {
|
|
335
|
+
return parseMissionRecord(JSON.parse(raw), filePath);
|
|
336
|
+
} catch (error) {
|
|
337
|
+
throw new Error(`Invalid mission file '${filePath}': ${error instanceof Error ? error.message : String(error)}`);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
export function listMissions(location: MissionStoreLocation): MissionListResult {
|
|
342
|
+
if (!fs.existsSync(location.missionDir)) return { records: [], warnings: [] };
|
|
343
|
+
const records: MissionRecord[] = [];
|
|
344
|
+
const warnings: string[] = [];
|
|
345
|
+
for (const name of fs.readdirSync(location.missionDir).filter((item) => item.endsWith(".json")).sort()) {
|
|
346
|
+
const filePath = path.join(location.missionDir, name);
|
|
347
|
+
try {
|
|
348
|
+
records.push(parseMissionRecord(JSON.parse(fs.readFileSync(filePath, "utf-8")), filePath));
|
|
349
|
+
} catch (error) {
|
|
350
|
+
warnings.push(`Skipped corrupt mission '${filePath}': ${error instanceof Error ? error.message : String(error)}`);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
records.sort((left, right) => right.updatedAt.localeCompare(left.updatedAt));
|
|
354
|
+
return { records, warnings };
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
export function updateMission(location: MissionStoreLocation, missionId: string, update: MissionUpdateInput, now = new Date(), retainTerminal = location.retainTerminal ?? DEFAULT_TERMINAL_MISSION_RETENTION): MissionRecord {
|
|
358
|
+
const current = readMission(location, missionId);
|
|
359
|
+
const runs = [...current.runs];
|
|
360
|
+
for (const candidate of update.addRuns ?? []) {
|
|
361
|
+
const run = parseRunLink(candidate, "mission.update.addRuns[]");
|
|
362
|
+
const existingIndex = runs.findIndex((item) => item.runId === run.runId && item.childIndex === run.childIndex);
|
|
363
|
+
if (existingIndex === -1) runs.push(run);
|
|
364
|
+
else runs[existingIndex] = { ...runs[existingIndex]!, ...run };
|
|
365
|
+
}
|
|
366
|
+
const artifacts = [...current.artifacts];
|
|
367
|
+
for (const candidate of update.addArtifacts ?? []) {
|
|
368
|
+
const artifact = parseArtifact(candidate, "mission.update.addArtifacts[]");
|
|
369
|
+
const existingIndex = artifacts.findIndex((item) => item.kind === artifact.kind && path.resolve(item.path) === path.resolve(artifact.path));
|
|
370
|
+
if (existingIndex === -1) artifacts.push(artifact);
|
|
371
|
+
else artifacts[existingIndex] = { ...artifacts[existingIndex]!, ...artifact };
|
|
372
|
+
}
|
|
373
|
+
const createdAt = now.toISOString();
|
|
374
|
+
const receipts = [...current.receipts];
|
|
375
|
+
for (const candidate of update.addReceipts ?? []) {
|
|
376
|
+
const receipt = parseReceipt({ ...candidate, createdAt }, "mission.update.addReceipts[]");
|
|
377
|
+
const existingIndex = receipts.findIndex((item) => item.kind === receipt.kind && item.url === receipt.url);
|
|
378
|
+
if (existingIndex === -1) receipts.push(receipt);
|
|
379
|
+
else receipts[existingIndex] = { ...receipt, createdAt: receipts[existingIndex]!.createdAt };
|
|
380
|
+
}
|
|
381
|
+
const decisions = [
|
|
382
|
+
...current.decisions,
|
|
383
|
+
...(update.addDecisions ?? []).map((decision): MissionDecision => ({
|
|
384
|
+
id: randomUUID(),
|
|
385
|
+
status: "open",
|
|
386
|
+
title: requiredString(decision.title, "mission.update.addDecisions[].title"),
|
|
387
|
+
createdAt,
|
|
388
|
+
...(decision.prompt ? { prompt: requiredString(decision.prompt, "mission.update.addDecisions[].prompt") } : {}),
|
|
389
|
+
...(decision.options ? { options: stringArray(decision.options, "mission.update.addDecisions[].options") } : {}),
|
|
390
|
+
...(decision.recommendation ? { recommendation: requiredString(decision.recommendation, "mission.update.addDecisions[].recommendation") } : {}),
|
|
391
|
+
})),
|
|
392
|
+
];
|
|
393
|
+
const next: MissionRecord = {
|
|
394
|
+
...current,
|
|
395
|
+
updatedAt: createdAt,
|
|
396
|
+
runs,
|
|
397
|
+
artifacts,
|
|
398
|
+
receipts,
|
|
399
|
+
decisions,
|
|
400
|
+
...(update.title !== undefined ? { title: requiredString(update.title, "mission.update.title").trim() } : {}),
|
|
401
|
+
...(update.goal !== undefined ? { goal: requiredString(update.goal, "mission.update.goal").trim() } : {}),
|
|
402
|
+
...(update.status !== undefined ? { status: missionStatus(update.status, "mission.update.status") } : {}),
|
|
403
|
+
...(update.summary !== undefined ? { summary: requiredString(update.summary, "mission.update.summary") } : {}),
|
|
404
|
+
...(update.labels !== undefined ? { labels: stringArray(update.labels, "mission.update.labels") } : {}),
|
|
405
|
+
...(update.acceptance !== undefined ? { acceptance: update.acceptance } : {}),
|
|
406
|
+
};
|
|
407
|
+
const updated = writeMission(location, next);
|
|
408
|
+
if (TERMINAL_MISSION_STATUSES.has(updated.status)) pruneTerminalMissions(location, retainTerminal);
|
|
409
|
+
return updated;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
export function listGlobalMissions(globalIndexDir: string): GlobalMissionListResult {
|
|
413
|
+
if (!fs.existsSync(globalIndexDir)) return { entries: [], warnings: [] };
|
|
414
|
+
const entries: GlobalMissionIndexRecord[] = [];
|
|
415
|
+
const warnings: string[] = [];
|
|
416
|
+
for (const name of fs.readdirSync(globalIndexDir).filter((item) => item.endsWith(".json")).sort()) {
|
|
417
|
+
const filePath = path.join(globalIndexDir, name);
|
|
418
|
+
try {
|
|
419
|
+
const entry = parseIndexEntry(JSON.parse(fs.readFileSync(filePath, "utf-8")), filePath);
|
|
420
|
+
try {
|
|
421
|
+
const record = parseMissionRecord(JSON.parse(fs.readFileSync(entry.recordPath, "utf-8")), entry.recordPath);
|
|
422
|
+
if (record.id !== entry.missionId) throw new Error(`record id '${record.id}' does not match index id '${entry.missionId}'`);
|
|
423
|
+
entries.push({ ...entry, stale: false });
|
|
424
|
+
} catch (error) {
|
|
425
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") {
|
|
426
|
+
try {
|
|
427
|
+
fs.rmSync(filePath, { force: true });
|
|
428
|
+
warnings.push(`Removed stale global mission pointer '${filePath}' because '${entry.recordPath}' no longer exists.`);
|
|
429
|
+
} catch (removeError) {
|
|
430
|
+
warnings.push(`Failed to remove stale global mission pointer '${filePath}': ${removeError instanceof Error ? removeError.message : String(removeError)}`);
|
|
431
|
+
}
|
|
432
|
+
continue;
|
|
433
|
+
}
|
|
434
|
+
entries.push({ ...entry, stale: true, staleReason: error instanceof Error ? error.message : String(error) });
|
|
435
|
+
}
|
|
436
|
+
} catch (error) {
|
|
437
|
+
warnings.push(`Skipped corrupt global mission index entry '${filePath}': ${error instanceof Error ? error.message : String(error)}`);
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
entries.sort((left, right) => right.updatedAt.localeCompare(left.updatedAt));
|
|
441
|
+
return { entries, warnings };
|
|
442
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
export const MISSION_STATUSES = [
|
|
2
|
+
"planned",
|
|
3
|
+
"active",
|
|
4
|
+
"waiting",
|
|
5
|
+
"needs_decision",
|
|
6
|
+
"completed",
|
|
7
|
+
"failed",
|
|
8
|
+
"cancelled",
|
|
9
|
+
] as const;
|
|
10
|
+
|
|
11
|
+
export type MissionStatus = typeof MISSION_STATUSES[number];
|
|
12
|
+
export type MissionRunMode = "single" | "parallel" | "chain" | "workflow" | "scheduled" | "external";
|
|
13
|
+
export type MissionArtifactKind = "status" | "output" | "patch" | "manifest" | "review" | "note" | "other";
|
|
14
|
+
export type MissionReceiptKind = "pull_request" | "ci" | "deployment" | "release";
|
|
15
|
+
export type MissionReceiptStatus = "pending" | "ready" | "succeeded" | "failed";
|
|
16
|
+
|
|
17
|
+
export interface MissionRunLink {
|
|
18
|
+
runId: string;
|
|
19
|
+
asyncDir?: string;
|
|
20
|
+
childIndex?: number;
|
|
21
|
+
agent?: string;
|
|
22
|
+
mode: MissionRunMode;
|
|
23
|
+
status?: string;
|
|
24
|
+
startedAt?: string;
|
|
25
|
+
completedAt?: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface MissionDecision {
|
|
29
|
+
id: string;
|
|
30
|
+
status: "open" | "resolved";
|
|
31
|
+
title: string;
|
|
32
|
+
prompt?: string;
|
|
33
|
+
options?: string[];
|
|
34
|
+
recommendation?: string;
|
|
35
|
+
createdAt: string;
|
|
36
|
+
resolvedAt?: string;
|
|
37
|
+
resolution?: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface MissionArtifact {
|
|
41
|
+
kind: MissionArtifactKind;
|
|
42
|
+
path: string;
|
|
43
|
+
description?: string;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface MissionReceipt {
|
|
47
|
+
kind: MissionReceiptKind;
|
|
48
|
+
status: MissionReceiptStatus;
|
|
49
|
+
title: string;
|
|
50
|
+
url: string;
|
|
51
|
+
createdAt: string;
|
|
52
|
+
description?: string;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface MissionRecord {
|
|
56
|
+
schemaVersion: 1;
|
|
57
|
+
id: string;
|
|
58
|
+
title: string;
|
|
59
|
+
goal: string;
|
|
60
|
+
status: MissionStatus;
|
|
61
|
+
createdAt: string;
|
|
62
|
+
updatedAt: string;
|
|
63
|
+
cwd?: string;
|
|
64
|
+
ownerSessionId?: string;
|
|
65
|
+
runs: MissionRunLink[];
|
|
66
|
+
decisions: MissionDecision[];
|
|
67
|
+
artifacts: MissionArtifact[];
|
|
68
|
+
receipts: MissionReceipt[];
|
|
69
|
+
summary?: string;
|
|
70
|
+
acceptance?: unknown;
|
|
71
|
+
labels?: string[];
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export interface MissionIndexEntry {
|
|
75
|
+
schemaVersion: 1;
|
|
76
|
+
missionId: string;
|
|
77
|
+
projectRoot: string;
|
|
78
|
+
recordPath: string;
|
|
79
|
+
title: string;
|
|
80
|
+
status: MissionStatus;
|
|
81
|
+
updatedAt: string;
|
|
82
|
+
lastRunId?: string;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export interface MissionStoreConfig {
|
|
86
|
+
enabled?: boolean;
|
|
87
|
+
directory?: string;
|
|
88
|
+
globalIndex?: boolean;
|
|
89
|
+
globalIndexDir?: string;
|
|
90
|
+
retainTerminal?: number;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export interface MissionStoreLocation {
|
|
94
|
+
projectRoot: string;
|
|
95
|
+
missionDir: string;
|
|
96
|
+
globalIndexDir: string;
|
|
97
|
+
writeGlobalIndex: boolean;
|
|
98
|
+
retainTerminal?: number;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export interface MissionListResult {
|
|
102
|
+
records: MissionRecord[];
|
|
103
|
+
warnings: string[];
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export interface GlobalMissionIndexRecord extends MissionIndexEntry {
|
|
107
|
+
stale: boolean;
|
|
108
|
+
staleReason?: string;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export interface GlobalMissionListResult {
|
|
112
|
+
entries: GlobalMissionIndexRecord[];
|
|
113
|
+
warnings: string[];
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export interface MissionCreateInput {
|
|
117
|
+
title: string;
|
|
118
|
+
goal: string;
|
|
119
|
+
status?: MissionStatus;
|
|
120
|
+
labels?: string[];
|
|
121
|
+
ownerSessionId?: string;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export interface MissionUpdateInput {
|
|
125
|
+
title?: string;
|
|
126
|
+
goal?: string;
|
|
127
|
+
status?: MissionStatus;
|
|
128
|
+
summary?: string;
|
|
129
|
+
labels?: string[];
|
|
130
|
+
acceptance?: unknown;
|
|
131
|
+
addRuns?: MissionRunLink[];
|
|
132
|
+
addArtifacts?: MissionArtifact[];
|
|
133
|
+
addDecisions?: Array<Omit<MissionDecision, "id" | "status" | "createdAt">>;
|
|
134
|
+
addReceipts?: Array<Omit<MissionReceipt, "createdAt">>;
|
|
135
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
export const AUTHORITY_ACTIONS = [
|
|
2
|
+
"discardWorktree",
|
|
3
|
+
"destructiveCleanup",
|
|
4
|
+
"spawnBudgetGrant",
|
|
5
|
+
"scheduleCreate",
|
|
6
|
+
"stopRun",
|
|
7
|
+
"steerRun",
|
|
8
|
+
] as const;
|
|
9
|
+
|
|
10
|
+
export type AuthorityAction = typeof AUTHORITY_ACTIONS[number];
|
|
11
|
+
export type AuthorityDecision = "auto" | "confirm" | "forbid";
|
|
12
|
+
export type AuthorityPolicyConfig = Partial<Record<AuthorityAction, AuthorityDecision>>;
|
|
13
|
+
|
|
14
|
+
const DEFAULT_AUTHORITY_POLICY: Record<AuthorityAction, AuthorityDecision> = {
|
|
15
|
+
discardWorktree: "confirm",
|
|
16
|
+
destructiveCleanup: "confirm",
|
|
17
|
+
spawnBudgetGrant: "confirm",
|
|
18
|
+
scheduleCreate: "auto",
|
|
19
|
+
stopRun: "auto",
|
|
20
|
+
steerRun: "auto",
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export function resolveAuthorityDecision(input: {
|
|
24
|
+
action: AuthorityAction;
|
|
25
|
+
policy?: AuthorityPolicyConfig;
|
|
26
|
+
}): AuthorityDecision {
|
|
27
|
+
return input.policy?.[input.action] ?? DEFAULT_AUTHORITY_POLICY[input.action];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function validateAuthorityPolicy(value: unknown, label = "config.authorityPolicy"): AuthorityPolicyConfig | undefined {
|
|
31
|
+
if (value === undefined) return undefined;
|
|
32
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
33
|
+
throw new Error(`${label} must be a JSON object`);
|
|
34
|
+
}
|
|
35
|
+
const policy = value as Record<string, unknown>;
|
|
36
|
+
const allowedActions = new Set<string>(AUTHORITY_ACTIONS);
|
|
37
|
+
for (const [action, decision] of Object.entries(policy)) {
|
|
38
|
+
if (!allowedActions.has(action)) {
|
|
39
|
+
throw new Error(`${label}.${action} is unknown; expected one of ${AUTHORITY_ACTIONS.join(", ")}`);
|
|
40
|
+
}
|
|
41
|
+
if (decision !== "auto" && decision !== "confirm" && decision !== "forbid") {
|
|
42
|
+
throw new Error(`${label}.${action} must be "auto", "confirm", or "forbid"`);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return policy as AuthorityPolicyConfig;
|
|
46
|
+
}
|
package/src/profiles/profiles.ts
CHANGED
|
@@ -18,11 +18,15 @@ export type RecommendedRoleTier = "cheap" | "medium" | "strong";
|
|
|
18
18
|
|
|
19
19
|
interface ProfileAgentOverride {
|
|
20
20
|
model?: string;
|
|
21
|
+
thinking?: string | false;
|
|
22
|
+
fallbackModels?: string[] | false;
|
|
21
23
|
}
|
|
22
24
|
|
|
23
25
|
export interface SubagentProfileFile {
|
|
24
26
|
subagents: {
|
|
25
27
|
agentOverrides: Record<string, ProfileAgentOverride>;
|
|
28
|
+
disableBuiltins?: boolean;
|
|
29
|
+
[key: string]: unknown;
|
|
26
30
|
};
|
|
27
31
|
}
|
|
28
32
|
|
|
@@ -130,10 +134,23 @@ function validateSubagentProfile(filePath: string, parsed: Record<string, unknow
|
|
|
130
134
|
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
131
135
|
throw new Error(`Profile '${filePath}' has invalid override '${name}'; expected an object.`);
|
|
132
136
|
}
|
|
133
|
-
const
|
|
137
|
+
const override = value as Record<string, unknown>;
|
|
138
|
+
const model = override.model;
|
|
134
139
|
if (model !== undefined && typeof model !== "string") {
|
|
135
140
|
throw new Error(`Profile '${filePath}' has invalid model for '${name}'; expected a string.`);
|
|
136
141
|
}
|
|
142
|
+
const thinking = override.thinking;
|
|
143
|
+
if (thinking !== undefined && thinking !== false && typeof thinking !== "string") {
|
|
144
|
+
throw new Error(`Profile '${filePath}' has invalid thinking for '${name}'; expected a string or false.`);
|
|
145
|
+
}
|
|
146
|
+
const fallbackModels = override.fallbackModels;
|
|
147
|
+
if (fallbackModels !== undefined && fallbackModels !== false && (!Array.isArray(fallbackModels) || fallbackModels.some((item) => typeof item !== "string"))) {
|
|
148
|
+
throw new Error(`Profile '${filePath}' has invalid fallbackModels for '${name}'; expected an array of strings or false.`);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
const disableBuiltins = (subagents as Record<string, unknown>).disableBuiltins;
|
|
152
|
+
if (disableBuiltins !== undefined && typeof disableBuiltins !== "boolean") {
|
|
153
|
+
throw new Error(`Profile '${filePath}' has invalid subagents.disableBuiltins; expected a boolean.`);
|
|
137
154
|
}
|
|
138
155
|
return parsed as unknown as SubagentProfileFile;
|
|
139
156
|
}
|
|
@@ -468,7 +485,16 @@ export function applySubagentProfile(name: string): { filePath: string; settings
|
|
|
468
485
|
const { filePath, profile } = readSubagentProfile(name);
|
|
469
486
|
const settingsPath = getUserSettingsPath();
|
|
470
487
|
const settings = readSettingsFile(settingsPath);
|
|
471
|
-
settings.subagents
|
|
488
|
+
const existing = settings.subagents && typeof settings.subagents === "object" && !Array.isArray(settings.subagents)
|
|
489
|
+
? settings.subagents as Record<string, unknown>
|
|
490
|
+
: {};
|
|
491
|
+
// A profile owns the complete agent mapping, but unrelated subagent settings
|
|
492
|
+
// (notably disableBuiltins, modelScope, watchdog, etc.) survive profile switches.
|
|
493
|
+
settings.subagents = {
|
|
494
|
+
...existing,
|
|
495
|
+
...profile.subagents,
|
|
496
|
+
agentOverrides: profile.subagents.agentOverrides,
|
|
497
|
+
};
|
|
472
498
|
writeJsonFile(settingsPath, settings);
|
|
473
499
|
return { filePath, settingsPath };
|
|
474
500
|
}
|
|
@@ -514,7 +540,7 @@ export async function refreshProviderModelCatalog(
|
|
|
514
540
|
probe: { status: ProbeStatus; message?: string };
|
|
515
541
|
}>;
|
|
516
542
|
for (const rawModel of availableModels) {
|
|
517
|
-
const modelRecord = rawModel as Record<string, unknown> & { provider: string; id: string; name?: string };
|
|
543
|
+
const modelRecord = rawModel as unknown as Record<string, unknown> & { provider: string; id: string; name?: string };
|
|
518
544
|
const fullId = `${modelRecord.provider}/${modelRecord.id}`;
|
|
519
545
|
const probe = options.probe === false
|
|
520
546
|
? { status: "skipped" as const, message: "Live probing disabled." }
|