pi-herdr-agents 3.0.2 → 3.1.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/AGENTS.md +2 -0
- package/CHANGELOG.md +42 -1
- package/CONTEXT.md +25 -4
- package/README.md +126 -13
- package/RELEASING.md +2 -0
- package/docs/README.md +2 -1
- package/docs/adr/0011-explicit-worktree-cleanup.md +1 -1
- package/docs/adr/0015-report-opened-primary-workspace.md +121 -0
- package/docs/worktree-subagents.md +5 -3
- package/maestro/adapters/pi/activity-file.ts +11 -1
- package/maestro/adapters/pi/launch.ts +29 -15
- package/maestro/adapters/pi/process-identity.ts +38 -0
- package/maestro/core/activity.ts +1 -1
- package/maestro/core/config/model-config.ts +83 -10
- package/maestro/core/config/task-model-init.ts +208 -13
- package/maestro/core/config/task-model-types.ts +19 -1
- package/maestro/core/config/type-guards.ts +5 -0
- package/maestro/core/opened-primary-workspace.ts +108 -0
- package/maestro/core/supervision.ts +38 -13
- package/maestro/core/surface-provider.ts +22 -0
- package/maestro/core/wake.ts +37 -0
- package/maestro/core/worktree-cleanup.ts +52 -1
- package/maestro/core/worktree.ts +2 -0
- package/maestro/runtime/index.ts +2 -1
- package/maestro/runtime/task-model-init.ts +106 -9
- package/maestro/runtime/worktree-operations.ts +2 -0
- package/maestro/surfaces/fake/fake-surface-provider.ts +8 -0
- package/maestro/surfaces/herdr/herdr-surface-provider.ts +12 -1
- package/maestro/surfaces/herdr/herdr.ts +345 -49
- package/maestro/surfaces/herdr/terminal.ts +0 -10
- package/package.json +1 -1
- package/pi-extension/subagents/index.ts +170 -16
- package/skills/pi-herdr-agents/SKILL.md +16 -4
|
@@ -8,6 +8,10 @@ import {
|
|
|
8
8
|
export const RECONCILE_INTERVAL_MS = 4_800;
|
|
9
9
|
export const POLLING_INTERVAL_MS = 1_000;
|
|
10
10
|
const BATCH_UNHEALTHY_MS = 5_000;
|
|
11
|
+
const ABORT_MESSAGE = "Aborted while waiting for subagent to finish";
|
|
12
|
+
const UNREGISTERED_MESSAGE = "Supervision registration was unregistered";
|
|
13
|
+
const CONCURRENT_WAIT_MESSAGE =
|
|
14
|
+
"Supervision registration already has a parked wait";
|
|
11
15
|
|
|
12
16
|
export interface PaneListEntry {
|
|
13
17
|
paneId: string;
|
|
@@ -30,12 +34,18 @@ interface Entry {
|
|
|
30
34
|
generation: number;
|
|
31
35
|
wakeRegistration?: WakeRegistration;
|
|
32
36
|
pending?: WakeReason;
|
|
33
|
-
|
|
37
|
+
parked?: ParkedWait;
|
|
38
|
+
unregistered?: boolean;
|
|
34
39
|
inspection?: PaneInspection;
|
|
35
40
|
fallback: boolean;
|
|
36
41
|
fileFallback: boolean;
|
|
37
42
|
}
|
|
38
43
|
|
|
44
|
+
interface ParkedWait {
|
|
45
|
+
resolve(reason: WakeReason): void;
|
|
46
|
+
reject(error: Error): void;
|
|
47
|
+
}
|
|
48
|
+
|
|
39
49
|
export interface SupervisionDiagnostics {
|
|
40
50
|
mode: "wake+batch" | "polling(forced)" | "polling(fallback)";
|
|
41
51
|
watcherCount: number;
|
|
@@ -138,12 +148,15 @@ export class SupervisionCoordinator {
|
|
|
138
148
|
}
|
|
139
149
|
|
|
140
150
|
private unregister(entry: Entry): void {
|
|
151
|
+
entry.unregistered = true;
|
|
141
152
|
entry.wakeRegistration?.unregister();
|
|
142
153
|
this.entries.delete(entry);
|
|
143
154
|
if (this.entries.size === 0 && this.timer) {
|
|
144
155
|
clearInterval(this.timer);
|
|
145
156
|
this.timer = undefined;
|
|
146
157
|
}
|
|
158
|
+
// Nothing can wake a parked wait now; settle it so its retain is released.
|
|
159
|
+
entry.parked?.reject(new Error(UNREGISTERED_MESSAGE));
|
|
147
160
|
}
|
|
148
161
|
|
|
149
162
|
private enterFallback(entry: Entry): void {
|
|
@@ -155,10 +168,12 @@ export class SupervisionCoordinator {
|
|
|
155
168
|
}
|
|
156
169
|
|
|
157
170
|
private wait(entry: Entry, signal: AbortSignal): Promise<WakeReason> {
|
|
158
|
-
if (signal.aborted)
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
171
|
+
if (signal.aborted) return Promise.reject(new Error(ABORT_MESSAGE));
|
|
172
|
+
if (entry.unregistered)
|
|
173
|
+
return Promise.reject(new Error(UNREGISTERED_MESSAGE));
|
|
174
|
+
// One completion loop consumes a registration: `pending` queues a single
|
|
175
|
+
// reason for a single waiter, so a second concurrent wait is a caller bug.
|
|
176
|
+
if (entry.parked) return Promise.reject(new Error(CONCURRENT_WAIT_MESSAGE));
|
|
162
177
|
if (entry.pending) {
|
|
163
178
|
const reason = entry.pending;
|
|
164
179
|
entry.pending = undefined;
|
|
@@ -166,14 +181,24 @@ export class SupervisionCoordinator {
|
|
|
166
181
|
}
|
|
167
182
|
if (entry.fallback) return this.poll(signal);
|
|
168
183
|
return new Promise((resolve, reject) => {
|
|
184
|
+
// Polling keeps its own timer referenced. A wake wait has only this
|
|
185
|
+
// promise, so retain the unref'd directory watch until it settles.
|
|
186
|
+
const release = this.wakeRegistry.retain();
|
|
187
|
+
let settled = false;
|
|
188
|
+
const finish = (settle: () => void) => {
|
|
189
|
+
if (settled) return;
|
|
190
|
+
settled = true;
|
|
191
|
+
signal.removeEventListener("abort", onAbort);
|
|
192
|
+
entry.parked = undefined;
|
|
193
|
+
release();
|
|
194
|
+
settle();
|
|
195
|
+
};
|
|
169
196
|
const onAbort = () => {
|
|
170
|
-
|
|
171
|
-
reject(new Error("Aborted while waiting for subagent to finish"));
|
|
197
|
+
finish(() => reject(new Error(ABORT_MESSAGE)));
|
|
172
198
|
};
|
|
173
|
-
entry.
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
resolve(reason);
|
|
199
|
+
entry.parked = {
|
|
200
|
+
resolve: (reason) => finish(() => resolve(reason)),
|
|
201
|
+
reject: (error) => finish(() => reject(error)),
|
|
177
202
|
};
|
|
178
203
|
signal.addEventListener("abort", onAbort, { once: true });
|
|
179
204
|
});
|
|
@@ -187,7 +212,7 @@ export class SupervisionCoordinator {
|
|
|
187
212
|
}, POLLING_INTERVAL_MS);
|
|
188
213
|
const onAbort = () => {
|
|
189
214
|
clearTimeout(timer);
|
|
190
|
-
reject(new Error(
|
|
215
|
+
reject(new Error(ABORT_MESSAGE));
|
|
191
216
|
};
|
|
192
217
|
signal.addEventListener("abort", onAbort, { once: true });
|
|
193
218
|
});
|
|
@@ -196,7 +221,7 @@ export class SupervisionCoordinator {
|
|
|
196
221
|
private signal(entry: Entry, reason: WakeReason): void {
|
|
197
222
|
if (this.closed) return;
|
|
198
223
|
if (reason === "wake") entry.inspection = undefined;
|
|
199
|
-
if (entry.
|
|
224
|
+
if (entry.parked) entry.parked.resolve(reason);
|
|
200
225
|
// A wake must never downgrade a queued reconciliation: the reconcile
|
|
201
226
|
// reason carries the epoch's pane inspection, and skipping it would
|
|
202
227
|
// defer pane-disappearance detection past the documented 5s bound.
|
|
@@ -6,6 +6,10 @@
|
|
|
6
6
|
* Methods may be sync or async; providers choose. The CLI is the only place
|
|
7
7
|
* that wires a provider to an adapter (via core's SurfaceHandle).
|
|
8
8
|
*/
|
|
9
|
+
import type {
|
|
10
|
+
OpenedPrimaryWorkspaceReport,
|
|
11
|
+
PrimaryWorkspaceClaim,
|
|
12
|
+
} from "./opened-primary-workspace.ts";
|
|
9
13
|
import type { PaneInspection, SurfaceHandle } from "./types.ts";
|
|
10
14
|
|
|
11
15
|
// pi-herdr-agents extension
|
|
@@ -48,6 +52,13 @@ export interface WorktreeSurface {
|
|
|
48
52
|
/** Provider-native workspace/container id for later removal. */
|
|
49
53
|
workspaceId: string;
|
|
50
54
|
surfaceId: string;
|
|
55
|
+
/** Non-fatal provisioning problems the parent should see, such as a failed snapshot. */
|
|
56
|
+
diagnostics?: string[];
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface ReportOpenedPrimaryWorkspaceInput {
|
|
60
|
+
sourceRepo: string;
|
|
61
|
+
claims: readonly PrimaryWorkspaceClaim[];
|
|
51
62
|
}
|
|
52
63
|
|
|
53
64
|
export type RecoveredWorktreeProvisioning = Pick<
|
|
@@ -135,6 +146,17 @@ export interface SurfaceProvider {
|
|
|
135
146
|
opts?: { timeoutMs?: number },
|
|
136
147
|
): void | Promise<void>;
|
|
137
148
|
|
|
149
|
+
/**
|
|
150
|
+
* Suggest closing a claimed primary workspace that still looks untouched.
|
|
151
|
+
* Never closes it. Unsupported providers return undefined.
|
|
152
|
+
*/
|
|
153
|
+
reportOpenedPrimaryWorkspace(
|
|
154
|
+
input: ReportOpenedPrimaryWorkspaceInput,
|
|
155
|
+
):
|
|
156
|
+
| OpenedPrimaryWorkspaceReport
|
|
157
|
+
| undefined
|
|
158
|
+
| Promise<OpenedPrimaryWorkspaceReport | undefined>;
|
|
159
|
+
|
|
138
160
|
/** Human-readable hint when isAvailable() is false; today's terminalSetupHint(). */
|
|
139
161
|
// pi-herdr-agents extension
|
|
140
162
|
setupHint(): string;
|
package/maestro/core/wake.ts
CHANGED
|
@@ -27,6 +27,7 @@ export class FileWakeRegistry {
|
|
|
27
27
|
private readonly directories = new Map<string, DirectoryWatcher>();
|
|
28
28
|
private readonly watchDirectory: typeof watch;
|
|
29
29
|
private closed = false;
|
|
30
|
+
private retained = 0;
|
|
30
31
|
|
|
31
32
|
constructor(watchDirectory: typeof watch = watch) {
|
|
32
33
|
this.watchDirectory = watchDirectory;
|
|
@@ -57,7 +58,10 @@ export class FileWakeRegistry {
|
|
|
57
58
|
}
|
|
58
59
|
});
|
|
59
60
|
current = { watcher, entries: new Set() };
|
|
61
|
+
// Idle watches must not keep the process running. A parked waiter
|
|
62
|
+
// retains them so this callback can still run.
|
|
60
63
|
watcher.unref();
|
|
64
|
+
if (this.retained > 0) watcher.ref();
|
|
61
65
|
watcher.on("error", () => this.failDirectory(directory));
|
|
62
66
|
this.directories.set(directory, current);
|
|
63
67
|
} catch {
|
|
@@ -85,12 +89,45 @@ export class FileWakeRegistry {
|
|
|
85
89
|
return this.directories.size;
|
|
86
90
|
}
|
|
87
91
|
|
|
92
|
+
/** Outstanding retains; every directory watch is referenced while nonzero. */
|
|
93
|
+
get retainedWaits(): number {
|
|
94
|
+
return this.retained;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Pin every active directory watch until the returned function runs.
|
|
99
|
+
* A bare wake promise does not keep the event loop alive, and idle watches
|
|
100
|
+
* are unref'd, so without this the callback never runs when that promise is
|
|
101
|
+
* the only pending work.
|
|
102
|
+
*/
|
|
103
|
+
retain(): () => void {
|
|
104
|
+
if (this.closed) return () => {};
|
|
105
|
+
this.retained++;
|
|
106
|
+
if (this.retained === 1) this.applyWatcherRef(true);
|
|
107
|
+
let released = false;
|
|
108
|
+
return () => {
|
|
109
|
+
if (released) return;
|
|
110
|
+
released = true;
|
|
111
|
+
if (this.closed || this.retained === 0) return;
|
|
112
|
+
this.retained--;
|
|
113
|
+
if (this.retained === 0) this.applyWatcherRef(false);
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
88
117
|
close(): void {
|
|
89
118
|
this.closed = true;
|
|
119
|
+
this.retained = 0;
|
|
90
120
|
for (const { watcher } of this.directories.values()) watcher.close();
|
|
91
121
|
this.directories.clear();
|
|
92
122
|
}
|
|
93
123
|
|
|
124
|
+
private applyWatcherRef(active: boolean): void {
|
|
125
|
+
for (const { watcher } of this.directories.values()) {
|
|
126
|
+
if (active) watcher.ref();
|
|
127
|
+
else watcher.unref();
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
94
131
|
private failDirectory(directory: string): void {
|
|
95
132
|
if (this.closed) return;
|
|
96
133
|
const current = this.directories.get(directory);
|
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import { isAbsolute, relative, sep } from "node:path";
|
|
2
2
|
import { isString, type JsonObject } from "./config/type-guards.ts";
|
|
3
|
+
import {
|
|
4
|
+
forgetOpenedPrimaryWorkspaceClaims,
|
|
5
|
+
openedPrimaryWorkspaceClaims,
|
|
6
|
+
type OpenedPrimaryWorkspaceReport,
|
|
7
|
+
type PrimaryWorkspaceClaim,
|
|
8
|
+
} from "./opened-primary-workspace.ts";
|
|
3
9
|
import type { WorktreeSurfaceInfo } from "./surface-provider.ts";
|
|
4
10
|
|
|
5
11
|
export interface CleanupGitState {
|
|
@@ -53,6 +59,17 @@ export interface WorktreeCleanupOperations {
|
|
|
53
59
|
exists(path: string): boolean;
|
|
54
60
|
preserve(entry: WorktreeInventoryEntry): string;
|
|
55
61
|
removeWorkspace(id: string): void | Promise<void>;
|
|
62
|
+
/**
|
|
63
|
+
* Suggest, never close, a primary workspace this process's worktree
|
|
64
|
+
* creation appears to have opened. Undefined means the surface cannot report.
|
|
65
|
+
*/
|
|
66
|
+
reportOpenedPrimaryWorkspace(
|
|
67
|
+
sourceRepo: string,
|
|
68
|
+
claims: readonly PrimaryWorkspaceClaim[],
|
|
69
|
+
):
|
|
70
|
+
| OpenedPrimaryWorkspaceReport
|
|
71
|
+
| undefined
|
|
72
|
+
| Promise<OpenedPrimaryWorkspaceReport | undefined>;
|
|
56
73
|
removeCheckout(sourceRepo: string, path: string): void | Promise<void>;
|
|
57
74
|
prune(sourceRepo: string): void;
|
|
58
75
|
writeManifest(file: string, value: JsonObject): void;
|
|
@@ -80,6 +97,21 @@ function contained(root: string, path: string): boolean {
|
|
|
80
97
|
);
|
|
81
98
|
}
|
|
82
99
|
|
|
100
|
+
function claimsForSource(
|
|
101
|
+
claims: readonly PrimaryWorkspaceClaim[],
|
|
102
|
+
sourceRepo: string,
|
|
103
|
+
ops: WorktreeCleanupOperations,
|
|
104
|
+
): PrimaryWorkspaceClaim[] {
|
|
105
|
+
return claims.filter((claim) => {
|
|
106
|
+
if (claim.checkoutPath === sourceRepo) return true;
|
|
107
|
+
try {
|
|
108
|
+
return ops.realpath(claim.checkoutPath) === sourceRepo;
|
|
109
|
+
} catch {
|
|
110
|
+
return false;
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
|
|
83
115
|
export function cleanupBlockers(entry: WorktreeInventoryEntry): string[] {
|
|
84
116
|
const git = entry.git;
|
|
85
117
|
const blockers: string[] = [];
|
|
@@ -345,6 +377,25 @@ export async function removeContainedWorktree(
|
|
|
345
377
|
checkoutRemoved = true;
|
|
346
378
|
if (!entry.workspaceId) ops.prune(entry.sourceRepo);
|
|
347
379
|
const warnings: string[] = [];
|
|
380
|
+
let primaryNote = "";
|
|
381
|
+
const claims = claimsForSource(
|
|
382
|
+
openedPrimaryWorkspaceClaims(),
|
|
383
|
+
entry.sourceRepo,
|
|
384
|
+
ops,
|
|
385
|
+
);
|
|
386
|
+
if (claims.length) {
|
|
387
|
+
try {
|
|
388
|
+
const report = await ops.reportOpenedPrimaryWorkspace(
|
|
389
|
+
entry.sourceRepo,
|
|
390
|
+
claims,
|
|
391
|
+
);
|
|
392
|
+
if (report?.note) primaryNote = ` ${report.note}`;
|
|
393
|
+
if (report?.releasedClaims.length)
|
|
394
|
+
forgetOpenedPrimaryWorkspaceClaims(report.releasedClaims);
|
|
395
|
+
} catch (error) {
|
|
396
|
+
warnings.push(`Primary workspace report failed: ${message(error)}`);
|
|
397
|
+
}
|
|
398
|
+
}
|
|
348
399
|
for (const manifest of entry.manifest) {
|
|
349
400
|
try {
|
|
350
401
|
ops.writeManifest(manifest.file, {
|
|
@@ -361,7 +412,7 @@ export async function removeContainedWorktree(
|
|
|
361
412
|
status: "removed",
|
|
362
413
|
entry,
|
|
363
414
|
preservationSha,
|
|
364
|
-
message: `Removed ${entry.path}. Branch ${entry.branch} and its commits retained.${preservationSha ? ` Preservation commit: ${preservationSha}.` : ""}${ignoredNotice()}${warnings.length ? ` Warning: ${warnings.join("; ")}` : entry.manifest.length ? " Manifest marked removed." : " No reachable manifest (orphan)."}`,
|
|
415
|
+
message: `Removed ${entry.path}. Branch ${entry.branch} and its commits retained.${preservationSha ? ` Preservation commit: ${preservationSha}.` : ""}${ignoredNotice()}${primaryNote}${warnings.length ? ` Warning: ${warnings.join("; ")}` : entry.manifest.length ? " Manifest marked removed." : " No reachable manifest (orphan)."}`,
|
|
365
416
|
});
|
|
366
417
|
} catch (error) {
|
|
367
418
|
return finish({
|
package/maestro/core/worktree.ts
CHANGED
|
@@ -11,6 +11,8 @@ export interface WorktreeLaunch {
|
|
|
11
11
|
sessionFile?: string;
|
|
12
12
|
sourceSessionFile?: string;
|
|
13
13
|
handoffMessage?: string;
|
|
14
|
+
/** Non-fatal provisioning problems reported with the launch. */
|
|
15
|
+
diagnostics?: string[];
|
|
14
16
|
}
|
|
15
17
|
|
|
16
18
|
export interface FailedWorktreeManifest extends JsonObject {
|
package/maestro/runtime/index.ts
CHANGED
|
@@ -12,7 +12,8 @@ export type {
|
|
|
12
12
|
RuntimeCandidate,
|
|
13
13
|
} from "./run-session.ts";
|
|
14
14
|
export { defaultRetainSurface } from "./surface-retention.ts";
|
|
15
|
-
export {
|
|
15
|
+
export { startTaskModelInit } from "./task-model-init.ts";
|
|
16
|
+
export type { TaskModelInitOutcome } from "./task-model-init.ts";
|
|
16
17
|
export {
|
|
17
18
|
createDefaultRunSession,
|
|
18
19
|
observePiActivity,
|
|
@@ -1,22 +1,119 @@
|
|
|
1
1
|
import {
|
|
2
2
|
buildTaskModelBrief,
|
|
3
3
|
buildTaskModelInitPrompt,
|
|
4
|
+
collectApprovalOffers,
|
|
5
|
+
DIRECT_WRITER_DESTINATION,
|
|
6
|
+
type NonEmpty,
|
|
7
|
+
type OfferCallback,
|
|
8
|
+
openApprovalOffer,
|
|
9
|
+
selectApprovalRoute,
|
|
10
|
+
type TaskModelBrief,
|
|
11
|
+
type TaskModelFacts,
|
|
12
|
+
type TaskModelInitDestination,
|
|
4
13
|
type TaskModelRegistryProjector,
|
|
5
14
|
type TaskModelRegistrySnapshot,
|
|
6
15
|
} from "../core/config/task-model-init.ts";
|
|
7
|
-
import type {
|
|
16
|
+
import type { ModelConfigSnapshot } from "../core/config/model-config.ts";
|
|
8
17
|
import { projectTaskModelRegistry } from "../adapters/pi/task-model-init.ts";
|
|
9
18
|
|
|
10
|
-
/**
|
|
11
|
-
export
|
|
19
|
+
/** What one init request did. Only `started` submitted a prompt. */
|
|
20
|
+
export type TaskModelInitOutcome =
|
|
21
|
+
| { kind: "started"; destination: string }
|
|
22
|
+
| { kind: "not-started"; reason: string };
|
|
23
|
+
|
|
24
|
+
export interface TaskModelInitInput {
|
|
25
|
+
preferences: string;
|
|
26
|
+
/** Saved settings and the revision of the same bytes; throws when invalid. */
|
|
27
|
+
readConfig(): ModelConfigSnapshot;
|
|
12
28
|
/** The host supplies its actual active registry to this projector exactly once. */
|
|
13
29
|
projectActiveRegistry(
|
|
14
30
|
project: TaskModelRegistryProjector,
|
|
15
31
|
): TaskModelRegistrySnapshot;
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
32
|
+
/** Emits the approval request for this brief and returns when listeners did. */
|
|
33
|
+
emitApprovalRequest(brief: TaskModelBrief, offer: OfferCallback): void;
|
|
34
|
+
isToolActive(name: string): boolean;
|
|
35
|
+
/** Hands the prompt to Pi; a throw means Pi did not take it. */
|
|
36
|
+
submit(prompt: string): void;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Listeners share the brief; freezing it keeps the prompt equal to what they saw. */
|
|
40
|
+
function deepFreeze<T>(value: T): T {
|
|
41
|
+
if (value == null || Object(value) !== value) return value;
|
|
42
|
+
// SAFETY: Object(value) === value holds only for objects and functions.
|
|
43
|
+
for (const item of Object.values(value as object)) deepFreeze(item);
|
|
44
|
+
return Object.freeze(value);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function isNonEmpty<T>(values: T[]): values is NonEmpty<T> {
|
|
48
|
+
return values.length > 0;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Capture one brief, pick where its proposal goes, and submit the prompt. It
|
|
53
|
+
* runs synchronously, so nothing can change between selection and submission.
|
|
54
|
+
* An opened offer that is not submitted is cancelled before this returns.
|
|
55
|
+
*/
|
|
56
|
+
export function startTaskModelInit(
|
|
57
|
+
input: TaskModelInitInput,
|
|
58
|
+
): TaskModelInitOutcome {
|
|
59
|
+
const notStarted = (reason: string): TaskModelInitOutcome => ({
|
|
60
|
+
kind: "not-started",
|
|
61
|
+
reason,
|
|
62
|
+
});
|
|
63
|
+
let snapshot: ModelConfigSnapshot;
|
|
64
|
+
try {
|
|
65
|
+
snapshot = input.readConfig();
|
|
66
|
+
} catch (error) {
|
|
67
|
+
return notStarted(error instanceof Error ? error.message : String(error));
|
|
68
|
+
}
|
|
69
|
+
const { models } = input.projectActiveRegistry(projectTaskModelRegistry);
|
|
70
|
+
if (!isNonEmpty<TaskModelFacts>(models))
|
|
71
|
+
return notStarted(
|
|
72
|
+
"the active registry has no authenticated models, so there is nothing to rank. Configure a provider, then run /subagents-init again",
|
|
73
|
+
);
|
|
74
|
+
const brief = deepFreeze(
|
|
75
|
+
buildTaskModelBrief({
|
|
76
|
+
models,
|
|
77
|
+
current: snapshot.config,
|
|
78
|
+
configRevision: snapshot.revision,
|
|
79
|
+
preferences: input.preferences,
|
|
80
|
+
}),
|
|
81
|
+
);
|
|
82
|
+
const route = selectApprovalRoute(
|
|
83
|
+
collectApprovalOffers((offer) => input.emitApprovalRequest(brief, offer)),
|
|
84
|
+
);
|
|
85
|
+
let destination: TaskModelInitDestination;
|
|
86
|
+
let cancel: () => string | undefined = () => undefined;
|
|
87
|
+
switch (route.kind) {
|
|
88
|
+
case "direct":
|
|
89
|
+
destination = DIRECT_WRITER_DESTINATION;
|
|
90
|
+
break;
|
|
91
|
+
case "refused":
|
|
92
|
+
return notStarted(route.reason);
|
|
93
|
+
case "offer": {
|
|
94
|
+
const opened = openApprovalOffer(route.offer);
|
|
95
|
+
if (opened.kind === "blocked") return notStarted(opened.reason);
|
|
96
|
+
destination = opened.destination;
|
|
97
|
+
cancel = opened.cancel;
|
|
98
|
+
break;
|
|
99
|
+
}
|
|
100
|
+
default: {
|
|
101
|
+
const _exhaustive: never = route;
|
|
102
|
+
return _exhaustive;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
const cancelled = (reason: string): TaskModelInitOutcome => {
|
|
106
|
+
const problem = cancel();
|
|
107
|
+
return notStarted(problem ? `${reason}; ${problem}` : reason);
|
|
108
|
+
};
|
|
109
|
+
if (!input.isToolActive(destination.toolName))
|
|
110
|
+
return cancelled(`${destination.toolName} is not an active tool`);
|
|
111
|
+
try {
|
|
112
|
+
input.submit(buildTaskModelInitPrompt(brief, destination));
|
|
113
|
+
} catch (error) {
|
|
114
|
+
return cancelled(
|
|
115
|
+
`Pi did not accept the init prompt (${error instanceof Error ? error.message : String(error)})`,
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
return { kind: "started", destination: destination.toolName };
|
|
22
119
|
}
|
|
@@ -612,6 +612,8 @@ export function createWorktreeCleanupOperations(
|
|
|
612
612
|
},
|
|
613
613
|
removeWorkspace: (id) =>
|
|
614
614
|
provider.removeWorktreeSurface(id, { timeoutMs: CLEANUP_TIMEOUT_MS }),
|
|
615
|
+
reportOpenedPrimaryWorkspace: (sourceRepo, claims) =>
|
|
616
|
+
provider.reportOpenedPrimaryWorkspace({ sourceRepo, claims }),
|
|
615
617
|
removeCheckout: (source, path) => {
|
|
616
618
|
git(source, ["worktree", "remove", "--", path]);
|
|
617
619
|
},
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
|
+
import type { OpenedPrimaryWorkspaceReport } from "../../core/opened-primary-workspace.ts";
|
|
2
3
|
import type {
|
|
3
4
|
CreateSurfaceOptions,
|
|
4
5
|
CreateWorktreeSurfaceOptions,
|
|
6
|
+
ReportOpenedPrimaryWorkspaceInput,
|
|
5
7
|
SurfaceInfo,
|
|
6
8
|
SurfaceProcessInfo,
|
|
7
9
|
SurfaceProvider,
|
|
@@ -147,6 +149,12 @@ export class FakeSurfaceProvider implements SurfaceProvider {
|
|
|
147
149
|
this.removeSurface(worktree.surfaceId);
|
|
148
150
|
}
|
|
149
151
|
|
|
152
|
+
reportOpenedPrimaryWorkspace(
|
|
153
|
+
_input: ReportOpenedPrimaryWorkspaceInput,
|
|
154
|
+
): OpenedPrimaryWorkspaceReport | undefined {
|
|
155
|
+
return undefined;
|
|
156
|
+
}
|
|
157
|
+
|
|
150
158
|
setupHint(): string {
|
|
151
159
|
return "Fake surface provider is always available in memory.";
|
|
152
160
|
}
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import type {
|
|
2
2
|
CreateSurfaceOptions,
|
|
3
3
|
CreateWorktreeSurfaceOptions,
|
|
4
|
+
ReportOpenedPrimaryWorkspaceInput,
|
|
4
5
|
SurfaceInfo,
|
|
5
6
|
SurfaceProcessInfo,
|
|
6
7
|
SurfaceProvider,
|
|
7
8
|
WorktreeSurface,
|
|
8
9
|
WorktreeSurfaceInfo,
|
|
9
10
|
} from "../../core/surface-provider.ts";
|
|
11
|
+
import type { OpenedPrimaryWorkspaceReport } from "../../core/opened-primary-workspace.ts";
|
|
10
12
|
import type { PaneInspection, SurfaceHandle } from "../../core/types.ts";
|
|
11
13
|
import type { PaneConfig } from "../../core/config/pane-config.ts";
|
|
12
14
|
import {
|
|
@@ -16,6 +18,7 @@ import {
|
|
|
16
18
|
createHerdrSurfaceSplit,
|
|
17
19
|
createHerdrWorktree,
|
|
18
20
|
focusHerdrWorkspace,
|
|
21
|
+
reportOpenedPrimaryWorkspace as reportHerdrOpenedPrimaryWorkspace,
|
|
19
22
|
getHerdrPaneProcessInfo,
|
|
20
23
|
getHerdrPaneProcessInfoAsync,
|
|
21
24
|
inspectHerdrPane,
|
|
@@ -111,12 +114,20 @@ export class HerdrSurfaceProvider implements SurfaceProvider {
|
|
|
111
114
|
opts.branch,
|
|
112
115
|
opts.base,
|
|
113
116
|
);
|
|
114
|
-
|
|
117
|
+
const surface: WorktreeSurface = {
|
|
115
118
|
path: worktree.path,
|
|
116
119
|
branch: worktree.branch,
|
|
117
120
|
workspaceId: worktree.workspaceId,
|
|
118
121
|
surfaceId: worktree.paneId,
|
|
119
122
|
};
|
|
123
|
+
if (worktree.diagnostics) surface.diagnostics = worktree.diagnostics;
|
|
124
|
+
return surface;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
reportOpenedPrimaryWorkspace(
|
|
128
|
+
input: ReportOpenedPrimaryWorkspaceInput,
|
|
129
|
+
): Promise<OpenedPrimaryWorkspaceReport | undefined> {
|
|
130
|
+
return reportHerdrOpenedPrimaryWorkspace(input.sourceRepo, input.claims);
|
|
120
131
|
}
|
|
121
132
|
|
|
122
133
|
removeWorktreeSurface(
|