@hcflabs/pi-subagent 28.3.3 → 28.4.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/README.md CHANGED
@@ -46,7 +46,7 @@ Commands, prompts, and UI are for you. Tools and the packaged skill are for Main
46
46
  | Direct status widget (`D`) | ui | Show direct workflows as a tree under `Main` with references, names, Role badges, status, routes, elapsed time, measured task tokens, cost, and tool activity. In fullscreen mode, a click on a working or finished worker row focuses that worker's verified Herdr pane. |
47
47
  | Isolated status widget (`I`) | ui | Show recorded task state, Role/model-class badges, and retained workspaces in the TUI. Readiness does not mean promotion. |
48
48
 
49
- `delegate_task`, `subagent_resume`, `subagent_stage`, `subagent_integrate`, and `subagent_abort` are model-only. Main must make these decisions visibly; `codemode` scripts can call only `subagent_status`.
49
+ `delegate_task`, `subagent_resume`, `subagent_stage`, `subagent_integrate`, and `subagent_abort` are model-only. Main must make these decisions visibly; `codemode` scripts can call only `subagent_status` and `subagent_repos`.
50
50
 
51
51
  ### Direct workflow
52
52
 
@@ -269,6 +269,8 @@ Role Markdown files in `~/.pi/agent/config/pi-subagent/` require frontmatter and
269
269
  | `mcps` | Optional exact global server names from `~/.pi/agent/mcp.json`, using native `mcpServers` format. Omitted or `[]` denies access to `mcp.json` servers. Servers that Role extensions register always connect. |
270
270
  | body | Required system instructions |
271
271
 
272
+ A Role's `modelClass` selects a Task Profile. When the profile has a primary and a fallback that resolve in Main, the worker selects the Task Profile Model `task/<profile>`. A retryable error on the primary then moves the worker to the fallback. The launch record names the primary route. A designated `model` uses no fallback.
273
+
272
274
  The package ships `implementer`, `reviewer`, and `scout`; same-name user Roles override built-ins. Roles define capabilities, not access or isolation. Unknown frontmatter keys invalidate that file and report its path and key. Other valid Roles load; duplicate names become unavailable. Delegation to an invalid file's claimed Role fails with that file's error. Inspect `/subagent roles`.
273
275
 
274
276
  The built-in reviewer answers exactly `PASS` when it finds no actionable correctness risk. Otherwise, each finding starts with a priority and a confidence (`high`, `medium`, or `low`), for example `[P1] confidence: high`. Then it gives file and line evidence, the trigger, the impact, and the smallest fix. Each finding blocks approval.
