@delorenj/pjangler 1.2.33 → 1.3.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.
Files changed (26) hide show
  1. package/dist/index.js +295 -130
  2. package/dist/mcp-server.js +297 -129
  3. package/package.json +1 -1
  4. package/templates/commonproject/template/.agents/hooks/README.md +10 -10
  5. package/templates/commonproject/template/.agents/hooks/hooks.master.json +1 -1
  6. package/templates/commonproject/template/.agents/hooks/sync.py +4 -4
  7. package/templates/commonproject/template/.agents/local.example.json +1 -1
  8. package/templates/commonproject/template/.mise/scripts/hindsight-setup.sh +1 -1
  9. package/templates/commonproject/template/mise.toml.jinja +11 -11
  10. package/templates/hermes-agent/copier.yml +27 -4
  11. package/templates/hermes-agent/template/.runtime-scaffold/README.md +11 -10
  12. package/templates/hermes-agent/template/.scripts/01-config.sh +4 -4
  13. package/templates/hermes-agent/template/.scripts/10-hermes-profile.sh +7 -12
  14. package/templates/hermes-agent/template/.scripts/20-runtime-repo.sh +37 -32
  15. package/templates/hermes-agent/template/.scripts/30-telegram.sh +18 -4
  16. package/templates/hermes-agent/template/.scripts/70-systemd.sh +72 -12
  17. package/templates/hermes-agent/template/.scripts/80-registry.sh +13 -3
  18. package/templates/hermes-agent/template/.scripts/_lib.sh +12 -5
  19. package/templates/hermes-agent/template/.scripts/config.example.toml +7 -4
  20. package/templates/hermes-agent/template/.scripts/credential-launch.sh +81 -0
  21. package/templates/hermes-agent/template/.scripts/heartbeat.sh +2 -1
  22. package/templates/hermes-agent/template/.scripts/providers/plane.sh +30 -4
  23. package/templates/hermes-agent/template/.scripts/secret-scan.py +82 -16
  24. package/templates/hermes-agent/template/SOUL.md.jinja +4 -4
  25. package/templates/hermes-agent/template/hermes.jinja +36 -7
  26. package/templates/hermes-agent/template/role.yaml.jinja +15 -5
@@ -878,8 +878,26 @@ var OP_INJECT_SCRIPT = `'{{config_root}}/${MATERIALIZE_ENV_SCRIPT_REL}'`;
878
878
  var PROVISION_PACKS_SCRIPT_REL = ".mise/scripts/provision-packs.py";
879
879
  var LEGACY_PROVISION_SCRIPT_REL = ".mise/scripts/provision-bmad-skills.py";
880
880
  var SYNC_SKILLS_SCRIPT_REL = ".mise/scripts/sync-skills.py";
881
- var PROVISION_PACKS_TASK = "skills-provision-packs";
881
+ var LINK_AGENTFILES_TASK = "link:agentfiles";
882
+ var SKILLS_SYNC_TASK = "skills:sync";
883
+ var PROVISION_PACKS_TASK = "skills:provision:packs";
882
884
  var LEGACY_PROVISION_TASK = "skills-provision-bmad";
885
+ var RETIRED_TASK_RENAMES = [
886
+ ["link-agentfiles", LINK_AGENTFILES_TASK],
887
+ ["skills-sync", SKILLS_SYNC_TASK],
888
+ ["skills-provision-packs", PROVISION_PACKS_TASK],
889
+ ["hooks-sync", "hooks:sync"],
890
+ ["hooks-check", "hooks:check"],
891
+ ["hooks-uninstall", "hooks:uninstall"],
892
+ ["hindsight-setup", "hindsight:setup"]
893
+ ];
894
+ function taskHeader(name) {
895
+ return `[tasks."${name}"]`;
896
+ }
897
+ function taskHeaderPattern(name) {
898
+ const esc = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
899
+ return new RegExp(`^\\[tasks\\.(?:"${esc}"|${esc})\\]$`);
900
+ }
883
901
  var PROVISION_PACKS_SCRIPT = `python3 '{{config_root}}/${PROVISION_PACKS_SCRIPT_REL}'`;
884
902
  var LEGACY_PROVISION_BMAD_SKILLS_SCRIPT = `python3 '{{config_root}}/${LEGACY_PROVISION_SCRIPT_REL}'`;
885
903
  var SYNC_SKILLS_SCRIPT = `python3 '{{config_root}}/${SYNC_SKILLS_SCRIPT_REL}' --scope project`;
@@ -913,22 +931,22 @@ var LINK_AGENTFILES_HOOK_ENTRIES = [
913
931
  ];
914
932
  var LINK_AGENTFILES_WATCH_TASK_BLOCK = `[[watch_files]]
915
933
  patterns = ["AGENTS.md"]
916
- task = "link-agentfiles"
934
+ task = "${LINK_AGENTFILES_TASK}"
917
935
 
918
936
  [[watch_files]]
919
937
  patterns = [".agents/skills.json"]
920
- task = "skills-sync"
938
+ task = "${SKILLS_SYNC_TASK}"
921
939
 
922
- [tasks.link-agentfiles]
940
+ ${taskHeader(LINK_AGENTFILES_TASK)}
923
941
  description = "Symlink all agent files to AGENTS.md"
924
942
  run = "'{{config_root}}/.mise/scripts/link-agentfiles.sh'"
925
943
 
926
- [tasks.skills-sync]
944
+ ${taskHeader(SKILLS_SYNC_TASK)}
927
945
  description = "Sync skills from manifest to local CLI dirs"
928
946
  depends = ["${PROVISION_PACKS_TASK}"]
929
947
  run = ${JSON.stringify(SYNC_SKILLS_SCRIPT)}
930
948
 
931
- [tasks.${PROVISION_PACKS_TASK}]
949
+ ${taskHeader(PROVISION_PACKS_TASK)}
932
950
  description = "Provision every Skillex pack declared in .agents/skills.json"
933
951
  run = ${JSON.stringify(PROVISION_PACKS_SCRIPT)}`;
