@hcflabs/pi-subagent 28.3.4 → 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
@@ -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 = [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hcflabs/pi-subagent",
3
- "version": "28.3.4",
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": {