package/dist/index.d.ts CHANGED
@@ -73,6 +73,11 @@ export interface PreparedRoleLaunch extends ResolvedRoleLaunch {
73
73
  export interface CreateRoleLaunchInput {
74
74
  role: Role;
75
75
  route: ResolvedTaskRoute;
76
+ /**
77
+ * Task Profile of the route. When set, the child selects the Task Profile Model `task/<profile>`,
78
+ * so a retryable error on the primary route moves the child to the fallback route.
79
+ */
80
+ failoverProfile?: ProfileName;
76
81
  extensions?: readonly string[];
77
82
  tools?: readonly string[];
78
83
  env?: Readonly<Record<string, string>>;
package/dist/index.js CHANGED
@@ -6,7 +6,7 @@ import { createConfigStore, extensionConfigDir, field, formatConfigIssues, parse
6
6
  import { hasDisplayControlCharacters } from "./display-text.js";
7
7
  import { DEFAULT_MAX_TURNS } from "./ephemeral.js";
8
8
  import { loadRoleMcpConfig, parseRoleMcpAllowlist } from "./mcp-role.js";
9
- import { loadTaskModelsConfig, modelReference, orderedProfileRoutes, PROFILE_NAMES, resolveConfiguredTaskRoute, resolveTaskModelRoute, } from "@hcflabs/pi-task-models";
9
+ import { loadTaskModelsConfig, modelReference, orderedProfileRoutes, PROFILE_NAMES, resolveConfiguredTaskRoute, resolveTaskModelRoute, TASK_PROFILE_MODELS_EXTENSION, taskProfileModelReference, } from "@hcflabs/pi-task-models";
10
10
  export { DISPLAY_TEXT_CONTRACT, hasDisplayControlCharacters } from "./display-text.js";
11
11
  export { loadRoleMcpConfig, parseRoleMcpAllowlist } from "./mcp-role.js";
12
12
  export { addUsage, capEphemeralSubagentOutput, createEphemeralSubagentExecutor, DEFAULT_MAX_TURNS, EphemeralSubagentError, EXECUTION_BUDGET_ENV, formatDuration, } from "./ephemeral.js";
@@ -450,6 +450,14 @@ function resolveConfiguredRoute(ctx, profileName, profile) {
450
450
  }
451
451
  throw new Error(`No usable ${profileName} task model route. Run /task-models.`);
452
452
  }
453
+ /** Return the profile when it has a primary and a fallback route that resolve in this session, so failover can help. */
454
+ function failoverProfile(ctx, profileName, agentDir = getAgentDir()) {
455
+ const profile = loadTaskModelsConfig(agentDir).value.profiles[profileName];
456
+ if (!profile)
457
+ return undefined;
458
+ const usable = orderedProfileRoutes(profile).filter((route) => resolveTaskModelRoute(ctx, route) !== undefined);
459
+ return usable.length > 1 ? profileName : undefined;
460
+ }
453
461
  export function resolveRoleSkills(pi, role) {
454
462
  const skills = new Map(pi.getCommands()
455
463
  .filter((command) => command.source === "skill")
@@ -512,6 +520,8 @@ function prepareRoleLaunchFromSkills(ctx, input, skills) {
512
520
  ROLE_MCP_EXTENSION,
513
521
  ...(role.tools.includes("git_read") ? [GIT_READ_EXTENSION] : []),
514
522
  ROLE_TOOLS_EXTENSION,
523
+ // The child runs with --no-extensions, so it loads the Task Profile Models only from this path.
524
+ ...(input.failoverProfile ? [TASK_PROFILE_MODELS_EXTENSION] : []),
515
525
  ];
516
526
  const env = Object.fromEntries(Object.entries(input.env ?? {}).map(([key, value]) => {
517
527
  if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key))
@@ -527,7 +537,7 @@ function prepareRoleLaunchFromSkills(ctx, input, skills) {
527
537
  for (const skill of skills.paths)
528
538
  args.push("--skill", skill);
529
539
  args.push(`--${ROLE_TOOL_POLICY_FLAG}`, JSON.stringify(tools));
530
- args.push("--model", modelReference(input.route.model));
540
+ args.push("--model", input.failoverProfile ? taskProfileModelReference(input.failoverProfile) : modelReference(input.route.model));
531
541
  if (input.route.thinkingLevel)
532
542
  args.push("--thinking", input.route.thinkingLevel);
533
543
  args.push(ctx.isProjectTrusted() ? "--approve" : "--no-approve");
@@ -546,11 +556,16 @@ function prepareRoleLaunchFromSkills(ctx, input, skills) {
546
556
  function prepareResolvedRoleLaunch(pi, ctx, input) {
547
557
  const { task, modelClass, agentDir, ...launchInput } = input;
548
558
  const selectedClass = modelClass ?? input.role.modelClass;
559
+ const route = selectedClass === undefined
560
+ ? resolveConfiguredTaskRoute(ctx, task, agentDir)
561
+ : resolveTaskRoute(ctx, selectedClass, agentDir);
562
+ // The route resolved, so the config loads. A task without a class uses its assigned or declared profile.
563
+ const profileName = selectedClass ?? loadTaskModelsConfig(agentDir).value.tasks[task.id] ?? task.defaultProfile;
564
+ const failover = failoverProfile(ctx, profileName, agentDir);
549
565
  return prepareRoleLaunchFromSkills(ctx, {
550
566
  ...launchInput,
551
- route: selectedClass === undefined
552
- ? resolveConfiguredTaskRoute(ctx, task, agentDir)
553
- : resolveTaskRoute(ctx, selectedClass, agentDir),
567
+ route,
568
+ ...(failover ? { failoverProfile: failover } : {}),
554
569
  }, resolveRoleSkills(pi, input.role));
555
570
  }
556
571
  export function resolveRoleLaunch(pi, ctx, input) {
@@ -589,9 +604,12 @@ export async function resolveConfiguredRoleLaunch(pi, ctx, input) {
589
604
  ...namedSkills,
590
605
  paths: [...new Set([...namedSkills.paths, ...resources.skills])],
591
606
  };
607
+ const route = resolveTaskRoute(ctx, input.modelClass);
608
+ const failover = failoverProfile(ctx, input.modelClass);
592
609
  const prepared = prepareRoleLaunchFromSkills(ctx, {
593
610
  role: effectiveRole,
594
- route: resolveTaskRoute(ctx, input.modelClass),
611
+ route,
612
+ ...(failover ? { failoverProfile: failover } : {}),
595
613
  }, skills);
596
614
  assertNoMissingRoleSkills(effectiveRole, prepared);
597
615
  const args = [
@@ -81,7 +81,7 @@ Direct `changeset` is rejected; use isolated mode for checked integration. Resou
81
81
 
82
82
  ### Dry run
83
83
 
84
- Set `"dryRun": true` on a direct or isolated `delegate_task` request to check it without a launch. The dry run does the same checks as a launch: request shape, Role lookup, route and model with thinking level, Skills, tools, MCP allowlist, Role extensions, config, and `continueFrom` eligibility. For an isolated request, it also checks the task graph and that Main is clean and committed. If the request has a changeset task, it also selects the Full suite, adds it to the final checks, and shows it. A failed check gives the same error as a launch.
84
+ Set `"dryRun": true` on a direct or isolated `delegate_task` request to check it without a launch. The dry run does the same checks as a launch: request shape, Role lookup, route and model with thinking level, Skills, tools, MCP allowlist, Role extensions, config, and `continueFrom` eligibility. For an isolated request, it also checks the task graph and pins Main's committed branch, HEAD, and tree. Dirty files in Main do not block the dry run or admission. If the request has a changeset task, it also selects the Full suite, adds it to the final checks, and shows it. A failed check gives the same error as a launch.
85
85
 
86
86
  The result is a bounded plan. It shows each task's Role, resolved `provider/model`, thinking level, access or kind, dependencies, and checks. An isolated plan also shows the final checks. The dry run makes no Herdr call, Herdr tab, session record, saved state, worktree, lease, checkout admission, or follow-up. The plan lists each check it cannot do without such an effect as `Not checked: <reason>`. Examples are the Herdr host preflight, checkout admission, and the stop of a continued agent.
87
87
 
@@ -281,7 +281,7 @@ Every task declares `id`, `kind`, `role`, `modelClass`, `requirements`, `deliver
281
281
 
282
282
  Admission and worker-wave dispatch pin Main's exact committed branch, HEAD, and tree without inspecting or modifying its staged, unstaged, or untracked edits. Worker allocation verifies the same committed identity around worktree creation, including resumed waves; staged-dependent waves verify their clean integration snapshot. A dirty Main can therefore produce a checked retained candidate, but branch/HEAD drift stops new waves. The runner executes ready worker tasks concurrently, inspects clean committed candidates, runs preliminary task checks and optional preliminary judgment, then records ready candidates for Main. **It does not automatically integrate them.**
283
283
 
284
- The extension waits up to about 10 seconds for the shell of a new worker pane to finish startup before it starts Pi.
284
+ The extension waits up to about 10 seconds for the shell of a new worker pane to finish startup. Then it waits up to about 20 more seconds for that shell to own the pane foreground, before the readiness probe and the Pi start.
285
285
 
286
286
  Main selects exact candidates through `subagent_stage` into an owned integration checkout in explicit order. Each stage is a committed merge of the candidate, not a mutation of Main. Conflicts retain the checkout for Main to resolve; `resolve` verifies the exact two-parent merge instead of replaying an uncertain merge. Main can explicitly run dependent tasks from an exact staged tip with `subagent_integrate advance` and stage their results afterward. Independent candidates may be staged without re-running workers.
287
287
 
@@ -396,13 +396,13 @@ If you removed resources outside Pi Subagent, abort can reconcile their absence.
396
396
 
397
397
  ### Recovery and state
398
398
 
399
- State version 5 lives privately under:
399
+ State version 6 lives privately under:
400
400
 
401
401
  ```text
402
402
  <agent-dir>/config/pi-subagent/state/<repository-hash>/<request-id>.json
403
403
  ```
404
404
 
405
- Parsing rejects unknown properties, malformed evidence, invalid lineage, and v4 or older state without migration. Preserve old files and recover work manually. Writes are atomic. A productive lease prevents concurrent execution or resumption but permits status inspection and abort.
405
+ Parsing rejects unknown properties, malformed evidence, invalid lineage, and v5 or older state without migration. Preserve old files and recover work manually. Writes are atomic. A productive lease prevents concurrent execution or resumption but permits status inspection and abort.
406
406
 
407
407
  `delegate_task`, `subagent_resume`, and `subagent_integrate advance` return a durable request ID after saving the wave. Work continues asynchronously. A saved worker attention state sends a follow-up to the launching session, including while siblings run. The wave also reports its final result.
408
408
 
@@ -600,4 +600,7 @@ Worktree finalization returns one of:
600
600
  - `retained`: exact commit and dirty measurements are known and work remains; or
601
601
  - `recovery`: cleanup or measurement needs deliberate action and reports only proved fields.
602
602
 
603
- No cleanup path uses `git clean`, force deletion, hidden fallback to Main, or deletion of unproved work. The one exception is user-confirmed discard: after the user selects **Close and delete worktrees (discard work)** in `/subagent` and confirms, it force-removes the reviewed owned worktrees.
603
+ No cleanup path uses `git clean`, force deletion, hidden fallback to Main, or deletion of unproved work. There are two exceptions, and each needs user confirmation:
604
+
605
+ - After the user selects **Close and delete worktrees (discard work)** in `/subagent` and confirms, the extension force-removes the reviewed owned worktrees.
606
+ - After a separate confirmation in `/subagent prune`, the command force-removes the reviewed items that it marked as forceable.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hcflabs/pi-subagent",
3
- "version": "28.3.3",
3
+ "version": "28.4.0",
4
4
  "description": "Delegate bounded direct work or durable checked isolated task graphs to Pi roles.",
5
5
  "keywords": [
6
6
  "pi-package",
@@ -74,7 +74,7 @@
74
74
  "@hcflabs/pi-herdr": "^0.7.0",
75
75
  "@hcflabs/pi-process": "^0.3.0",
76
76
  "@hcflabs/pi-repo": "^0.1.0",
77
- "@hcflabs/pi-task-models": "^9.0.0",
77
+ "@hcflabs/pi-task-models": "^9.3.0",
78
78
  "proper-lockfile": "^4.1.2"
79
79
  },
80
80
  "devDependencies": {