934
952
  var VERSIONING_BLOCK = `# >>> mise-versioning >>> (managed block \u2014 do not edit by hand; re-run init to update)
@@ -1090,6 +1108,7 @@ function discoverRoles(repoRoot) {
1090
1108
  ticketProviderName: yamlGet(text2, "ticket_provider.name"),
1091
1109
  ticketProviderBoardId: yamlGet(text2, "ticket_provider.board_id"),
1092
1110
  ticketProviderIdentifier: yamlGet(text2, "plane.identifier"),
1111
+ bloodbankEnabled: yamlGet(text2, "bloodbank.enabled"),
1093
1112
  deploymentSystemd: yamlGet(text2, "deployment.systemd"),
1094
1113
  legacyReconcileEnabled: yamlGet(text2, "reconcile.enabled"),
1095
1114
  legacyReconcileGraceHours: yamlGet(text2, "reconcile.grace_hours"),
@@ -2292,10 +2311,41 @@ function upsertOpInjectHook(text2) {
2292
2311
  [OP_INJECT_SCRIPT]
2293
2312
  );
2294
2313
  }
2314
+ function retiredTaskNameIssues(text2) {
2315
+ const issues = [];
2316
+ for (const [oldName, newName] of RETIRED_TASK_RENAMES) {
2317
+ const esc = oldName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2318
+ const present = new RegExp(
2319
+ `^\\[tasks\\.(?:"${esc}"|${esc})\\]|^\\s*task\\s*=\\s*"${esc}"|^\\s*depends\\s*=\\s*\\[[^\\]]*"${esc}"`,
2320
+ "m"
2321
+ ).test(text2);
2322
+ if (present) issues.push(`mise.toml still uses the retired task name "${oldName}" (renamed to "${newName}")`);
2323
+ }
2324
+ return issues;
2325
+ }
2326
+ function renameRetiredMiseTasks(text2) {
2327
+ let out = text2;
2328
+ for (const [oldName, newName] of RETIRED_TASK_RENAMES) {
2329
+ const esc = oldName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2330
+ out = out.replace(new RegExp(`^\\[tasks\\.(?:"${esc}"|${esc})\\]`, "gm"), taskHeader(newName));
2331
+ out = out.replace(new RegExp(`^(\\s*task\\s*=\\s*)"${esc}"`, "gm"), `$1"${newName}"`);
2332
+ out = out.replace(new RegExp(`\\bmise run ${esc}\\b`, "g"), `mise run ${newName}`);
2333
+ }
2334
+ return out.replace(/^(\s*depends\s*=\s*)(\[[^\]]*\])/gm, (_whole, head, arr) => {
2335
+ let next = arr;
2336
+ for (const [oldName, newName] of RETIRED_TASK_RENAMES) {
2337
+ next = next.split(`"${oldName}"`).join(`"${newName}"`);
2338
+ }
2339
+ return head + next;
2340
+ });
2341
+ }
2295
2342
  function upsertLinkAgentfilesBlock(text2, ctx) {
2296
- const withPath = upsertMisePath(text2, requiredMisePathEntries(ctx));
2297
- let cleaned = removeTomlSection(withPath, /^\[tasks\.link-agentfiles\]$/, /link-agentfiles/, { includePrecedingComments: false });
2343
+ const withPath = upsertMisePath(renameRetiredMiseTasks(text2), requiredMisePathEntries(ctx));
2344
+ let cleaned = removeTomlSection(withPath, taskHeaderPattern(LINK_AGENTFILES_TASK), /link-agentfiles/, { includePrecedingComments: false });
2345
+ cleaned = removeTomlSection(cleaned, /^\[tasks\.link-agentfiles\]$/, /link-agentfiles/, { includePrecedingComments: false });
2346
+ cleaned = removeTomlSection(cleaned, taskHeaderPattern(SKILLS_SYNC_TASK), void 0, { includePrecedingComments: false });
2298
2347
  cleaned = removeTomlSection(cleaned, /^\[tasks\.skills-sync\]$/, void 0, { includePrecedingComments: false });
2348
+ cleaned = removeTomlSection(cleaned, taskHeaderPattern(PROVISION_PACKS_TASK), void 0, { includePrecedingComments: false });
2299
2349
  cleaned = removeTomlSection(cleaned, /^\[tasks\.skills-provision-packs\]$/, void 0, { includePrecedingComments: false });
2300
2350
  cleaned = removeTomlSection(cleaned, /^\[tasks\.skills-provision-bmad\]$/, void 0, { includePrecedingComments: false });
2301
2351
  cleaned = removeTomlSection(cleaned, /^\[tasks\.link-project-skills-to-clis\]$/, void 0, { includePrecedingComments: false });
@@ -2537,7 +2587,7 @@ You are **${role.displayName || role.agentId}** \u2014 a Hermes agent provisione
2537
2587
 
2538
2588
  ## Scope
2539
2589
 
2540
- You operate only within the working directory of \`${role.repo}\`. Your HERMES_HOME is the ignored local directory at \`./runtime/\`, which \`~/.hermes/profiles/${role.profileName || role.agentId}\` projects into (so \`--profile\` invocations resolve here too). Secrets, SOUL, memories, skills, sessions, gateway state, and runtime files stay local to that runtime and are never project gitlinks.
2590
+ You operate only within the working directory of \`${role.repo}\`. HERMES_HOME is the real named profile at \`~/.hermes/profiles/${role.profileName || role.agentId}\`; shared config/auth/skills remain linked to fleet truth while owned state lives in ignored \`./runtime/\`. The launcher supplies the project root through process-local \`TERMINAL_CWD\` and never persists it into shared config.
2541
2591
 
2542
2592
  ## Tone
2543
2593
 
@@ -2552,51 +2602,62 @@ ${roleSpecific}
2552
2602
  Your memory is stored locally at \`./runtime/memories/\`. Use durable memory deliberately and keep \`memories/MEMORY.md\` current.
2553
2603
  `;
2554
2604
  }
