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
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type {
|
|
2
2
|
AgentToolResult,
|
|
3
3
|
ExtensionAPI,
|
|
4
|
+
ExtensionCommandContext,
|
|
4
5
|
ExtensionContext,
|
|
5
6
|
SessionShutdownEvent,
|
|
6
7
|
} from "@earendil-works/pi-coding-agent";
|
|
@@ -19,8 +20,9 @@ import { randomUUID } from "node:crypto";
|
|
|
19
20
|
import type { RunCancellation, Task } from "../../maestro/core/types.ts";
|
|
20
21
|
import {
|
|
21
22
|
createDefaultRunSession,
|
|
22
|
-
|
|
23
|
+
startTaskModelInit,
|
|
23
24
|
observePiActivity,
|
|
25
|
+
type TaskModelInitOutcome,
|
|
24
26
|
type PiRunSession,
|
|
25
27
|
type PiRunSessionInfrastructure,
|
|
26
28
|
type PiRunRecord,
|
|
@@ -60,9 +62,17 @@ import { wrapPiModelRegistry } from "./model-registry.ts";
|
|
|
60
62
|
import {
|
|
61
63
|
loadModelConfig,
|
|
62
64
|
MISSING_CONFIG_REVISION,
|
|
65
|
+
parseRankingBasis,
|
|
66
|
+
readModelConfigSnapshot,
|
|
63
67
|
resolveModelDefault,
|
|
64
68
|
writeTaskModelConfig,
|
|
65
69
|
} from "../../maestro/core/config/model-config.ts";
|
|
70
|
+
import {
|
|
71
|
+
type OfferCallback,
|
|
72
|
+
type TaskModelBrief,
|
|
73
|
+
TASK_MODEL_INIT_APPROVAL_EVENT,
|
|
74
|
+
TASK_MODEL_INIT_START_EVENT,
|
|
75
|
+
} from "../../maestro/core/config/task-model-init.ts";
|
|
66
76
|
import {
|
|
67
77
|
TASK_CATEGORIES,
|
|
68
78
|
TASK_CATEGORY_DESCRIPTIONS,
|
|
@@ -98,6 +108,7 @@ import { isSubagentActivityScope } from "../../maestro/core/activity.ts";
|
|
|
98
108
|
import type { SubagentActivityState } from "../../maestro/core/types.ts";
|
|
99
109
|
import {
|
|
100
110
|
isFiniteNumber,
|
|
111
|
+
isFunction,
|
|
101
112
|
isPlainObject,
|
|
102
113
|
isString,
|
|
103
114
|
} from "../../maestro/core/config/type-guards.ts";
|
|
@@ -344,6 +355,47 @@ function discoverAgentDefinitions(
|
|
|
344
355
|
return discoverAgentCatalog(pi).agents;
|
|
345
356
|
}
|
|
346
357
|
|
|
358
|
+
/**
|
|
359
|
+
* Payload of TASK_MODEL_INIT_APPROVAL_EVENT. `context` is the live context of
|
|
360
|
+
* the command that started init. It passes between trusted in-process
|
|
361
|
+
* extensions and is current only while the request is emitted.
|
|
362
|
+
*/
|
|
363
|
+
interface TaskModelApprovalRequest {
|
|
364
|
+
apiVersion: 1;
|
|
365
|
+
brief: TaskModelBrief;
|
|
366
|
+
context: ExtensionCommandContext;
|
|
367
|
+
offer: OfferCallback;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
function hasMethods(value: any, names: readonly string[]): boolean {
|
|
371
|
+
return value != null && names.every((name) => isFunction(value[name]));
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* Read a TASK_MODEL_INIT_START_EVENT v1 payload. A v1 request whose context or
|
|
376
|
+
* preferences cannot run init still gets an offer, so its emitter reports why
|
|
377
|
+
* instead of concluding that no host is loaded.
|
|
378
|
+
*/
|
|
379
|
+
function parseInitStartRequest(raw: any):
|
|
380
|
+
| {
|
|
381
|
+
offer: OfferCallback;
|
|
382
|
+
input?: { context: ExtensionCommandContext; preferences: string };
|
|
383
|
+
}
|
|
384
|
+
| undefined {
|
|
385
|
+
if (raw?.apiVersion !== 1 || !isFunction(raw.offer)) return undefined;
|
|
386
|
+
const { offer, context, preferences } = raw;
|
|
387
|
+
const usable =
|
|
388
|
+
isString(preferences) &&
|
|
389
|
+
hasMethods(context, ["isIdle", "hasPendingMessages"]) &&
|
|
390
|
+
hasMethods(context.modelRegistry, ["getAvailable"]);
|
|
391
|
+
return {
|
|
392
|
+
offer: (value) => offer.call(raw, value),
|
|
393
|
+
// The members init reads were checked above; the rest is the emitting
|
|
394
|
+
// command's own live context.
|
|
395
|
+
input: usable ? { context, preferences } : undefined,
|
|
396
|
+
};
|
|
397
|
+
}
|
|
398
|
+
|
|
347
399
|
function missingRoleMessage(agentName: string): string {
|
|
348
400
|
return `Agent "${agentName}" was not found. pi-herdr-agents ships no roles: define it in .pi/agents or the global agents directory, install a role pack that provides it, or omit agent for a bare launch.`;
|
|
349
401
|
}
|
|
@@ -684,6 +736,16 @@ function formatWorktreeHandoff(worktree: WorktreeHandoff): string {
|
|
|
684
736
|
return lines.join("\n");
|
|
685
737
|
}
|
|
686
738
|
|
|
739
|
+
function launchDiagnosticsText(
|
|
740
|
+
diagnostics: readonly string[] | undefined,
|
|
741
|
+
prefix: string,
|
|
742
|
+
suffix: string,
|
|
743
|
+
): string {
|
|
744
|
+
return diagnostics?.length
|
|
745
|
+
? `${prefix}Launch diagnostics: ${diagnostics.join("; ")}.${suffix}`
|
|
746
|
+
: "";
|
|
747
|
+
}
|
|
748
|
+
|
|
687
749
|
function resolveResultPresentation(
|
|
688
750
|
result: Pick<
|
|
689
751
|
SubagentResult,
|
|
@@ -2606,7 +2668,7 @@ export default function subagentsExtension(
|
|
|
2606
2668
|
pi.registerTool({
|
|
2607
2669
|
name: "subagents_write_task_models",
|
|
2608
2670
|
label: "Write task model preferences",
|
|
2609
|
-
description: `Validate and atomically replace models.tasks and models.tasksMeta in the durable Pi agent config, preserving unrelated settings. Supported categories: ${TASK_CATEGORIES.join(", ")}. Partial nonempty categories are accepted; omitted categories are removed. Rejects duplicate exact refs within a category. Review the active authenticated registry and existing preferences first. Optional expectedConfigRevision makes the write conditional: it fails without replacing configuration when the config file no longer matches the revision the proposal was read from. Cooperating writes are serialized and fail on contention rather than waiting. Returns normalized saved preferences, missing categories,
|
|
2671
|
+
description: `Validate and atomically replace models.tasks and models.tasksMeta in the durable Pi agent config, preserving unrelated settings. Supported categories: ${TASK_CATEGORIES.join(", ")}. Partial nonempty categories are accepted; omitted categories are removed. Rejects duplicate exact refs within a category. Review the active authenticated registry and existing preferences first. Optional expectedConfigRevision makes the write conditional: it fails without replacing configuration when the config file no longer matches the revision the proposal was read from. Optional basis records what the ranking rests on; its kind must equal tasksMeta.method, and it is returned but not saved. Cooperating writes are serialized and fail on contention rather than waiting. Returns normalized saved preferences, missing categories, the configRevision of the written file, and any basis; reload required.`,
|
|
2610
2672
|
parameters: Type.Object({
|
|
2611
2673
|
tasks: Type.Object(
|
|
2612
2674
|
Object.fromEntries(
|
|
@@ -2640,6 +2702,37 @@ export default function subagentsExtension(
|
|
|
2640
2702
|
},
|
|
2641
2703
|
),
|
|
2642
2704
|
),
|
|
2705
|
+
basis: Type.Optional(
|
|
2706
|
+
Type.Union(
|
|
2707
|
+
[
|
|
2708
|
+
Type.Object(
|
|
2709
|
+
{ kind: Type.Literal("registry-only") },
|
|
2710
|
+
{ additionalProperties: false },
|
|
2711
|
+
),
|
|
2712
|
+
Type.Object(
|
|
2713
|
+
{
|
|
2714
|
+
kind: Type.Literal("research"),
|
|
2715
|
+
sources: Type.Array(
|
|
2716
|
+
Type.Object(
|
|
2717
|
+
{
|
|
2718
|
+
url: Type.String({ minLength: 1 }),
|
|
2719
|
+
influence: Type.String({ minLength: 1 }),
|
|
2720
|
+
},
|
|
2721
|
+
{ additionalProperties: false },
|
|
2722
|
+
),
|
|
2723
|
+
{ minItems: 1 },
|
|
2724
|
+
),
|
|
2725
|
+
uncertainty: Type.String({ minLength: 1 }),
|
|
2726
|
+
},
|
|
2727
|
+
{ additionalProperties: false },
|
|
2728
|
+
),
|
|
2729
|
+
],
|
|
2730
|
+
{
|
|
2731
|
+
description:
|
|
2732
|
+
'What the ranking rests on, as submitted by the proposer and not independently verified: {"kind":"registry-only"}, or {"kind":"research"} with the http(s) sources consulted in this run, how each informed the ranking, and the remaining uncertainty. kind must equal tasksMeta.method. Not saved. Omit for a legacy write.',
|
|
2733
|
+
},
|
|
2734
|
+
),
|
|
2735
|
+
),
|
|
2643
2736
|
}),
|
|
2644
2737
|
execute: async (_id, params, _signal, _update, ctx) => {
|
|
2645
2738
|
const registry = wrapPiModelRegistry(ctx.modelRegistry);
|
|
@@ -2647,6 +2740,10 @@ export default function subagentsExtension(
|
|
|
2647
2740
|
const tasks = params.tasks as TaskPreferences;
|
|
2648
2741
|
// SAFETY: TypeBox validates the tool payload; the write seam performs stricter schema validation.
|
|
2649
2742
|
const tasksMeta = params.tasksMeta as TaskPreferencesMeta;
|
|
2743
|
+
// Present-but-invalid values (including null) fail before anything is written.
|
|
2744
|
+
const basis = Object.hasOwn(params, "basis")
|
|
2745
|
+
? parseRankingBasis(params.basis, tasksMeta.method)
|
|
2746
|
+
: undefined;
|
|
2650
2747
|
const saved = writeTaskModelConfig(
|
|
2651
2748
|
getSubagentsConfigPath(),
|
|
2652
2749
|
getSubagentsConfigExamplePath(),
|
|
@@ -2663,14 +2760,15 @@ export default function subagentsExtension(
|
|
|
2663
2760
|
? { expectedConfigRevision: params.expectedConfigRevision }
|
|
2664
2761
|
: {},
|
|
2665
2762
|
);
|
|
2763
|
+
const details = basis ? { ...saved, basis } : saved;
|
|
2666
2764
|
return {
|
|
2667
2765
|
content: [
|
|
2668
2766
|
{
|
|
2669
2767
|
type: "text",
|
|
2670
|
-
text: `Wrote task model preferences. Reload required.\n${JSON.stringify(
|
|
2768
|
+
text: `Wrote task model preferences. Reload required.\n${JSON.stringify(details, null, 2)}`,
|
|
2671
2769
|
},
|
|
2672
2770
|
],
|
|
2673
|
-
details
|
|
2771
|
+
details,
|
|
2674
2772
|
};
|
|
2675
2773
|
},
|
|
2676
2774
|
});
|
|
@@ -2833,6 +2931,7 @@ export default function subagentsExtension(
|
|
|
2833
2931
|
(running.worktree
|
|
2834
2932
|
? ` in worktree ${running.worktree.path} on branch ${running.worktree.branch}. `
|
|
2835
2933
|
: ". ") +
|
|
2934
|
+
launchDiagnosticsText(running.worktree?.diagnostics, "", " ") +
|
|
2836
2935
|
`Do NOT generate or assume any results — you have no idea what the sub-agent will do or produce. ` +
|
|
2837
2936
|
`The results will be delivered to you automatically as a steer message when the sub-agent finishes. ` +
|
|
2838
2937
|
`Until then, move on to other work or tell the user you're waiting.`,
|
|
@@ -3316,21 +3415,73 @@ export default function subagentsExtension(
|
|
|
3316
3415
|
},
|
|
3317
3416
|
});
|
|
3318
3417
|
|
|
3319
|
-
if (!process.env.PI_SUBAGENT_ID)
|
|
3418
|
+
if (!process.env.PI_SUBAGENT_ID) {
|
|
3419
|
+
const runTaskModelInit = (
|
|
3420
|
+
ctx: ExtensionCommandContext,
|
|
3421
|
+
preferences: string,
|
|
3422
|
+
): TaskModelInitOutcome => {
|
|
3423
|
+
if (!ctx.isIdle() || ctx.hasPendingMessages())
|
|
3424
|
+
return {
|
|
3425
|
+
kind: "not-started",
|
|
3426
|
+
reason:
|
|
3427
|
+
"a turn is in progress or messages are queued. Nothing was queued; retry when idle",
|
|
3428
|
+
};
|
|
3429
|
+
// Pi refuses a prompt without a model only after submission; refuse first.
|
|
3430
|
+
if (!ctx.model)
|
|
3431
|
+
return {
|
|
3432
|
+
kind: "not-started",
|
|
3433
|
+
reason:
|
|
3434
|
+
"no model is selected, so Pi cannot run the init prompt. Select a model, then retry",
|
|
3435
|
+
};
|
|
3436
|
+
return startTaskModelInit({
|
|
3437
|
+
preferences,
|
|
3438
|
+
readConfig: () => readModelConfigSnapshot(getSubagentsConfigDir()),
|
|
3439
|
+
projectActiveRegistry: (project) => project(ctx.modelRegistry),
|
|
3440
|
+
emitApprovalRequest: (brief, offer) =>
|
|
3441
|
+
pi.events.emit(
|
|
3442
|
+
TASK_MODEL_INIT_APPROVAL_EVENT,
|
|
3443
|
+
Object.freeze({
|
|
3444
|
+
apiVersion: 1,
|
|
3445
|
+
brief,
|
|
3446
|
+
context: ctx,
|
|
3447
|
+
offer,
|
|
3448
|
+
} satisfies TaskModelApprovalRequest),
|
|
3449
|
+
),
|
|
3450
|
+
isToolActive: (name) => pi.getActiveTools().includes(name),
|
|
3451
|
+
submit: (prompt) => pi.sendUserMessage(prompt),
|
|
3452
|
+
});
|
|
3453
|
+
};
|
|
3320
3454
|
pi.registerCommand("subagents-init", {
|
|
3321
3455
|
description:
|
|
3322
3456
|
"Draft task-category model preferences from the live registry; optional arguments set ranking preferences",
|
|
3323
3457
|
handler: async (args, ctx) => {
|
|
3324
|
-
const
|
|
3325
|
-
|
|
3326
|
-
|
|
3327
|
-
|
|
3328
|
-
|
|
3329
|
-
|
|
3330
|
-
});
|
|
3331
|
-
pi.sendUserMessage(prompt);
|
|
3458
|
+
const outcome = runTaskModelInit(ctx, args);
|
|
3459
|
+
if (outcome.kind === "not-started")
|
|
3460
|
+
ctx.ui.notify(
|
|
3461
|
+
`Task-model init not started: ${outcome.reason}. Nothing was written.`,
|
|
3462
|
+
"warning",
|
|
3463
|
+
);
|
|
3332
3464
|
},
|
|
3333
3465
|
});
|
|
3466
|
+
// Another extension's command may start the same init; it renders the outcome.
|
|
3467
|
+
const stopInitStart = pi.events.on(TASK_MODEL_INIT_START_EVENT, (raw) => {
|
|
3468
|
+
const request = parseInitStartRequest(raw);
|
|
3469
|
+
if (!request) return;
|
|
3470
|
+
const { input } = request;
|
|
3471
|
+
request.offer({
|
|
3472
|
+
owner: "pi-herdr-agents",
|
|
3473
|
+
start: (): TaskModelInitOutcome =>
|
|
3474
|
+
input
|
|
3475
|
+
? runTaskModelInit(input.context, input.preferences)
|
|
3476
|
+
: {
|
|
3477
|
+
kind: "not-started",
|
|
3478
|
+
reason:
|
|
3479
|
+
"the start request lacks a usable command context or preferences text",
|
|
3480
|
+
},
|
|
3481
|
+
});
|
|
3482
|
+
});
|
|
3483
|
+
pi.on("session_shutdown", () => stopInitStart());
|
|
3484
|
+
}
|
|
3334
3485
|
|
|
3335
3486
|
pi.registerCommand("worktree", {
|
|
3336
3487
|
description: parentSession
|
|
@@ -3462,10 +3613,13 @@ export default function subagentsExtension(
|
|
|
3462
3613
|
throw new Error("Worktree handoff did not return worktree metadata");
|
|
3463
3614
|
}
|
|
3464
3615
|
ctx.ui.notify(
|
|
3465
|
-
result.focusError
|
|
3616
|
+
(result.focusError
|
|
3466
3617
|
? `Worktree launched, but workspace focus failed: ${result.focusError}\nWorktree: ${worktree.path}`
|
|
3467
|
-
: `Worktree launched in ${worktree.path} (workspace ${worktree.workspaceId})
|
|
3468
|
-
|
|
3618
|
+
: `Worktree launched in ${worktree.path} (workspace ${worktree.workspaceId}).`) +
|
|
3619
|
+
launchDiagnosticsText(worktree.diagnostics, "\n", ""),
|
|
3620
|
+
result.focusError || worktree.diagnostics?.length
|
|
3621
|
+
? "warning"
|
|
3622
|
+
: "info",
|
|
3469
3623
|
);
|
|
3470
3624
|
} catch (error) {
|
|
3471
3625
|
ctx.ui.notify(
|
|
@@ -210,7 +210,12 @@ independent writer.
|
|
|
210
210
|
blockers. Startup does not scan it.
|
|
211
211
|
- `worktree_remove({ target, preserve? })` / `/worktree remove <target>
|
|
212
212
|
[--preserve]` is explicit, parent-only, and needs separate authorization;
|
|
213
|
-
stopping or interrupting a child never authorizes cleanup.
|
|
213
|
+
stopping or interrupting a child never authorizes cleanup. Removal never
|
|
214
|
+
closes the source repository's primary workspace. If this process's create
|
|
215
|
+
appears to have opened that workspace and it still looks untouched, the
|
|
216
|
+
result suggests `herdr workspace close <id>` if you haven't used it. Another
|
|
217
|
+
session or a restarted process says nothing about it. Primary-workspace
|
|
218
|
+
snapshot failures at launch appear as launch diagnostics. Dirty work is
|
|
214
219
|
blocked unless committed first or `preserve: true` makes a WIP commit (SHA
|
|
215
220
|
reported). Eligibility is rechecked at removal and fails closed (unknown
|
|
216
221
|
state, live holder, untracked files, conflicts, detached HEAD, locks,
|
|
@@ -240,12 +245,19 @@ changes. Without a real file, only non-model settings fall back to the example.
|
|
|
240
245
|
| `roles.bundled` | Deprecated no-op; remove it |
|
|
241
246
|
|
|
242
247
|
Model values must be exact authenticated IDs. `/subagents-init [preferences]`
|
|
243
|
-
drafts task preferences from the live model registry and
|
|
244
|
-
`
|
|
245
|
-
`models.tasks` and `tasksMeta`,
|
|
248
|
+
drafts task preferences from the live model registry and the saved config's
|
|
249
|
+
`configRevision`, then saves them through `subagents_write_task_models`
|
|
250
|
+
(parent-only; validates, atomically replaces `models.tasks` and `tasksMeta`,
|
|
251
|
+
preserves other settings). When one loaded extension offers to approve the
|
|
252
|
+
write, the init prompt names that extension's tool instead; follow the prompt
|
|
253
|
+
and do not call the writer around it. Payload: `tasks`
|
|
246
254
|
(partial but nonempty categories; empty `tasks: {}` and duplicate refs within a
|
|
247
255
|
category are rejected) and required `tasksMeta`. Warning: categories you omit
|
|
248
256
|
are removed from the saved config, so send every category you want to keep.
|
|
257
|
+
Optional `basis` states what the ranking rests on and is not saved: use
|
|
258
|
+
`{"kind":"registry-only"}` unless sources consulted in this run informed it,
|
|
259
|
+
then `{"kind":"research","sources":[{"url":"https://...","influence":"how it changed the ranking"}],"uncertainty":"what remains unknown"}`.
|
|
260
|
+
Its kind must equal `tasksMeta.method`.
|
|
249
261
|
Optional `expectedConfigRevision` (`sha256:` plus 64 lowercase hex digits of the exact file bytes,
|
|
250
262
|
or `missing`) makes the write conditional; a stale revision fails with
|
|
251
263
|
`Stale task model config revision`: re-read, re-propose, re-approve, do not
|