pi-crew 0.1.41 → 0.1.43
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 +47 -0
- package/README.md +51 -0
- 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 +11 -11
- package/agents/writer.md +11 -11
- 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-extension-examples.md +297 -297
- package/docs/research-extension-system.md +324 -324
- package/docs/research-optimization-plan.md +548 -548
- 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 +83 -83
- package/index.ts +6 -6
- package/package.json +1 -1
- package/src/agents/agent-serializer.ts +34 -34
- package/src/agents/discover-agents.ts +4 -4
- package/src/config/config.ts +9 -4
- package/src/extension/cross-extension-rpc.ts +82 -82
- package/src/extension/management.ts +37 -8
- package/src/extension/notification-router.ts +2 -2
- package/src/extension/register.ts +3 -0
- package/src/extension/registration/commands.ts +11 -9
- package/src/extension/registration/compaction-guard.ts +125 -125
- package/src/extension/registration/subagent-tools.ts +28 -19
- package/src/extension/result-watcher.ts +4 -4
- package/src/extension/run-bundle-schema.ts +8 -4
- package/src/extension/run-import.ts +4 -0
- package/src/extension/run-index.ts +4 -1
- package/src/extension/run-maintenance.ts +43 -24
- package/src/extension/team-tool/api.ts +1 -1
- package/src/extension/team-tool/cancel.ts +31 -31
- package/src/extension/team-tool/doctor.ts +8 -1
- package/src/extension/team-tool/handle-settings.ts +188 -0
- package/src/extension/team-tool/inspect.ts +41 -41
- package/src/extension/team-tool/lifecycle-actions.ts +79 -79
- package/src/extension/team-tool/plan.ts +19 -19
- package/src/extension/team-tool/run.ts +4 -2
- package/src/extension/team-tool/status.ts +93 -93
- package/src/extension/team-tool.ts +3 -1
- package/src/i18n.ts +184 -0
- package/src/observability/correlation.ts +2 -2
- package/src/observability/event-to-metric.ts +4 -3
- package/src/observability/exporters/adapter.ts +7 -1
- package/src/observability/exporters/otlp-exporter.ts +14 -2
- package/src/observability/exporters/prometheus-exporter.ts +9 -2
- package/src/observability/metric-registry.ts +18 -3
- package/src/observability/metric-retention.ts +11 -3
- package/src/observability/metric-sink.ts +9 -4
- package/src/observability/metrics-primitives.ts +4 -3
- package/src/prompt/prompt-runtime.ts +72 -68
- 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 -23
- package/src/runtime/background-runner.ts +53 -53
- package/src/runtime/child-pi.ts +4 -4
- package/src/runtime/completion-guard.ts +103 -99
- package/src/runtime/concurrency.ts +1 -1
- package/src/runtime/crash-recovery.ts +2 -1
- package/src/runtime/crew-agent-runtime.ts +58 -58
- package/src/runtime/deadletter.ts +14 -4
- 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 +48 -4
- package/src/runtime/live-agent-control.ts +87 -87
- package/src/runtime/live-agent-manager.ts +85 -85
- package/src/runtime/live-control-realtime.ts +36 -36
- package/src/runtime/live-session-runtime.ts +305 -305
- package/src/runtime/manifest-cache.ts +2 -2
- package/src/runtime/model-fallback.ts +269 -261
- package/src/runtime/parallel-research.ts +44 -44
- package/src/runtime/parallel-utils.ts +1 -1
- package/src/runtime/pi-json-output.ts +111 -111
- package/src/runtime/policy-engine.ts +79 -78
- package/src/runtime/post-exit-stdio-guard.ts +2 -2
- package/src/runtime/process-status.ts +56 -56
- package/src/runtime/progress-event-coalescer.ts +43 -43
- package/src/runtime/recovery-recipes.ts +74 -74
- package/src/runtime/retry-executor.ts +5 -0
- package/src/runtime/role-permission.ts +39 -39
- package/src/runtime/runtime-resolver.ts +1 -1
- package/src/runtime/session-usage.ts +79 -79
- package/src/runtime/sidechain-output.ts +29 -29
- package/src/runtime/subagent-manager.ts +3 -3
- package/src/runtime/task-display.ts +38 -38
- package/src/runtime/task-output-context.ts +127 -127
- package/src/runtime/task-runner/live-executor.ts +101 -101
- package/src/runtime/task-runner/progress.ts +119 -111
- package/src/runtime/task-runner/result-utils.ts +14 -14
- package/src/runtime/task-runner/state-helpers.ts +22 -22
- package/src/runtime/team-runner.ts +3 -6
- package/src/runtime/worker-heartbeat.ts +21 -21
- package/src/runtime/worker-startup.ts +57 -57
- package/src/schema/config-schema.ts +1 -1
- package/src/schema/team-tool-schema.ts +110 -109
- package/src/state/artifact-store.ts +4 -2
- package/src/state/atomic-write.ts +12 -4
- package/src/state/contracts.ts +105 -105
- package/src/state/event-log.ts +3 -4
- package/src/state/jsonl-writer.ts +4 -1
- package/src/state/locks.ts +9 -1
- package/src/state/task-claims.ts +44 -42
- 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/discover-teams.ts +2 -2
- 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/dashboard-panes/metrics-pane.ts +34 -34
- package/src/ui/dynamic-border.ts +25 -25
- package/src/ui/layout-primitives.ts +106 -106
- package/src/ui/live-run-sidebar.ts +1 -1
- package/src/ui/loaders.ts +158 -158
- package/src/ui/mascot.ts +3 -2
- package/src/ui/powerbar-publisher.ts +4 -3
- package/src/ui/render-diff.ts +119 -119
- package/src/ui/render-scheduler.ts +54 -14
- package/src/ui/run-dashboard.ts +39 -11
- package/src/ui/run-snapshot-cache.ts +63 -37
- package/src/ui/spinner.ts +17 -17
- package/src/ui/status-colors.ts +54 -54
- package/src/ui/syntax-highlight.ts +116 -116
- package/src/ui/theme-adapter.ts +1 -1
- package/src/ui/transcript-viewer.ts +7 -2
- package/src/utils/completion-dedupe.ts +63 -63
- package/src/utils/file-coalescer.ts +5 -3
- package/src/utils/frontmatter.ts +68 -36
- package/src/utils/git.ts +262 -262
- package/src/utils/ids.ts +12 -12
- package/src/utils/internal-error.ts +1 -1
- package/src/utils/names.ts +27 -26
- package/src/utils/paths.ts +1 -1
- package/src/utils/redaction.ts +44 -41
- package/src/utils/safe-paths.ts +47 -34
- package/src/utils/sleep.ts +2 -2
- package/src/utils/timings.ts +2 -0
- package/src/utils/visual.ts +9 -1
- package/src/workflows/discover-workflows.ts +4 -1
- package/src/workflows/validate-workflow.ts +40 -40
- package/src/worktree/branch-freshness.ts +45 -45
- package/src/worktree/worktree-manager.ts +6 -1
- 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
|
@@ -7,6 +7,7 @@ import { readPersistedSubagentRecord, savePersistedSubagentRecord, type Subagent
|
|
|
7
7
|
import { loadConfig } from "../../config/config.ts";
|
|
8
8
|
import { logInternalError } from "../../utils/internal-error.ts";
|
|
9
9
|
import { __test__subagentSpawnParams, formatSubagentRecord, readSubagentRunResult, refreshPersistedSubagentRecord, subagentToolResult } from "./subagent-helpers.ts";
|
|
10
|
+
import { t } from "../../i18n.ts";
|
|
10
11
|
|
|
11
12
|
export interface SubagentToolRegistrationOptions {
|
|
12
13
|
ownerSessionGeneration?: () => number;
|
|
@@ -37,7 +38,7 @@ export function registerSubagentTools(pi: ExtensionAPI, subagentManager: Subagen
|
|
|
37
38
|
if (!permission.allowed) return subagentToolResult(permission.reason ?? "Current role cannot spawn subagents.", { role: currentRole, mode: permission.mode }, true);
|
|
38
39
|
const spawnOptions = __test__subagentSpawnParams(params as Record<string, unknown>, ctx);
|
|
39
40
|
spawnOptions.ownerSessionGeneration = options.ownerSessionGeneration?.();
|
|
40
|
-
if (!spawnOptions.prompt.trim()) return subagentToolResult("
|
|
41
|
+
if (!spawnOptions.prompt.trim()) return subagentToolResult(t("agent.requiresPrompt"), {}, true);
|
|
41
42
|
const runner = async (currentOptions: SubagentSpawnOptions, childSignal?: AbortSignal) => handleTeamTool({ action: "run", agent: currentOptions.type, goal: currentOptions.prompt, model: currentOptions.model, async: currentOptions.background, config: currentOptions.maxTurns ? { runtime: { maxTurns: currentOptions.maxTurns } } : undefined } as TeamToolParamsValue, currentOptions.background ? { ...ctx, signal: childSignal } : { ...ctx, signal: childSignal });
|
|
42
43
|
const record = subagentManager.spawn(spawnOptions, runner, spawnOptions.background ? undefined : signal);
|
|
43
44
|
if (spawnOptions.background || record.status === "queued") {
|
|
@@ -45,11 +46,11 @@ export function registerSubagentTools(pi: ExtensionAPI, subagentManager: Subagen
|
|
|
45
46
|
// Phase 1.6: Record was terminated for telemetry.
|
|
46
47
|
record.terminated = true;
|
|
47
48
|
savePersistedSubagentRecord(ctx.cwd, record);
|
|
48
|
-
return { ...subagentToolResult([
|
|
49
|
+
return { ...subagentToolResult([t("agent.started", { state: record.status === "queued" ? "queued" : "started" }), t("agent.id", { id: record.id }), t("agent.type", { type: record.type }), t("agent.description", { description: record.description }), t("agent.retrieveHint")].join("\n"), { agentId: record.id, status: record.status }), terminate: true };
|
|
49
50
|
}
|
|
50
51
|
await record.promise;
|
|
51
|
-
const output = readSubagentRunResult(ctx, record) ?? record.result ?? "
|
|
52
|
-
const foregroundResult = subagentToolResult([
|
|
52
|
+
const output = readSubagentRunResult(ctx, record) ?? record.result ?? t("agent.noOutput");
|
|
53
|
+
const foregroundResult = subagentToolResult([t("agent.foregroundStatus", { id: record.id, status: record.status }), "", output].join("\n"), { agentId: record.id, runId: record.runId, status: record.status }, record.status === "failed" || record.status === "error");
|
|
53
54
|
if (loadConfig(ctx.cwd).config.tools?.terminateOnForeground === true) {
|
|
54
55
|
record.terminated = true;
|
|
55
56
|
savePersistedSubagentRecord(ctx.cwd, record);
|
|
@@ -66,14 +67,14 @@ export function registerSubagentTools(pi: ExtensionAPI, subagentManager: Subagen
|
|
|
66
67
|
parameters: Type.Object({ agent_id: Type.String({ description: "Agent ID returned by Agent." }), wait: Type.Optional(Type.Boolean({ description: "Wait for completion before returning." })), verbose: Type.Optional(Type.Boolean({ description: "Include status metadata before output." })) }) as never,
|
|
67
68
|
async execute(_id, params, signal, _onUpdate, ctx) {
|
|
68
69
|
const p = params as { agent_id?: string; wait?: boolean; verbose?: boolean };
|
|
69
|
-
if (!p.agent_id) return subagentToolResult("
|
|
70
|
+
if (!p.agent_id) return subagentToolResult(t("result.requiresAgentId"), {}, true);
|
|
70
71
|
const inMemory = subagentManager.getRecord(p.agent_id);
|
|
71
72
|
const record = inMemory ?? readPersistedSubagentRecord(ctx.cwd, p.agent_id);
|
|
72
|
-
if (!record) return subagentToolResult(
|
|
73
|
+
if (!record) return subagentToolResult(t("result.notFound", { id: p.agent_id }), {}, true);
|
|
73
74
|
let current = refreshPersistedSubagentRecord(ctx, record);
|
|
74
75
|
if (inMemory && current !== inMemory) Object.assign(inMemory, current);
|
|
75
76
|
if (!inMemory && !current.runId && (current.status === "running" || current.status === "queued")) {
|
|
76
|
-
current = { ...current, status: "error", error: "
|
|
77
|
+
current = { ...current, status: "error", error: t("result.unrecoverable"), completedAt: current.completedAt ?? Date.now() };
|
|
77
78
|
savePersistedSubagentRecord(ctx.cwd, current);
|
|
78
79
|
}
|
|
79
80
|
if (p.wait && (current.status === "running" || current.status === "queued")) {
|
|
@@ -83,16 +84,24 @@ export function registerSubagentTools(pi: ExtensionAPI, subagentManager: Subagen
|
|
|
83
84
|
current.resultConsumed = false;
|
|
84
85
|
if (inMemory) inMemory.resultConsumed = false;
|
|
85
86
|
savePersistedSubagentRecord(ctx.cwd, current);
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
87
|
+
} else {
|
|
88
|
+
const waitStartMs = Date.now();
|
|
89
|
+
const maxWaitMs = 300_000; // 5 minutes
|
|
90
|
+
while (current.status === "running" || current.status === "queued") {
|
|
91
|
+
if (signal?.aborted) {
|
|
92
|
+
current = { ...current, status: "error", error: t("result.waitAborted"), completedAt: Date.now() };
|
|
93
|
+
savePersistedSubagentRecord(ctx.cwd, current);
|
|
94
|
+
break;
|
|
95
|
+
}
|
|
96
|
+
if (Date.now() - waitStartMs > maxWaitMs) {
|
|
97
|
+
current = { ...current, status: "error", error: t("result.waitTimeout"), completedAt: Date.now() };
|
|
98
|
+
savePersistedSubagentRecord(ctx.cwd, current);
|
|
99
|
+
break;
|
|
100
|
+
}
|
|
101
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
102
|
+
current = refreshPersistedSubagentRecord(ctx, current);
|
|
103
|
+
if (!current.runId) break;
|
|
92
104
|
}
|
|
93
|
-
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
94
|
-
current = refreshPersistedSubagentRecord(ctx, current);
|
|
95
|
-
if (!current.runId) break;
|
|
96
105
|
}
|
|
97
106
|
}
|
|
98
107
|
const output = readSubagentRunResult(ctx, current);
|
|
@@ -101,7 +110,7 @@ export function registerSubagentTools(pi: ExtensionAPI, subagentManager: Subagen
|
|
|
101
110
|
if (inMemory) inMemory.resultConsumed = true;
|
|
102
111
|
savePersistedSubagentRecord(ctx.cwd, current);
|
|
103
112
|
}
|
|
104
|
-
const text = [p.verbose ? formatSubagentRecord(current) : undefined, output ? `${p.verbose ? "\n" : ""}${output}` : current.status === "running" || current.status === "queued" ? "
|
|
113
|
+
const text = [p.verbose ? formatSubagentRecord(current) : undefined, output ? `${p.verbose ? "\n" : ""}${output}` : current.status === "running" || current.status === "queued" ? t("result.stillRunning") : current.error ?? t("agent.noOutput")].filter((line): line is string => Boolean(line)).join("\n");
|
|
105
114
|
return subagentToolResult(text, { agentId: current.id, runId: current.runId, status: current.status }, current.status === "failed" || current.status === "error");
|
|
106
115
|
},
|
|
107
116
|
};
|
|
@@ -114,8 +123,8 @@ export function registerSubagentTools(pi: ExtensionAPI, subagentManager: Subagen
|
|
|
114
123
|
async execute(_id, params, _signal, _onUpdate, ctx) {
|
|
115
124
|
const p = params as { agent_id?: string; message?: string };
|
|
116
125
|
const record = p.agent_id ? subagentManager.getRecord(p.agent_id) ?? readPersistedSubagentRecord(ctx.cwd, p.agent_id) : undefined;
|
|
117
|
-
if (!record) return subagentToolResult(
|
|
118
|
-
return subagentToolResult([
|
|
126
|
+
if (!record) return subagentToolResult(t("result.notFound", { id: p.agent_id ?? "" }), {}, true);
|
|
127
|
+
return subagentToolResult([t("steer.noted", { id: record.id }), t("steer.unavailable"), record.runId ? t("steer.cancelHint", { runId: record.runId }) : undefined].filter((line): line is string => Boolean(line)).join("\n"), { agentId: record.id, runId: record.runId, status: record.status });
|
|
119
128
|
},
|
|
120
129
|
};
|
|
121
130
|
|
|
@@ -61,7 +61,7 @@ export function createResultWatcher(events: ResultWatcherEvents, resultsDir: str
|
|
|
61
61
|
coalescer.schedule(file, RESULT_WATCHER_POLL_MS);
|
|
62
62
|
return;
|
|
63
63
|
}
|
|
64
|
-
const key = buildCompletionKey(payload as Record<string, unknown
|
|
64
|
+
const key = buildCompletionKey(payload && typeof payload === "object" && !Array.isArray(payload) ? payload as Record<string, unknown> : {}, `file:${file}`);
|
|
65
65
|
if (!markSeenWithTtl(seen, key, Date.now(), completionTtlMs)) {
|
|
66
66
|
events.emit(eventName, payload);
|
|
67
67
|
}
|
|
@@ -78,7 +78,7 @@ export function createResultWatcher(events: ResultWatcherEvents, resultsDir: str
|
|
|
78
78
|
const startPolling = () => {
|
|
79
79
|
if (pollTimer) return;
|
|
80
80
|
pollTimer = setInterval(poll, RESULT_WATCHER_POLL_MS);
|
|
81
|
-
pollTimer.unref
|
|
81
|
+
pollTimer.unref();
|
|
82
82
|
poll();
|
|
83
83
|
};
|
|
84
84
|
const stopPolling = () => {
|
|
@@ -98,7 +98,7 @@ export function createResultWatcher(events: ResultWatcherEvents, resultsDir: str
|
|
|
98
98
|
logInternalError("result-watcher.restart", error, `resultsDir=${resultsDir}`);
|
|
99
99
|
}
|
|
100
100
|
}, RESULT_WATCHER_RESTART_MS);
|
|
101
|
-
restartTimer.unref
|
|
101
|
+
restartTimer.unref();
|
|
102
102
|
};
|
|
103
103
|
const handle: ResultWatcherHandle = {
|
|
104
104
|
start() {
|
|
@@ -110,7 +110,7 @@ export function createResultWatcher(events: ResultWatcherEvents, resultsDir: str
|
|
|
110
110
|
coalescer.schedule(fileName.toString());
|
|
111
111
|
}, scheduleRestart);
|
|
112
112
|
if (watcher) stopPolling();
|
|
113
|
-
watcher?.unref
|
|
113
|
+
watcher?.unref();
|
|
114
114
|
},
|
|
115
115
|
prime() {
|
|
116
116
|
poll();
|
|
@@ -17,12 +17,13 @@ function validateArtifact(value: unknown, index: number, errors: string[]): valu
|
|
|
17
17
|
errors.push(`manifest.artifacts[${index}] must be an object.`);
|
|
18
18
|
return false;
|
|
19
19
|
}
|
|
20
|
+
const before = errors.length;
|
|
20
21
|
if (typeof value.kind !== "string") errors.push(`manifest.artifacts[${index}].kind must be a string.`);
|
|
21
22
|
if (typeof value.path !== "string") errors.push(`manifest.artifacts[${index}].path must be a string.`);
|
|
22
23
|
if (typeof value.createdAt !== "string") errors.push(`manifest.artifacts[${index}].createdAt must be a string.`);
|
|
23
24
|
if (typeof value.producer !== "string") errors.push(`manifest.artifacts[${index}].producer must be a string.`);
|
|
24
25
|
if (value.retention !== "run" && value.retention !== "project" && value.retention !== "temporary") errors.push(`manifest.artifacts[${index}].retention is invalid.`);
|
|
25
|
-
return
|
|
26
|
+
return errors.length === before;
|
|
26
27
|
}
|
|
27
28
|
|
|
28
29
|
function validateManifest(value: unknown, errors: string[]): value is TeamRunManifest {
|
|
@@ -30,6 +31,7 @@ function validateManifest(value: unknown, errors: string[]): value is TeamRunMan
|
|
|
30
31
|
errors.push("manifest must be an object.");
|
|
31
32
|
return false;
|
|
32
33
|
}
|
|
34
|
+
const before = errors.length;
|
|
33
35
|
if (value.schemaVersion !== 1) errors.push("manifest.schemaVersion must be 1.");
|
|
34
36
|
for (const field of ["runId", "team", "goal", "createdAt", "updatedAt", "cwd", "stateRoot", "artifactsRoot", "tasksPath", "eventsPath"] as const) {
|
|
35
37
|
if (typeof value[field] !== "string") errors.push(`manifest.${field} must be a string.`);
|
|
@@ -38,7 +40,7 @@ function validateManifest(value: unknown, errors: string[]): value is TeamRunMan
|
|
|
38
40
|
if (value.workspaceMode !== "single" && value.workspaceMode !== "worktree") errors.push("manifest.workspaceMode must be single or worktree.");
|
|
39
41
|
if (!Array.isArray(value.artifacts)) errors.push("manifest.artifacts must be an array.");
|
|
40
42
|
else value.artifacts.forEach((artifact, index) => validateArtifact(artifact, index, errors));
|
|
41
|
-
return
|
|
43
|
+
return errors.length === before;
|
|
42
44
|
}
|
|
43
45
|
|
|
44
46
|
function validateTask(value: unknown, index: number, errors: string[]): value is TeamTaskState {
|
|
@@ -46,12 +48,13 @@ function validateTask(value: unknown, index: number, errors: string[]): value is
|
|
|
46
48
|
errors.push(`tasks[${index}] must be an object.`);
|
|
47
49
|
return false;
|
|
48
50
|
}
|
|
51
|
+
const before = errors.length;
|
|
49
52
|
for (const field of ["id", "runId", "role", "agent", "title", "cwd"] as const) {
|
|
50
53
|
if (typeof value[field] !== "string") errors.push(`tasks[${index}].${field} must be a string.`);
|
|
51
54
|
}
|
|
52
55
|
if (!isTeamTaskStatus(value.status)) errors.push(`tasks[${index}].status is invalid.`);
|
|
53
56
|
if (!Array.isArray(value.dependsOn)) errors.push(`tasks[${index}].dependsOn must be an array.`);
|
|
54
|
-
return
|
|
57
|
+
return errors.length === before;
|
|
55
58
|
}
|
|
56
59
|
|
|
57
60
|
function validateEvent(value: unknown, index: number, errors: string[]): value is TeamEvent {
|
|
@@ -59,10 +62,11 @@ function validateEvent(value: unknown, index: number, errors: string[]): value i
|
|
|
59
62
|
errors.push(`events[${index}] must be an object.`);
|
|
60
63
|
return false;
|
|
61
64
|
}
|
|
65
|
+
const before = errors.length;
|
|
62
66
|
for (const field of ["time", "type", "runId"] as const) {
|
|
63
67
|
if (typeof value[field] !== "string") errors.push(`events[${index}].${field} must be a string.`);
|
|
64
68
|
}
|
|
65
|
-
return
|
|
69
|
+
return errors.length === before;
|
|
66
70
|
}
|
|
67
71
|
|
|
68
72
|
export function validateRunBundle(value: unknown): BundleValidationResult {
|
|
@@ -29,6 +29,10 @@ export function importRunBundle(cwd: string, bundlePath: string, scope: "project
|
|
|
29
29
|
resolveRealContainedPath(path.dirname(importsRoot), path.basename(importsRoot));
|
|
30
30
|
const root = resolveContainedRelativePath(importsRoot, runId, "runId");
|
|
31
31
|
fs.mkdirSync(root, { recursive: true });
|
|
32
|
+
// TOCTOU note: mkdirSync would throw EEXIST if a symlink already existed.
|
|
33
|
+
// The lstatSync check catches a symlink swapped in between mkdirSync and the check
|
|
34
|
+
// (theoretically possible but requires local attacker with exact timing).
|
|
35
|
+
// resolveRealContainedPath provides an additional real-path containment barrier.
|
|
32
36
|
if (fs.lstatSync(root).isSymbolicLink()) throw new Error(`Invalid import directory: ${root}`);
|
|
33
37
|
resolveRealContainedPath(importsRoot, runId);
|
|
34
38
|
const targetJson = path.join(root, "run-export.json");
|
|
@@ -40,7 +40,10 @@ function mergeRuns(runSets: TeamRunManifest[][], max?: number): TeamRunManifest[
|
|
|
40
40
|
}
|
|
41
41
|
|
|
42
42
|
function scopedRunRoots(cwd: string): string[] {
|
|
43
|
-
|
|
43
|
+
const roots: string[] = [userCrewRoot()];
|
|
44
|
+
const projectRoot = findRepoRoot(cwd);
|
|
45
|
+
if (projectRoot) roots.unshift(projectCrewRoot(cwd));
|
|
46
|
+
return roots;
|
|
44
47
|
}
|
|
45
48
|
|
|
46
49
|
export function listRuns(cwd: string): TeamRunManifest[] {
|
|
@@ -1,24 +1,43 @@
|
|
|
1
|
-
import * as fs from "node:fs";
|
|
2
|
-
import type { TeamRunManifest } from "../state/types.ts";
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import type { TeamRunManifest } from "../state/types.ts";
|
|
3
|
+
import { resolveRealContainedPath } from "../utils/safe-paths.ts";
|
|
4
|
+
import { projectCrewRoot } from "../utils/paths.ts";
|
|
5
|
+
import { listRuns } from "./run-index.ts";
|
|
6
|
+
import { logInternalError } from "../utils/internal-error.ts";
|
|
7
|
+
|
|
8
|
+
export interface PruneRunsResult {
|
|
9
|
+
kept: string[];
|
|
10
|
+
removed: string[];
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function isFinished(run: TeamRunManifest): boolean {
|
|
14
|
+
return run.status === "completed" || run.status === "failed" || run.status === "cancelled" || run.status === "blocked";
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function isSafeToPrune(cwd: string, run: TeamRunManifest): boolean {
|
|
18
|
+
try {
|
|
19
|
+
const crewRoot = projectCrewRoot(cwd);
|
|
20
|
+
resolveRealContainedPath(crewRoot, run.stateRoot);
|
|
21
|
+
resolveRealContainedPath(crewRoot, run.artifactsRoot);
|
|
22
|
+
return true;
|
|
23
|
+
} catch {
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function pruneFinishedRuns(cwd: string, keep: number): PruneRunsResult {
|
|
29
|
+
const crewRoot = projectCrewRoot(cwd);
|
|
30
|
+
const finished = listRuns(cwd).filter((run) => run.cwd === cwd && isFinished(run)).sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
|
|
31
|
+
const kept = finished.slice(0, keep).map((run) => run.runId);
|
|
32
|
+
const removed: string[] = [];
|
|
33
|
+
for (const run of finished.slice(keep)) {
|
|
34
|
+
if (!isSafeToPrune(cwd, run)) {
|
|
35
|
+
logInternalError("prune.path-unsafe", new Error(`Skipping unsafe prune: stateRoot=${run.stateRoot}, artifactsRoot=${run.artifactsRoot}`), `runId=${run.runId}`);
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
fs.rmSync(run.stateRoot, { recursive: true, force: true });
|
|
39
|
+
fs.rmSync(run.artifactsRoot, { recursive: true, force: true });
|
|
40
|
+
removed.push(run.runId);
|
|
41
|
+
}
|
|
42
|
+
return { kept, removed };
|
|
43
|
+
}
|
|
@@ -22,7 +22,7 @@ import type { PiTeamsToolResult } from "../tool-result.ts";
|
|
|
22
22
|
import { configRecord, result, type TeamContext } from "./context.ts";
|
|
23
23
|
|
|
24
24
|
function globMatch(value: string, pattern: string): boolean {
|
|
25
|
-
const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
|
|
25
|
+
const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\?/g, "\\?").replace(/\*/g, ".*");
|
|
26
26
|
return new RegExp(`^${escaped}$`).test(value);
|
|
27
27
|
}
|
|
28
28
|
|
|
@@ -1,31 +1,31 @@
|
|
|
1
|
-
import type { TeamToolParamsValue } from "../../schema/team-tool-schema.ts";
|
|
2
|
-
import { withRunLockSync } from "../../state/locks.ts";
|
|
3
|
-
import { loadRunManifestById, saveRunTasks, updateRunStatus } from "../../state/state-store.ts";
|
|
4
|
-
import { saveCrewAgents, recordFromTask } from "../../runtime/crew-agent-records.ts";
|
|
5
|
-
import { writeForegroundInterruptRequest } from "../../runtime/foreground-control.ts";
|
|
6
|
-
import { logInternalError } from "../../utils/internal-error.ts";
|
|
7
|
-
import type { PiTeamsToolResult } from "../tool-result.ts";
|
|
8
|
-
import { result, type TeamContext } from "./context.ts";
|
|
9
|
-
|
|
10
|
-
export function handleCancel(params: TeamToolParamsValue, ctx: TeamContext): PiTeamsToolResult {
|
|
11
|
-
if (!params.runId) return result("Cancel requires runId.", { action: "cancel", status: "error" }, true);
|
|
12
|
-
const loaded = loadRunManifestById(ctx.cwd, params.runId);
|
|
13
|
-
if (!loaded) return result(`Run '${params.runId}' not found.`, { action: "cancel", status: "error" }, true);
|
|
14
|
-
return withRunLockSync(loaded.manifest, () => {
|
|
15
|
-
if (loaded.manifest.status === "completed" && !params.force) return result(`Run ${loaded.manifest.runId} is already completed; nothing to cancel. Use force: true to mark it cancelled anyway.`, { action: "cancel", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot });
|
|
16
|
-
const tasks = loaded.tasks.map((task) => task.status === "queued" || task.status === "running" ? { ...task, status: "cancelled" as const, finishedAt: new Date().toISOString(), error: "Run cancelled by user request." } : task);
|
|
17
|
-
saveRunTasks(loaded.manifest, tasks);
|
|
18
|
-
try {
|
|
19
|
-
saveCrewAgents(loaded.manifest, tasks.map((task) => recordFromTask(loaded.manifest, task, "child-process")));
|
|
20
|
-
} catch (error) {
|
|
21
|
-
logInternalError("team-tool.handleCancel.crewAgents", error, `runId=${loaded.manifest.runId}`);
|
|
22
|
-
}
|
|
23
|
-
try {
|
|
24
|
-
writeForegroundInterruptRequest(loaded.manifest, "Run cancelled by user request.");
|
|
25
|
-
} catch (error) {
|
|
26
|
-
logInternalError("team-tool.handleCancel.interruptRequest", error, `runId=${loaded.manifest.runId}`);
|
|
27
|
-
}
|
|
28
|
-
const updated = updateRunStatus(loaded.manifest, "cancelled", "Run cancelled by user request. Already-finished worker processes are not retroactively changed.");
|
|
29
|
-
return result(`Cancelled run ${updated.runId}.`, { action: "cancel", status: "ok", runId: updated.runId, artifactsRoot: updated.artifactsRoot });
|
|
30
|
-
});
|
|
31
|
-
}
|
|
1
|
+
import type { TeamToolParamsValue } from "../../schema/team-tool-schema.ts";
|
|
2
|
+
import { withRunLockSync } from "../../state/locks.ts";
|
|
3
|
+
import { loadRunManifestById, saveRunTasks, updateRunStatus } from "../../state/state-store.ts";
|
|
4
|
+
import { saveCrewAgents, recordFromTask } from "../../runtime/crew-agent-records.ts";
|
|
5
|
+
import { writeForegroundInterruptRequest } from "../../runtime/foreground-control.ts";
|
|
6
|
+
import { logInternalError } from "../../utils/internal-error.ts";
|
|
7
|
+
import type { PiTeamsToolResult } from "../tool-result.ts";
|
|
8
|
+
import { result, type TeamContext } from "./context.ts";
|
|
9
|
+
|
|
10
|
+
export function handleCancel(params: TeamToolParamsValue, ctx: TeamContext): PiTeamsToolResult {
|
|
11
|
+
if (!params.runId) return result("Cancel requires runId.", { action: "cancel", status: "error" }, true);
|
|
12
|
+
const loaded = loadRunManifestById(ctx.cwd, params.runId);
|
|
13
|
+
if (!loaded) return result(`Run '${params.runId}' not found.`, { action: "cancel", status: "error" }, true);
|
|
14
|
+
return withRunLockSync(loaded.manifest, () => {
|
|
15
|
+
if (loaded.manifest.status === "completed" && !params.force) return result(`Run ${loaded.manifest.runId} is already completed; nothing to cancel. Use force: true to mark it cancelled anyway.`, { action: "cancel", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot });
|
|
16
|
+
const tasks = loaded.tasks.map((task) => task.status === "queued" || task.status === "running" ? { ...task, status: "cancelled" as const, finishedAt: new Date().toISOString(), error: "Run cancelled by user request." } : task);
|
|
17
|
+
saveRunTasks(loaded.manifest, tasks);
|
|
18
|
+
try {
|
|
19
|
+
saveCrewAgents(loaded.manifest, tasks.map((task) => recordFromTask(loaded.manifest, task, "child-process")));
|
|
20
|
+
} catch (error) {
|
|
21
|
+
logInternalError("team-tool.handleCancel.crewAgents", error, `runId=${loaded.manifest.runId}`);
|
|
22
|
+
}
|
|
23
|
+
try {
|
|
24
|
+
writeForegroundInterruptRequest(loaded.manifest, "Run cancelled by user request.");
|
|
25
|
+
} catch (error) {
|
|
26
|
+
logInternalError("team-tool.handleCancel.interruptRequest", error, `runId=${loaded.manifest.runId}`);
|
|
27
|
+
}
|
|
28
|
+
const updated = updateRunStatus(loaded.manifest, "cancelled", "Run cancelled by user request. Already-finished worker processes are not retroactively changed.");
|
|
29
|
+
return result(`Cancelled run ${updated.runId}.`, { action: "cancel", status: "ok", runId: updated.runId, artifactsRoot: updated.artifactsRoot });
|
|
30
|
+
});
|
|
31
|
+
}
|
|
@@ -52,7 +52,14 @@ function checkWritableDir(dir: string): { ok: boolean; detail: string } {
|
|
|
52
52
|
try {
|
|
53
53
|
if (!fs.existsSync(dir)) return { ok: false, detail: `${dir}: missing` };
|
|
54
54
|
if (!fs.statSync(dir).isDirectory()) return { ok: false, detail: `${dir}: not a directory` };
|
|
55
|
-
fs.accessSync(
|
|
55
|
+
// fs.accessSync(W_OK) is unreliable on Windows; verify by writing a temp file.
|
|
56
|
+
const probePath = `${dir}/.pi-crew-write-test`;
|
|
57
|
+
try {
|
|
58
|
+
fs.writeFileSync(probePath, "ok", "utf-8");
|
|
59
|
+
fs.rmSync(probePath, { force: true });
|
|
60
|
+
} catch {
|
|
61
|
+
return { ok: false, detail: `${dir}: not writable (write test failed)` };
|
|
62
|
+
}
|
|
56
63
|
return { ok: true, detail: dir };
|
|
57
64
|
} catch (error) {
|
|
58
65
|
const message = error instanceof Error ? error.message : String(error);
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import type { TeamContext } from "../team-tool/context.ts";
|
|
2
|
+
import { loadConfig, updateConfig } from "../../config/config.ts";
|
|
3
|
+
import { configPatchFromConfig } from "../team-tool/config-patch.ts";
|
|
4
|
+
import { result } from "../team-tool/context.ts";
|
|
5
|
+
import type { PiTeamsToolResult } from "../tool-result.ts";
|
|
6
|
+
|
|
7
|
+
// ---------------------------------------------------------------------------
|
|
8
|
+
// Helpers
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
10
|
+
|
|
11
|
+
function setNested(obj: Record<string, unknown>, path: string, value: unknown): void {
|
|
12
|
+
const keys = path.split(".");
|
|
13
|
+
let target: Record<string, unknown> = obj;
|
|
14
|
+
for (let i = 0; i < keys.length - 1; i++) {
|
|
15
|
+
if (!target[keys[i]] || typeof target[keys[i]] !== "object") {
|
|
16
|
+
target[keys[i]] = {};
|
|
17
|
+
}
|
|
18
|
+
target = target[keys[i]] as Record<string, unknown>;
|
|
19
|
+
}
|
|
20
|
+
target[keys[keys.length - 1]] = value;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function getNested(obj: Record<string, unknown>, path: string): unknown {
|
|
24
|
+
const keys = path.split(".");
|
|
25
|
+
let current: unknown = obj;
|
|
26
|
+
for (const key of keys) {
|
|
27
|
+
if (!current || typeof current !== "object") return undefined;
|
|
28
|
+
current = (current as Record<string, unknown>)[key];
|
|
29
|
+
}
|
|
30
|
+
return current;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function formatValue(value: unknown): string {
|
|
34
|
+
if (value === undefined) return "<not set>";
|
|
35
|
+
if (typeof value === "object") return JSON.stringify(value);
|
|
36
|
+
return String(value);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function parseValue(raw: string): unknown {
|
|
40
|
+
// JSON handles strings (quoted), numbers, booleans, null, arrays, objects.
|
|
41
|
+
try { return JSON.parse(raw); } catch { /* keep as string */ }
|
|
42
|
+
return raw;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// ---------------------------------------------------------------------------
|
|
46
|
+
// Known config keys — mirrors config-schema.ts + config.ts.
|
|
47
|
+
// When adding new config fields, add the dotted path here so team-settings
|
|
48
|
+
// can discover and display them.
|
|
49
|
+
// ---------------------------------------------------------------------------
|
|
50
|
+
|
|
51
|
+
const KNOWN_KEYS = new Set([
|
|
52
|
+
// top-level
|
|
53
|
+
"asyncByDefault",
|
|
54
|
+
"executeWorkers",
|
|
55
|
+
"notifierIntervalMs",
|
|
56
|
+
"requireCleanWorktreeLeader",
|
|
57
|
+
// runtime
|
|
58
|
+
"runtime.mode",
|
|
59
|
+
"runtime.preferLiveSession",
|
|
60
|
+
"runtime.allowChildProcessFallback",
|
|
61
|
+
"runtime.maxTurns",
|
|
62
|
+
"runtime.graceTurns",
|
|
63
|
+
"runtime.inheritContext",
|
|
64
|
+
"runtime.promptMode",
|
|
65
|
+
"runtime.groupJoin",
|
|
66
|
+
"runtime.groupJoinAckTimeoutMs",
|
|
67
|
+
"runtime.requirePlanApproval",
|
|
68
|
+
"runtime.completionMutationGuard",
|
|
69
|
+
// limits
|
|
70
|
+
"limits.maxConcurrentWorkers",
|
|
71
|
+
"limits.allowUnboundedConcurrency",
|
|
72
|
+
"limits.maxTaskDepth",
|
|
73
|
+
"limits.maxChildrenPerTask",
|
|
74
|
+
"limits.maxRunMinutes",
|
|
75
|
+
"limits.maxRetriesPerTask",
|
|
76
|
+
"limits.maxTasksPerRun",
|
|
77
|
+
"limits.heartbeatStaleMs",
|
|
78
|
+
// control
|
|
79
|
+
"control.enabled",
|
|
80
|
+
"control.needsAttentionAfterMs",
|
|
81
|
+
// autonomous
|
|
82
|
+
"autonomous.profile",
|
|
83
|
+
"autonomous.enabled",
|
|
84
|
+
"autonomous.injectPolicy",
|
|
85
|
+
"autonomous.preferAsyncForLongTasks",
|
|
86
|
+
"autonomous.allowWorktreeSuggestion",
|
|
87
|
+
// tools
|
|
88
|
+
"tools.enableClaudeStyleAliases",
|
|
89
|
+
"tools.enableSteer",
|
|
90
|
+
"tools.terminateOnForeground",
|
|
91
|
+
// agents
|
|
92
|
+
"agents.disableBuiltins",
|
|
93
|
+
// observability
|
|
94
|
+
"observability.prometheus.enabled",
|
|
95
|
+
"observability.otlp.enabled",
|
|
96
|
+
// worktree
|
|
97
|
+
"worktree.enabled",
|
|
98
|
+
]);
|
|
99
|
+
|
|
100
|
+
const KNOWN_SORTED = [...KNOWN_KEYS].sort();
|
|
101
|
+
|
|
102
|
+
// ---------------------------------------------------------------------------
|
|
103
|
+
// Detail objects – all require { action, status } from TeamToolDetails.
|
|
104
|
+
// Extras (count, key, value, path) are passed as never to bypass the narrow
|
|
105
|
+
// TeamToolDetails interface (consistent with the rest of the codebase).
|
|
106
|
+
// ---------------------------------------------------------------------------
|
|
107
|
+
|
|
108
|
+
const OK = { action: "settings", status: "ok" as const };
|
|
109
|
+
const ERR = { action: "settings", status: "error" as const };
|
|
110
|
+
|
|
111
|
+
// ---------------------------------------------------------------------------
|
|
112
|
+
// Main handler
|
|
113
|
+
// ---------------------------------------------------------------------------
|
|
114
|
+
|
|
115
|
+
export function handleSettings(params: { config?: Record<string, unknown> }, ctx: TeamContext): PiTeamsToolResult {
|
|
116
|
+
const cfg = (params.config ?? {}) as Record<string, unknown>;
|
|
117
|
+
const args = typeof cfg.args === "string" ? cfg.args.trim() : "";
|
|
118
|
+
const scope = cfg.scope === "project" ? "project" : "user";
|
|
119
|
+
const loaded = loadConfig(ctx.cwd);
|
|
120
|
+
const effective = loaded.config as Record<string, unknown>;
|
|
121
|
+
|
|
122
|
+
// team-settings list
|
|
123
|
+
if (!args || args === "list") {
|
|
124
|
+
const lines = ["pi-crew settings:", `Path: ${loaded.path}`, ""];
|
|
125
|
+
for (const key of KNOWN_SORTED) {
|
|
126
|
+
const value = getNested(effective, key);
|
|
127
|
+
lines.push(` ${key} = ${formatValue(value)}`);
|
|
128
|
+
}
|
|
129
|
+
lines.push("", "Usage: team-settings [list|get <key>|set <key> <value>|unset <key>|path|scope]");
|
|
130
|
+
return result(lines.join("\n"), { ...OK, count: KNOWN_KEYS.size } as never);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// team-settings path
|
|
134
|
+
if (args === "path") {
|
|
135
|
+
return result(`pi-crew config path: ${loaded.path}`, { ...OK, path: loaded.path } as never);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// team-settings scope
|
|
139
|
+
if (args === "scope") {
|
|
140
|
+
return result(`Current scope: ${scope}\nConfig at: ${loaded.path}`, { ...OK, scope } as never);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// team-settings get <key>
|
|
144
|
+
if (args.startsWith("get ")) {
|
|
145
|
+
const key = args.slice(4).trim();
|
|
146
|
+
if (!key) return result("Usage: team-settings get <key>", { ...ERR }, true);
|
|
147
|
+
const value = getNested(effective, key);
|
|
148
|
+
const note = KNOWN_KEYS.has(key) ? "" : " (unknown key — may not take effect)";
|
|
149
|
+
return result(`${key} = ${formatValue(value)}${note}`, { ...OK, key, value } as never);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// team-settings unset <key>
|
|
153
|
+
if (args.startsWith("unset ")) {
|
|
154
|
+
const key = args.slice(6).trim();
|
|
155
|
+
if (!key) return result("Usage: team-settings unset <key>", { ...ERR }, true);
|
|
156
|
+
try {
|
|
157
|
+
const saved = updateConfig({}, { cwd: ctx.cwd, scope, unsetPaths: [key] });
|
|
158
|
+
return result(`Unset ${key}\nPath: ${saved.path}`, { ...OK, key } as never);
|
|
159
|
+
} catch (error) {
|
|
160
|
+
return result(error instanceof Error ? error.message : String(error), { ...ERR }, true);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// team-settings set <key> <value>
|
|
165
|
+
if (args.startsWith("set ")) {
|
|
166
|
+
const rest = args.slice(4).trim();
|
|
167
|
+
const spaceIdx = rest.indexOf(" ");
|
|
168
|
+
if (spaceIdx === -1) return result("Usage: team-settings set <key> <value>", { ...ERR }, true);
|
|
169
|
+
const key = rest.slice(0, spaceIdx);
|
|
170
|
+
const rawValue = rest.slice(spaceIdx + 1).trim();
|
|
171
|
+
if (!key) return result("Usage: team-settings set <key> <value>", { ...ERR }, true);
|
|
172
|
+
|
|
173
|
+
const value = parseValue(rawValue);
|
|
174
|
+
const patch = {};
|
|
175
|
+
setNested(patch as Record<string, unknown>, key, value);
|
|
176
|
+
|
|
177
|
+
try {
|
|
178
|
+
const converted = configPatchFromConfig({ config: patch as Record<string, unknown> });
|
|
179
|
+
const saved = updateConfig(converted, { cwd: ctx.cwd, scope });
|
|
180
|
+
const warning = KNOWN_KEYS.has(key) ? "" : "\nWarning: unknown key — verify it exists in config schema.";
|
|
181
|
+
return result(`Set ${key} = ${formatValue(value)}\nPath: ${saved.path}${warning}`, { ...OK, key, value } as never);
|
|
182
|
+
} catch (error) {
|
|
183
|
+
return result(error instanceof Error ? error.message : String(error), { ...ERR }, true);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return result("Unknown subcommand. Usage: team-settings [list|get <key>|set <key> <value>|unset <key>|path|scope]", { ...ERR }, true);
|
|
188
|
+
}
|