2555
- function renderHermesWrapper(role) {
2556
- return `#!/usr/bin/env bash
2557
- # Launcher for ${role.agentId}. Resolves HERMES_HOME to the local runtime.
2558
-
2559
- set -euo pipefail
2560
-
2561
- ROLE_DIR="$(cd "$(dirname "$0")" && pwd)"
2562
- RUNTIME_HOME="$ROLE_DIR/runtime"
2563
-
2564
- FLEET_ENV="{HERMES_FLEET_ENV:-$HOME/.hermes/fleet.env}"
2565
- if [[ -f "$FLEET_ENV" ]]; then
2566
- # shellcheck disable=SC1090
2567
- source "$FLEET_ENV"
2568
- fi
2569
-
2570
- HERMES_BIN="{HERMES_BIN:-{HERMES_FLEET_BIN:-$HOME/.hermes/hermes-agent/.venv/bin/hermes}}"
2571
- CODEX_HOME="{CODEX_HOME:-{HERMES_FLEET_CODEX_HOME:-$HOME/.codex}}"
2572
-
2573
- FLEET_HOME="{HERMES_FLEET_HOME:-$HOME/.hermes}"
2574
- PROFILE_NAME="{HERMES_PROFILE_NAME:-${role.profileName || role.agentId}}"
2575
-
2576
- # Singleton-runtime contract: HERMES_HOME MUST be the named profile dir, never
2577
- # the raw runtime path. Hermes treats any HERMES_HOME that is neither under
2578
- # ~/.hermes nor a child of a "profiles" dir as its own standalone root, which
2579
- # makes get_active_profile_name() report "default" and _global_auth_file_path()
2580
- # return None -- silently disabling shared fleet auth and giving every agent a
2581
- # divergent config.yaml. The profile dir is a REAL dir whose shared entries
2582
- # (config.yaml, .env, skills) symlink to the fleet root and whose person-owned
2583
- # entries (memories, sessions, state.db, workspace) symlink into $RUNTIME_HOME.
2584
- HERMES_HOME="$FLEET_HOME/profiles/$PROFILE_NAME"
2585
-
2586
- if [[ ! -d "$RUNTIME_HOME" ]]; then
2587
- echo "hermes: local runtime not provisioned at $RUNTIME_HOME" >&2
2588
- echo " fix: run the role's .scripts/20-runtime-repo.sh" >&2
2589
- exit 1
2590
- fi
2591
-
2592
- if [[ ! -d "$HERMES_HOME" ]]; then
2593
- echo "hermes: profile not provisioned at $HERMES_HOME" >&2
2594
- echo " fix: pj migrate hermes.runtime-singleton" >&2
2595
- exit 1
2596
- fi
2597
-
2598
- exec env HERMES_HOME="$HERMES_HOME" HERMES_FLEET_ENV="$FLEET_ENV" CODEX_HOME="$CODEX_HOME" "$HERMES_BIN" "$@"
2599
- `.replace(/\u0010/g, "$");
2605
+ function renderHermesWrapper(role, templateRoleDir) {
2606
+ return readText(join3(templateRoleDir, "hermes.jinja")).replace(/\{\{\s*agent_id\s*\}\}/g, role.agentId);
2607
+ }
2608
+ function templateFiles(sourceDir, current = sourceDir) {
2609
+ if (!existsSync2(current)) return [];
2610
+ const files = [];
2611
+ for (const entry of readdirSync2(current, { withFileTypes: true })) {
2612
+ if (entry.name === "__pycache__" || entry.name.endsWith(".pyc") || entry.name.endsWith(".pyo")) continue;
2613
+ const sourcePath = join3(current, entry.name);
2614
+ if (entry.isDirectory()) files.push(...templateFiles(sourceDir, sourcePath));
2615
+ else if (entry.isFile()) files.push(relative2(sourceDir, sourcePath));
2616
+ }
2617
+ return files.sort();
2618
+ }
2619
+ function managedHermesScaffoldRoles(ctx) {
2620
+ const discovered = discoverRoles(ctx.repoRoot);
2621
+ const declared = readDeclaredAgents(ctx).filter((entry) => entry.role === "pm" || entry.role === "director");
2622
+ if (declared.length === 0) {
2623
+ const orchestrators = discovered.filter((role) => role.role === "pm" || role.role === "director");
2624
+ const blockers2 = orchestrators.filter((role) => roleBloodbankEnabled(role) === null).map((role) => `${relative2(ctx.repoRoot, role.roleYamlPath)} bloodbank.enabled must be the strict YAML boolean true or false`);
2625
+ return {
2626
+ roles: orchestrators.filter((role) => roleBloodbankEnabled(role) !== null),
2627
+ blockers: blockers2
2628
+ };
2629
+ }
2630
+ const roles = [];
2631
+ const blockers = [];
2632
+ for (const entry of declared) {
2633
+ if (!entry.roleDir) {
2634
+ blockers.push(`agents.${entry.agentId}.role_dir missing`);
2635
+ continue;
2636
+ }
2637
+ const roleDir = resolve3(ctx.repoRoot, entry.roleDir);
2638
+ if (!isContainedBy(ctx.repoRoot, roleDir)) {
2639
+ blockers.push(`agents.${entry.agentId}.role_dir resolves outside the project`);
2640
+ continue;
2641
+ }
2642
+ const role = discovered.find((candidate) => resolve3(candidate.roleDir) === roleDir);
2643
+ if (!role) {
2644
+ blockers.push(`agents.${entry.agentId}.role_dir ${entry.roleDir} missing role.yaml`);
2645
+ continue;
2646
+ }
2647
+ if (role.agentId !== entry.agentId || role.role !== entry.role) {
2648
+ blockers.push(`agents.${entry.agentId} identity does not match ${entry.roleDir}/role.yaml`);
2649
+ continue;
2650
+ }
2651
+ if (roleBloodbankEnabled(role) === null) {
2652
+ blockers.push(`${entry.roleDir}/role.yaml bloodbank.enabled must be the strict YAML boolean true or false`);
2653
+ continue;
2654
+ }
2655
+ roles.push(role);
2656
+ }
2657
+ return { roles, blockers };
2658
+ }
2659
+ function renderSentinelPrompt(role, templateRoleDir) {
2660
+ return readText(join3(templateRoleDir, ".scripts", "sentinel.prompt.md.jinja")).replace(/\{\{\s*agent_id\s*\}\}/g, role.agentId).replace(/\{\{\s*role\s*\}\}/g, role.role).replace(/\{\{\s*target_repo\s*\}\}/g, role.repo).replace(/\{\{\s*display_name\s*\}\}/g, role.displayName || role.agentId).replace(/\{\{\s*ticket_provider\s*\}\}/g, role.ticketProviderName || "plane");
2600
2661
  }
2601
2662
  function copyMissingRecursive(sourceDir, targetDir, changedFiles, dryRun, skip) {
2602
2663
  if (!existsSync2(sourceDir)) return;
@@ -2693,6 +2754,8 @@ function upsertRegistryEntry(role, homeDir, changedFiles, dryRun) {
2693
2754
  const path = registryPath(homeDir);
2694
2755
  const current = safeReadText(path) ?? "# Hermes agent fleet registry.\n# One entry per provisioned agent. Managed by hermes-agent-template/.scripts/80-registry.sh.\nschema_version: 1\nagents: {}\n";
2695
2756
  if (current.includes(`${role.agentId}:`)) return null;
2757
+ const enabled = roleBloodbankEnabled(role);
2758
+ if (enabled === null) return null;
2696
2759
  const block = ` ${role.agentId}:
2697
2760
  repo: ${role.repo}
2698
2761
  role: ${role.role}
@@ -2708,6 +2771,7 @@ function upsertRegistryEntry(role, homeDir, changedFiles, dryRun) {
2708
2771
  identifier: ${ctxEscape(role.ticketProviderIdentifier)}
2709
2772
  runtime_repo: ${ctxEscape(role.runtimeRepo)}
2710
2773
  bloodbank:
2774
+ enabled: ${enabled ? "true" : "false"}
2711
2775
  gateway_scope: fleet
2712
2776
  target_agent_id: ${role.agentId}
2713
2777
  systemd:
@@ -2720,6 +2784,11 @@ ${block}`) : `${current.replace(/\s*$/, "\n")}${block}`;
2720
2784
  if (!dryRun) writeText(path, next);
2721
2785
  return path;
2722
2786
  }
2787
+ function roleBloodbankEnabled(role) {
2788
+ if (role.bloodbankEnabled === "" || role.bloodbankEnabled === "false") return false;
2789
+ if (role.bloodbankEnabled === "true") return true;
2790
+ return null;
2791
+ }
2723
2792
  function profileMetaInheritsDefault(path) {
2724
2793
  const text2 = safeReadText(path);
2725
2794
  return Boolean(
@@ -3186,8 +3255,30 @@ function dropDeclaredAgent(ctx, agentId, changedFiles, details) {
3186
3255
  if (!ctx.dryRun) writeText(path, `${JSON.stringify(doc, null, 2)}
3187
3256
  `);
3188
3257
  }
3189
- function rewriteLauncher(text2) {
3190
- let next = text2.replace(/^HERMES_HOME="\$RUNTIME_HOME"\s*$/m, 'HERMES_HOME="$FLEET_HOME/profiles/$PROFILE_NAME"');
3258
+ function isProfileHomeExpr(assigned) {
3259
+ const bare = assigned.replace(/^["']|["']$/g, "");
3260
+ return bare === "$FLEET_HOME/profiles/$PROFILE_NAME" || /^\$\{?HERMES_FLEET_HOME.*\}?\/profiles\//.test(bare) || /\/\.hermes\/profiles\/[^/]+$/.test(bare);
3261
+ }
3262
+ function rewriteLauncher(text2, profileName) {
3263
+ let next = text2;
3264
+ const assigned = /^HERMES_HOME=(.*)$/m.exec(next)?.[1]?.trim();
3265
+ if (assigned !== void 0 && !isProfileHomeExpr(assigned)) {
3266
+ const name = profileName ? `\${HERMES_PROFILE_NAME:-${profileName}}` : '${HERMES_PROFILE_NAME:-$(basename "$ROLE_DIR")}';
3267
+ next = next.replace(
3268
+ /^HERMES_HOME=(.*)$/m,
3269
+ [
3270
+ `RUNTIME_HOME=$1`,
3271
+ `FLEET_HOME="\${HERMES_FLEET_HOME:-$HOME/.hermes}"`,
3272
+ `PROFILE_NAME="${name}"`,
3273
+ `# Singleton-runtime contract: HERMES_HOME MUST be the named profile dir.`,
3274
+ `HERMES_HOME="$FLEET_HOME/profiles/$PROFILE_NAME"`
3275
+ ].join("\n")
3276
+ );
3277
+ next = next.replace(
3278
+ /if \[\[ ! -d "\$HERMES_HOME" \]\]; then\n(\s*)echo "hermes: local runtime not provisioned at \$HERMES_HOME"/,
3279
+ 'if [[ ! -d "$RUNTIME_HOME" ]]; then\n$1echo "hermes: local runtime not provisioned at $RUNTIME_HOME"'
3280
+ );
3281
+ }
3191
3282
  next = next.replace(/^HERMES_OAUTH_FILE=.*\n/m, "");
3192
3283
  next = next.replace(/\s*HERMES_OAUTH_FILE="\$HERMES_OAUTH_FILE"/g, "");
3193
3284
  next = next.replace(
@@ -3394,7 +3485,8 @@ function createMiseChecks() {
3394
3485
  if (missingPathValues.length) details.push(`[env]._.path should include ${missingPathValues.join(", ")}`);
3395
3486
  if (!text2.includes("'{{config_root}}/.mise/scripts/link-agentfiles.sh'")) details.push("link-agentfiles hook must use single-quoted {{config_root}} guard");
3396
3487
  if (!text2.includes('patterns = ["AGENTS.md"]')) details.push("watch_files must monitor AGENTS.md");
3397
- if (!text2.includes('task = "link-agentfiles"')) details.push("watch_files must dispatch link-agentfiles task");
3488
+ if (!text2.includes(`task = "${LINK_AGENTFILES_TASK}"`)) details.push(`watch_files must dispatch the ${LINK_AGENTFILES_TASK} task`);
3489
+ details.push(...retiredTaskNameIssues(text2));
3398
3490
  return {
3399
3491
  id: "mise.config-root",
3400
3492
  title: "mise config_root + AGENTS link hooks",
@@ -3442,7 +3534,7 @@ function createMiseChecks() {
3442
3534
  status: changedFiles.length ? "applied" : "noop",
3443
3535
  summary: changedFiles.length ? "Updated mise AGENTS-linking contract" : "No changes required",
3444
3536
  changedFiles,
3445
- details: changedFiles.length ? ["Normalized hooks/watch_files/tasks.link-agentfiles block and script"] : []
3537
+ details: changedFiles.length ? [`Normalized hooks/watch_files/tasks."${LINK_AGENTFILES_TASK}" block and script`] : []
3446
3538
  };
3447
3539
  }
3448
3540
  },
@@ -3653,8 +3745,9 @@ function createAgentHooksChecks() {
3653
3745
  details.push("mise.toml still invokes the missing bare sync-skills.py executable");
3654
3746
  }
3655
3747
  if (!mise?.includes('patterns = [".agents/skills.json"]')) details.push("mise.toml should watch .agents/skills.json");
3656
- if (!mise?.includes("[tasks.skills-sync]")) details.push("mise.toml should define a skills-sync task");
3657
- if (!mise?.includes(`depends = ["${PROVISION_PACKS_TASK}"]`)) details.push(`skills-sync task should depend on ${PROVISION_PACKS_TASK}`);
3748
+ if (!mise?.includes(taskHeader(SKILLS_SYNC_TASK))) details.push(`mise.toml should define a ${SKILLS_SYNC_TASK} task`);
3749
+ if (!mise?.includes(`depends = ["${PROVISION_PACKS_TASK}"]`)) details.push(`${SKILLS_SYNC_TASK} task should depend on ${PROVISION_PACKS_TASK}`);
3750
+ if (mise) details.push(...retiredTaskNameIssues(mise));
3658
3751
  for (const [rel, label] of [
3659
3752
  [PROVISION_PACKS_SCRIPT_REL, "Skillex pack provisioning script"],
3660
3753
  [SYNC_SKILLS_SCRIPT_REL, "Project-local skills sync engine"]
@@ -4547,75 +4640,90 @@ function createHermesChecks() {
4547
4640
  return [
4548
4641
  {
4549
4642
  id: "hermes.pm-scaffold",
4550
- title: "Hermes PM scaffold parity",
4643
+ title: "Hermes orchestrator scaffold parity",
4551
4644
  audit: (ctx) => {
4552
- const roles = discoverRoles(ctx.repoRoot);
4553
- const role = roles.find((item) => item.role === "pm");
4554
- if (!role) {
4555
- return { id: "hermes.pm-scaffold", title: "Hermes PM scaffold parity", status: "skip", summary: "No pm role present", details: [], fixable: false };
4645
+ const selection = managedHermesScaffoldRoles(ctx);
4646
+ if (selection.roles.length === 0 && selection.blockers.length === 0) {
4647
+ return { id: "hermes.pm-scaffold", title: "Hermes orchestrator scaffold parity", status: "skip", summary: "No provisioned pm or director role present", details: [], fixable: false };
4556
4648
  }
4557
- const details = [];
4558
- for (const rel of ["role.yaml", "SOUL.md", "hermes", ".gitignore", ".scripts/70-systemd.sh", ".scripts/heartbeat.sh", ".scripts/checkpoint.sh", ".runtime-scaffold/README.md", "runtime/memories/MEMORY.md"]) {
4559
- if (!existsSync2(join3(role.roleDir, rel))) details.push(`missing ${relative2(ctx.repoRoot, join3(role.roleDir, rel))}`);
4560
- }
4561
- if (hasRuntimeSubmoduleMapping(ctx.repoRoot, role)) {
4562
- details.push(".gitmodules contains retired pm runtime submodule mapping");
4563
- }
4564
- if (!profileMetaInheritsDefault(join3(role.roleDir, "runtime", "profile.yaml"))) {
4565
- details.push("runtime/profile.yaml missing inherited default config metadata");
4649
+ const details = [...selection.blockers];
4650
+ const templateRoleDir = join3(ctx.pjanglerRoot, "templates", "hermes-agent", "template");
4651
+ const managedScripts = templateFiles(join3(templateRoleDir, ".scripts")).filter((rel) => rel !== "sentinel.prompt.md.jinja");
4652
+ for (const role of selection.roles) {
4653
+ const prefix = role.agentId || role.role;
4654
+ for (const rel of ["role.yaml", "SOUL.md", ".runtime-scaffold/README.md", "runtime/memories/MEMORY.md"]) {
4655
+ if (!existsSync2(join3(role.roleDir, rel))) details.push(`${prefix}: missing ${relative2(ctx.repoRoot, join3(role.roleDir, rel))}`);
4656
+ }
4657
+ const wrapper = join3(role.roleDir, "hermes");
4658
+ const expectedWrapper = renderHermesWrapper(role, templateRoleDir);
4659
+ if (!existsSync2(wrapper)) details.push(`${prefix}: missing ${relative2(ctx.repoRoot, wrapper)}`);
4660
+ else if (safeReadText(wrapper) !== expectedWrapper) details.push(`${prefix}: stale ${relative2(ctx.repoRoot, wrapper)}`);
4661
+ const expectedIgnore = readText(join3(templateRoleDir, ".gitignore.jinja")).replace(/\{\{\s*role\s*\}\}/g, role.role);
4662
+ const ignorePath = join3(role.roleDir, ".gitignore");
4663
+ if (!existsSync2(ignorePath)) details.push(`${prefix}: missing ${relative2(ctx.repoRoot, ignorePath)}`);
4664
+ else if (safeReadText(ignorePath) !== expectedIgnore) details.push(`${prefix}: stale ${relative2(ctx.repoRoot, ignorePath)}`);
4665
+ for (const rel of managedScripts) {
4666
+ const source = join3(templateRoleDir, ".scripts", rel);
4667
+ const target = join3(role.roleDir, ".scripts", rel);
4668
+ if (!existsSync2(target)) details.push(`${prefix}: missing ${relative2(ctx.repoRoot, target)}`);
4669
+ else if (safeReadText(target) !== readText(source)) details.push(`${prefix}: stale ${relative2(ctx.repoRoot, target)}`);
4670
+ }
4671
+ const promptPath = join3(role.roleDir, ".scripts", "sentinel.prompt.md");
4672
+ if (!existsSync2(promptPath)) details.push(`${prefix}: missing ${relative2(ctx.repoRoot, promptPath)}`);
4673
+ else if (safeReadText(promptPath) !== renderSentinelPrompt(role, templateRoleDir)) details.push(`${prefix}: stale ${relative2(ctx.repoRoot, promptPath)}`);
4674
+ if (hasRuntimeSubmoduleMapping(ctx.repoRoot, role)) details.push(`${prefix}: .gitmodules contains retired ${role.role} runtime submodule mapping`);
4675
+ if (!profileMetaInheritsDefault(join3(role.roleDir, "runtime", "profile.yaml"))) details.push(`${prefix}: runtime/profile.yaml missing inherited default config metadata`);
4676
+ const registry = safeReadText(registryPath(ctx.homeDir));
4677
+ if (!registry?.includes(`${role.agentId}:`)) details.push(`fleet registry missing ${role.agentId}`);
4566
4678
  }
4567
- const registry = safeReadText(registryPath(ctx.homeDir));
4568
- if (!registry?.includes(`${role.agentId}:`)) details.push(`fleet registry missing ${role.agentId}`);
4569
4679
  return {
4570
4680
  id: "hermes.pm-scaffold",
4571
- title: "Hermes PM scaffold parity",
4681
+ title: "Hermes orchestrator scaffold parity",
4572
4682
  status: details.length === 0 ? "pass" : "fail",
4573
- summary: details.length === 0 ? "PM scaffold parity verified" : `${details.length} PM scaffold issue(s) detected`,
4683
+ summary: details.length === 0 ? `${selection.roles.length} orchestrator scaffold(s) verified` : `${details.length} orchestrator scaffold issue(s) detected`,
4574
4684
  details,
4575
- fixable: true
4685
+ fixable: selection.blockers.length === 0
4576
4686
  };
4577
4687
  },
4578
4688
  migrate: (ctx, finding) => {
4579
- const role = discoverRoles(ctx.repoRoot).find((item) => item.role === "pm");
4689
+ const selection = managedHermesScaffoldRoles(ctx);
4580
4690
  const changedFiles = [];
4581
4691
  const details = [];
4582
- if (!role) {
4583
- return { id: finding.id, title: finding.title, status: "blocked", summary: "No pm role present", changedFiles, details: [] };
4692
+ if (selection.blockers.length > 0) {
4693
+ return { id: finding.id, title: finding.title, status: "blocked", summary: "Provisioned orchestrator manifest is invalid", changedFiles, details: selection.blockers };
4584
4694
  }
4585
- const retirement = retireRuntimeSubmodule(ctx.repoRoot, role, changedFiles, ctx.dryRun);
4586
- details.push(...retirement.details);
4587
- if (!retirement.ok) {
4588
- return {
4589
- id: finding.id,
4590
- title: finding.title,
4591
- status: "blocked",
4592
- summary: "Failed to retire PM runtime submodule metadata safely",
4593
- changedFiles,
4594
- details: [retirement.error ?? "unknown runtime retirement failure"]
4595
- };
4695
+ if (selection.roles.length === 0) {
4696
+ return { id: finding.id, title: finding.title, status: "blocked", summary: "No provisioned pm or director role present", changedFiles, details: [] };
4596
4697
  }
4597
4698
  const templateRoleDir = join3(ctx.pjanglerRoot, "templates", "hermes-agent", "template");
4598
- writeIfDifferent(join3(role.roleDir, "SOUL.md"), renderSoul(role), ctx.dryRun, changedFiles);
4599
- writeIfDifferent(join3(role.roleDir, "hermes"), renderHermesWrapper(role), ctx.dryRun, changedFiles, 493);
4600
- writeIfDifferent(join3(role.roleDir, ".gitignore"), readText(join3(templateRoleDir, ".gitignore.jinja")).replace(/\{\{ role \}\}/g, role.role), ctx.dryRun, changedFiles);
4601
- copyMissingRecursive(join3(templateRoleDir, ".runtime-scaffold"), join3(role.roleDir, ".runtime-scaffold"), changedFiles, ctx.dryRun);
4602
- copyMissingRecursive(join3(templateRoleDir, ".runtime-scaffold"), join3(role.roleDir, "runtime"), changedFiles, ctx.dryRun);
4603
- copyMissingRecursive(join3(templateRoleDir, ".scripts"), join3(role.roleDir, ".scripts"), changedFiles, ctx.dryRun, (source) => source.endsWith("sentinel.prompt.md.jinja"));
4604
- const promptSource = join3(templateRoleDir, ".scripts", "sentinel.prompt.md.jinja");
4605
- const promptTarget = join3(role.roleDir, ".scripts", "sentinel.prompt.md");
4606
- if (existsSync2(promptSource) && !existsSync2(promptTarget)) {
4607
- const prompt = readText(promptSource).replace(/\{\{ agent_id \}\}/g, role.agentId).replace(/\{\{ role \}\}/g, role.role).replace(/\{\{ target_repo \}\}/g, role.repo).replace(/\{\{ display_name \}\}/g, role.displayName || role.agentId);
4608
- writeIfDifferent(promptTarget, prompt, ctx.dryRun, changedFiles);
4699
+ const managedScripts = templateFiles(join3(templateRoleDir, ".scripts")).filter((rel) => rel !== "sentinel.prompt.md.jinja");
4700
+ for (const role of selection.roles) {
4701
+ const retirement = retireRuntimeSubmodule(ctx.repoRoot, role, changedFiles, ctx.dryRun);
4702
+ details.push(...retirement.details);
4703
+ if (!retirement.ok) {
4704
+ return { id: finding.id, title: finding.title, status: "blocked", summary: `Failed to retire ${role.role} runtime submodule metadata safely`, changedFiles, details: [retirement.error ?? "unknown runtime retirement failure"] };
4705
+ }
4706
+ if (!existsSync2(join3(role.roleDir, "SOUL.md"))) writeIfDifferent(join3(role.roleDir, "SOUL.md"), renderSoul(role), ctx.dryRun, changedFiles);
4707
+ writeIfDifferent(join3(role.roleDir, "hermes"), renderHermesWrapper(role, templateRoleDir), ctx.dryRun, changedFiles, 493);
4708
+ writeIfDifferent(join3(role.roleDir, ".gitignore"), readText(join3(templateRoleDir, ".gitignore.jinja")).replace(/\{\{\s*role\s*\}\}/g, role.role), ctx.dryRun, changedFiles);
4709
+ copyMissingRecursive(join3(templateRoleDir, ".runtime-scaffold"), join3(role.roleDir, ".runtime-scaffold"), changedFiles, ctx.dryRun);
4710
+ copyMissingRecursive(join3(templateRoleDir, ".runtime-scaffold"), join3(role.roleDir, "runtime"), changedFiles, ctx.dryRun);
4711
+ for (const rel of managedScripts) {
4712
+ const source = join3(templateRoleDir, ".scripts", rel);
4713
+ const executable = (lstatSync2(source).mode & 73) !== 0;
4714
+ writeIfDifferent(join3(role.roleDir, ".scripts", rel), readText(source), ctx.dryRun, changedFiles, executable ? 493 : void 0);
4715
+ }
4716
+ writeIfDifferent(join3(role.roleDir, ".scripts", "sentinel.prompt.md"), renderSentinelPrompt(role, templateRoleDir), ctx.dryRun, changedFiles);
4717
+ const profileMetaUpdated = upsertInheritedProfileMeta(join3(role.roleDir, "runtime", "profile.yaml"), changedFiles, ctx.dryRun);
4718
+ if (profileMetaUpdated) details.push(`updated ${profileMetaUpdated}`);
4719
+ const registryUpdated = upsertRegistryEntry(role, ctx.homeDir, changedFiles, ctx.dryRun);
4720
+ if (registryUpdated) details.push(`updated ${registryUpdated}`);
4609
4721
  }
4610
- const profileMetaUpdated = upsertInheritedProfileMeta(join3(role.roleDir, "runtime", "profile.yaml"), changedFiles, ctx.dryRun);
4611
- if (profileMetaUpdated) details.push(`updated ${profileMetaUpdated}`);
4612
- const registryUpdated = upsertRegistryEntry(role, ctx.homeDir, changedFiles, ctx.dryRun);
4613
- if (registryUpdated) details.push(`updated ${registryUpdated}`);
4614
4722
  return {
4615
4723
  id: finding.id,
4616
4724
  title: finding.title,
4617
4725
  status: changedFiles.length ? "applied" : "noop",
4618
- summary: changedFiles.length ? "PM scaffold normalized" : "No changes required",
4726
+ summary: changedFiles.length ? `${selection.roles.length} orchestrator scaffold(s) normalized` : "No changes required",
4619
4727
  changedFiles,
4620
4728
  details
4621
4729
  };
@@ -4922,8 +5030,9 @@ function createHermesChecks() {
4922
5030
  if (text2 === null) {
4923
5031
  details.push(`launcher missing: ${relative2(ctx.repoRoot, launcher)}`);
4924
5032
  } else {
4925
- if (/^HERMES_HOME="\$RUNTIME_HOME"\s*$/m.test(text2)) {
4926
- details.push(`launcher sets HERMES_HOME to the raw runtime path (disables shared auth + profile identity): ${relative2(ctx.repoRoot, launcher)}`);
5033
+ const assigned = /^HERMES_HOME=(.*)$/m.exec(text2)?.[1]?.trim();
5034
+ if (assigned !== void 0 && !isProfileHomeExpr(assigned)) {
5035
+ details.push(`launcher sets HERMES_HOME=${assigned} instead of the named profile dir (disables shared auth + profile identity): ${relative2(ctx.repoRoot, launcher)}`);
4927
5036
  }
4928
5037
  if (/HERMES_OAUTH_FILE/.test(text2)) {
4929
5038
  details.push(`launcher exports HERMES_OAUTH_FILE, which Hermes does not implement (dead config): ${relative2(ctx.repoRoot, launcher)}`);
@@ -4961,9 +5070,16 @@ function createHermesChecks() {
4961
5070
  const launcher = join3(role.roleDir, "hermes");
4962
5071
  const text2 = safeReadText(launcher);
4963
5072
  if (text2 !== null) {
4964
- const rewritten = rewriteLauncher(text2);
5073
+ const before = /^HERMES_HOME=(.*)$/m.exec(text2)?.[1]?.trim();
5074
+ const rewritten = rewriteLauncher(text2, role.profileName || role.agentId);
4965
5075
  if (rewritten !== text2) {
4966
- details.push(`rewrite launcher HERMES_HOME -> profile path: ${relative2(ctx.repoRoot, launcher)}`);
5076
+ const rel = relative2(ctx.repoRoot, launcher);
5077
+ if (before !== void 0 && !isProfileHomeExpr(before)) {
5078
+ details.push(`rewrite launcher HERMES_HOME ${before} -> ${plan.profileDir}: ${rel}`);
5079
+ }
5080
+ if (/HERMES_OAUTH_FILE/.test(text2)) {
5081
+ details.push(`strip dead HERMES_OAUTH_FILE export: ${rel}`);
5082
+ }
4967
5083
  writeIfDifferent(launcher, rewritten, ctx.dryRun, changedFiles, 493);
4968
5084
  }
4969
5085
  }
@@ -5000,6 +5116,7 @@ function createHermesChecks() {
5000
5116
  audit: (ctx) => {
5001
5117
  const roles = discoverRoles(ctx.repoRoot);
5002
5118
  const details = [];
5119
+ let malformedRoleGate = false;
5003
5120
  const registryPath2 = join3(ctx.homeDir, ".hermes", "agents-registry.yaml");
5004
5121
  const registry = readRegistry(registryPath2);
5005
5122
  if (!registry) {
@@ -5036,6 +5153,11 @@ function createHermesChecks() {
5036
5153
  details.push(`.project.json declares agent "${extra}" that no role.yaml claims`);
5037
5154
  }
5038
5155
  for (const role of roles) {
5156
+ const expectedBloodbankEnabled = roleBloodbankEnabled(role);
5157
+ if (expectedBloodbankEnabled === null) {
5158
+ details.push(`${relative2(ctx.repoRoot, role.roleYamlPath)} bloodbank.enabled must be the strict YAML boolean true or false`);
5159
+ malformedRoleGate = true;
5160
+ }
5039
5161
  const entry = registry[role.agentId];
5040
5162
  if (!entry) {
5041
5163
  details.push(`registry is missing an entry for ${role.agentId}`);
@@ -5050,6 +5172,11 @@ function createHermesChecks() {
5050
5172
  details.push(`registry hermes.bin for ${role.agentId} does not exist: ${bin}`);
5051
5173
  }
5052
5174
  const bloodbank = entry.bloodbank ?? {};
5175
+ if (typeof bloodbank.enabled !== "boolean") {
5176
+ details.push(`registry entry for ${role.agentId} bloodbank.enabled must be a strict boolean`);
5177
+ } else if (expectedBloodbankEnabled !== null && bloodbank.enabled !== expectedBloodbankEnabled) {
5178
+ details.push(`registry entry for ${role.agentId} bloodbank.enabled must match explicit role value ${expectedBloodbankEnabled}`);
5179
+ }
5053
5180
  if (bloodbank.gateway_scope !== "fleet" || bloodbank.target_agent_id !== role.agentId) {
5054
5181
  details.push(`registry entry for ${role.agentId} must advertise bloodbank { gateway_scope: fleet, target_agent_id: ${role.agentId} }`);
5055
5182
  }
@@ -5070,7 +5197,7 @@ function createHermesChecks() {
5070
5197
  status: details.length === 0 ? "pass" : "fail",
5071
5198
  summary: details.length === 0 ? "Fleet registry is in parity" : `${details.length} registry parity issue(s) detected`,
5072
5199
  details,
5073
- fixable: true
5200
+ fixable: !malformedRoleGate
5074
5201
  };
5075
5202
  },
5076
5203
  migrate: (ctx, finding) => {
@@ -5082,6 +5209,19 @@ function createHermesChecks() {
5082
5209
  return { id: finding.id, title: finding.title, status: "blocked", summary: `registry unreadable at ${registryPath2}`, changedFiles, details };
5083
5210
  }
5084
5211
  const roles = discoverRoles(ctx.repoRoot);
5212
+ const malformedRoleGates = roles.filter((role) => roleBloodbankEnabled(role) === null);
5213
+ if (malformedRoleGates.length > 0) {
5214
+ return {
5215
+ id: finding.id,
5216
+ title: finding.title,
5217
+ status: "blocked",
5218
+ summary: "Registry parity is blocked by malformed role Bloodbank gates",
5219
+ changedFiles,
5220
+ details: malformedRoleGates.map(
5221
+ (role) => `${relative2(ctx.repoRoot, role.roleYamlPath)} bloodbank.enabled must be the strict YAML boolean true or false`
5222
+ )
5223
+ };
5224
+ }
5085
5225
  const missingRoles = roles.filter((role) => !raw.includes(`${role.agentId}:`));
5086
5226
  for (const role of missingRoles) {
5087
5227
  const updated = upsertRegistryEntry(role, ctx.homeDir, changedFiles, ctx.dryRun);
@@ -5145,9 +5285,10 @@ function createHermesChecks() {
5145
5285
  dirty = true;
5146
5286
  }
5147
5287
  const bloodbank = entry.bloodbank ?? {};
5148
- if (bloodbank.gateway_scope !== "fleet" || bloodbank.target_agent_id !== role.agentId) {
5149
- details.push(`advertise fleet bloodbank routing for ${role.agentId}`);
5150
- entry.bloodbank = { gateway_scope: "fleet", target_agent_id: role.agentId };
5288
+ const expectedBloodbankEnabled = roleBloodbankEnabled(role) ?? false;
5289
+ if (bloodbank.enabled !== expectedBloodbankEnabled || bloodbank.gateway_scope !== "fleet" || bloodbank.target_agent_id !== role.agentId) {
5290
+ details.push(`normalize fleet bloodbank routing for ${role.agentId} with enabled=${expectedBloodbankEnabled}`);
5291
+ entry.bloodbank = { ...bloodbank, enabled: expectedBloodbankEnabled, gateway_scope: "fleet", target_agent_id: role.agentId };
5151
5292
  dirty = true;
5152
5293
  }
5153
5294
  const systemd = entry.systemd;
@@ -5307,10 +5448,10 @@ var AgentHooksRecipe = class extends Recipe {
5307
5448
  printNextSteps() {
5308
5449
  console.log("\u{1FA9D} Agent-hooks layer installed!");
5309
5450
  console.log(" Next steps:");
5310
- console.log(" 1. mise run skills-sync # sync .agents/skills.json into local CLI dirs");
5311
- console.log(" 2. mise run hooks-sync # generate .claude/settings.json + inject codex/kimi/hermes");
5451
+ console.log(" 1. mise run skills:sync # sync .agents/skills.json into local CLI dirs");
5452
+ console.log(" 2. mise run hooks:sync # generate .claude/settings.json + inject codex/kimi/hermes");
5312
5453
  console.log(" 3. git add .claude/settings.json .agents/hooks .agents/skills.json && commit (codex/kimi/hermes are per-dev)");
5313
- console.log(" 4. mise run hindsight-setup # set HINDSIGHT_OP_KEY_REF to your 1Password item first");
5454
+ console.log(" 4. mise run hindsight:setup # set HINDSIGHT_OP_KEY_REF to your 1Password item first");
5314
5455
  console.log(" 5. Optional per-dev hook opt-out: copy .agents/local.example.json -> .agents/local.json");
5315
5456
  }
5316
5457
  };
@@ -5662,6 +5803,9 @@ var PromptForAgentConfig = class extends Command {
5662
5803
  ctx.soulTone ??= "direct";
5663
5804
  ctx.modelProvider ??= "";
5664
5805
  ctx.modelName ??= "";
5806
+ ctx.modelBaseUrl ??= "";
5807
+ ctx.modelApiMode ??= "";
5808
+ ctx.modelKeyEnv ??= "";
5665
5809
  ctx.ticketProvider ??= detectTicketProvider(ctx.targetDir) ?? "plane";
5666
5810
  ctx.skipEmail ??= true;
5667
5811
  ctx.agentId = deriveAgentId(ctx.targetRepo, ctx.role);
@@ -5728,7 +5872,17 @@ function resolveVendoredTemplate(name) {
5728
5872
  var RunCopierTemplate = class extends Command {
5729
5873
  async invoke() {
5730
5874
  const ctx = this.context;
5731
- const { targetRepo, role, agentPurpose, soulTone, modelProvider, modelName } = ctx;
5875
+ const {
5876
+ targetRepo,
5877
+ role,
5878
+ agentPurpose,
5879
+ soulTone,
5880
+ modelProvider,
5881
+ modelName,
5882
+ modelBaseUrl,
5883
+ modelApiMode,
5884
+ modelKeyEnv
5885
+ } = ctx;
5732
5886
  const ticketProvider = ctx.ticketProvider ?? "plane";
5733
5887
  const profileName = ctx.profileName ?? (targetRepo && role ? deriveProfileName(targetRepo, role) : void 0);
5734
5888
  if (!targetRepo || !role) {
@@ -5795,6 +5949,12 @@ var RunCopierTemplate = class extends Command {
5795
5949
  "--data",
5796
5950
  `model_name=${modelName ?? ""}`,
5797
5951
  "--data",
5952
+ `model_base_url=${modelBaseUrl ?? ""}`,
5953
+ "--data",
5954
+ `model_api_mode=${modelApiMode ?? ""}`,
5955
+ "--data",
5956
+ `model_key_env=${modelKeyEnv ?? ""}`,
5957
+ "--data",
5798
5958
  `profile_name=${profileName ?? ""}`,
5799
5959
  "--data",
5800
5960
  `soul_tone=${soulTone ?? "direct"}`,
@@ -8101,31 +8261,33 @@ ${leaveBlock}`);
8101
8261
  const appended = [
8102
8262
  "",
8103
8263
  _WireMiseAgentHooks.MARKER + " (generated \u2014 see .agents/hooks/README.md)",
8264
+ // PJAN-61: task names use the colon namespace form. A colon is not legal
8265
+ // in a BARE toml key, so every header here MUST stay quoted.
8104
8266
  "[[watch_files]]",
8105
8267
  'patterns = [".agents/skills.json"]',
8106
- 'task = "skills-sync"',
8268
+ 'task = "skills:sync"',
8107
8269
  "",
8108
- "[tasks.skills-sync]",
8270
+ '[tasks."skills:sync"]',
8109
8271
  'description = "Sync skills from manifest to local CLI dirs"',
8110
8272
  'run = "sync-skills.py --scope project"',
8111
8273
  "",
8112
8274
  "[[watch_files]]",
8113
8275
  'patterns = [".agents/hooks/hooks.master.json"]',
8114
- 'task = "hooks-sync"',
8276
+ 'task = "hooks:sync"',
8115
8277
  "",
8116
- "[tasks.hooks-sync]",
8278
+ '[tasks."hooks:sync"]',
8117
8279
  'description = "Fan out hooks.master.json to each agent CLI (claude/codex/kimi/hermes)"',
8118
8280
  `run = "${cr}/.agents/hooks/sync.py --install"`,
8119
8281
  "",
8120
- "[tasks.hooks-check]",
8282
+ '[tasks."hooks:check"]',
8121
8283
  'description = "Drift gate: verify generated hook configs match hooks.master.json"',
8122
8284
  `run = "${cr}/.agents/hooks/sync.py --check"`,
8123
8285
  "",
8124
- "[tasks.hooks-uninstall]",
8286
+ '[tasks."hooks:uninstall"]',
8125
8287
  'description = "Remove per-user agent-hook injections (codex/kimi/hermes)"',
8126
8288
  `run = "${cr}/.agents/hooks/sync.py --uninstall"`,
8127
8289
  "",
8128
- "[tasks.hindsight-setup]",
8290
+ '[tasks."hindsight:setup"]',
8129
8291
  `description = "Provision this dev's shared project Hindsight key from 1Password into .env"`,
8130
8292
  `run = "${cr}/.mise/scripts/hindsight-setup.sh"`,
8131
8293
  "",
@@ -8942,11 +9104,14 @@ server.registerTool(
8942
9104
  inputSchema: {
8943
9105
  targetDir: z.string(),
8944
9106
  targetRepo: z.string().optional(),
8945
- role: z.enum(["pm", "dev", "review", "ops", "qa"]),
9107
+ role: z.enum(["pm", "director", "dev", "review", "ops", "qa"]),
8946
9108
  agentPurpose: z.string().optional(),
8947
9109
  soulTone: z.enum(["direct", "playful", "formal", "terse"]).optional(),
8948
9110
  modelProvider: z.string().optional(),
8949
9111
  modelName: z.string().optional(),
9112
+ modelBaseUrl: z.string().optional(),
9113
+ modelApiMode: z.enum(["", "chat_completions", "codex_responses", "anthropic_messages", "bedrock_converse", "codex_app_server"]).optional(),
9114
+ modelKeyEnv: z.string().regex(/^[A-Za-z_][A-Za-z0-9_]*$/).optional(),
8950
9115
  local: z.boolean().optional(),
8951
9116
  force: z.boolean().optional(),
8952
9117
  dryRun: z.boolean().optional(),
@@ -8973,6 +9138,9 @@ server.registerTool(
8973
9138
  soulTone: input.soulTone,
8974
9139
  modelProvider: input.modelProvider,
8975
9140
  modelName: input.modelName,
9141
+ modelBaseUrl: input.modelBaseUrl,
9142
+ modelApiMode: input.modelApiMode,
9143
+ modelKeyEnv: input.modelKeyEnv,
8976
9144
  ticketProvider: input.ticketProvider,
8977
9145
  force: input.force ?? false,
8978
9146
  dryRun: input.dryRun ?? false,