@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
package/dist/index.js CHANGED
@@ -912,8 +912,26 @@ var OP_INJECT_SCRIPT = `'{{config_root}}/${MATERIALIZE_ENV_SCRIPT_REL}'`;
912
912
  var PROVISION_PACKS_SCRIPT_REL = ".mise/scripts/provision-packs.py";
913
913
  var LEGACY_PROVISION_SCRIPT_REL = ".mise/scripts/provision-bmad-skills.py";
914
914
  var SYNC_SKILLS_SCRIPT_REL = ".mise/scripts/sync-skills.py";
915
- var PROVISION_PACKS_TASK = "skills-provision-packs";
915
+ var LINK_AGENTFILES_TASK = "link:agentfiles";
916
+ var SKILLS_SYNC_TASK = "skills:sync";
917
+ var PROVISION_PACKS_TASK = "skills:provision:packs";
916
918
  var LEGACY_PROVISION_TASK = "skills-provision-bmad";
919
+ var RETIRED_TASK_RENAMES = [
920
+ ["link-agentfiles", LINK_AGENTFILES_TASK],
921
+ ["skills-sync", SKILLS_SYNC_TASK],
922
+ ["skills-provision-packs", PROVISION_PACKS_TASK],
923
+ ["hooks-sync", "hooks:sync"],
924
+ ["hooks-check", "hooks:check"],
925
+ ["hooks-uninstall", "hooks:uninstall"],
926
+ ["hindsight-setup", "hindsight:setup"]
927
+ ];
928
+ function taskHeader(name) {
929
+ return `[tasks."${name}"]`;
930
+ }
931
+ function taskHeaderPattern(name) {
932
+ const esc = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
933
+ return new RegExp(`^\\[tasks\\.(?:"${esc}"|${esc})\\]$`);
934
+ }
917
935
  var PROVISION_PACKS_SCRIPT = `python3 '{{config_root}}/${PROVISION_PACKS_SCRIPT_REL}'`;
918
936
  var LEGACY_PROVISION_BMAD_SKILLS_SCRIPT = `python3 '{{config_root}}/${LEGACY_PROVISION_SCRIPT_REL}'`;
919
937
  var SYNC_SKILLS_SCRIPT = `python3 '{{config_root}}/${SYNC_SKILLS_SCRIPT_REL}' --scope project`;
@@ -947,22 +965,22 @@ var LINK_AGENTFILES_HOOK_ENTRIES = [
947
965
  ];
948
966
  var LINK_AGENTFILES_WATCH_TASK_BLOCK = `[[watch_files]]
949
967
  patterns = ["AGENTS.md"]
950
- task = "link-agentfiles"
968
+ task = "${LINK_AGENTFILES_TASK}"
951
969
 
952
970
  [[watch_files]]
953
971
  patterns = [".agents/skills.json"]
954
- task = "skills-sync"
972
+ task = "${SKILLS_SYNC_TASK}"
955
973
 
956
- [tasks.link-agentfiles]
974
+ ${taskHeader(LINK_AGENTFILES_TASK)}
957
975
  description = "Symlink all agent files to AGENTS.md"
958
976
  run = "'{{config_root}}/.mise/scripts/link-agentfiles.sh'"
959
977
 
960
- [tasks.skills-sync]
978
+ ${taskHeader(SKILLS_SYNC_TASK)}
961
979
  description = "Sync skills from manifest to local CLI dirs"
962
980
  depends = ["${PROVISION_PACKS_TASK}"]
963
981
  run = ${JSON.stringify(SYNC_SKILLS_SCRIPT)}
964
982
 
965
- [tasks.${PROVISION_PACKS_TASK}]
983
+ ${taskHeader(PROVISION_PACKS_TASK)}
966
984
  description = "Provision every Skillex pack declared in .agents/skills.json"
967
985
  run = ${JSON.stringify(PROVISION_PACKS_SCRIPT)}`;
968
986
  var VERSIONING_BLOCK = `# >>> mise-versioning >>> (managed block \u2014 do not edit by hand; re-run init to update)
@@ -1124,6 +1142,7 @@ function discoverRoles(repoRoot) {
1124
1142
  ticketProviderName: yamlGet(text3, "ticket_provider.name"),
1125
1143
  ticketProviderBoardId: yamlGet(text3, "ticket_provider.board_id"),
1126
1144
  ticketProviderIdentifier: yamlGet(text3, "plane.identifier"),
1145
+ bloodbankEnabled: yamlGet(text3, "bloodbank.enabled"),
1127
1146
  deploymentSystemd: yamlGet(text3, "deployment.systemd"),
1128
1147
  legacyReconcileEnabled: yamlGet(text3, "reconcile.enabled"),
1129
1148
  legacyReconcileGraceHours: yamlGet(text3, "reconcile.grace_hours"),
@@ -2326,10 +2345,41 @@ function upsertOpInjectHook(text3) {
2326
2345
  [OP_INJECT_SCRIPT]
2327
2346
  );
2328
2347
  }
2348
+ function retiredTaskNameIssues(text3) {
2349
+ const issues = [];
2350
+ for (const [oldName, newName] of RETIRED_TASK_RENAMES) {
2351
+ const esc = oldName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2352
+ const present = new RegExp(
2353
+ `^\\[tasks\\.(?:"${esc}"|${esc})\\]|^\\s*task\\s*=\\s*"${esc}"|^\\s*depends\\s*=\\s*\\[[^\\]]*"${esc}"`,
2354
+ "m"
2355
+ ).test(text3);
2356
+ if (present) issues.push(`mise.toml still uses the retired task name "${oldName}" (renamed to "${newName}")`);
2357
+ }
2358
+ return issues;
2359
+ }
2360
+ function renameRetiredMiseTasks(text3) {
2361
+ let out = text3;
2362
+ for (const [oldName, newName] of RETIRED_TASK_RENAMES) {
2363
+ const esc = oldName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2364
+ out = out.replace(new RegExp(`^\\[tasks\\.(?:"${esc}"|${esc})\\]`, "gm"), taskHeader(newName));
2365
+ out = out.replace(new RegExp(`^(\\s*task\\s*=\\s*)"${esc}"`, "gm"), `$1"${newName}"`);
2366
+ out = out.replace(new RegExp(`\\bmise run ${esc}\\b`, "g"), `mise run ${newName}`);
2367
+ }
2368
+ return out.replace(/^(\s*depends\s*=\s*)(\[[^\]]*\])/gm, (_whole, head, arr) => {
2369
+ let next = arr;
2370
+ for (const [oldName, newName] of RETIRED_TASK_RENAMES) {
2371
+ next = next.split(`"${oldName}"`).join(`"${newName}"`);
2372
+ }
2373
+ return head + next;
2374
+ });
2375
+ }
2329
2376
  function upsertLinkAgentfilesBlock(text3, ctx) {
2330
- const withPath = upsertMisePath(text3, requiredMisePathEntries(ctx));
2331
- let cleaned = removeTomlSection(withPath, /^\[tasks\.link-agentfiles\]$/, /link-agentfiles/, { includePrecedingComments: false });
2377
+ const withPath = upsertMisePath(renameRetiredMiseTasks(text3), requiredMisePathEntries(ctx));
2378
+ let cleaned = removeTomlSection(withPath, taskHeaderPattern(LINK_AGENTFILES_TASK), /link-agentfiles/, { includePrecedingComments: false });
2379
+ cleaned = removeTomlSection(cleaned, /^\[tasks\.link-agentfiles\]$/, /link-agentfiles/, { includePrecedingComments: false });
2380
+ cleaned = removeTomlSection(cleaned, taskHeaderPattern(SKILLS_SYNC_TASK), void 0, { includePrecedingComments: false });
2332
2381
  cleaned = removeTomlSection(cleaned, /^\[tasks\.skills-sync\]$/, void 0, { includePrecedingComments: false });
2382
+ cleaned = removeTomlSection(cleaned, taskHeaderPattern(PROVISION_PACKS_TASK), void 0, { includePrecedingComments: false });
2333
2383
  cleaned = removeTomlSection(cleaned, /^\[tasks\.skills-provision-packs\]$/, void 0, { includePrecedingComments: false });
2334
2384
  cleaned = removeTomlSection(cleaned, /^\[tasks\.skills-provision-bmad\]$/, void 0, { includePrecedingComments: false });
2335
2385
  cleaned = removeTomlSection(cleaned, /^\[tasks\.link-project-skills-to-clis\]$/, void 0, { includePrecedingComments: false });
@@ -2571,7 +2621,7 @@ You are **${role.displayName || role.agentId}** \u2014 a Hermes agent provisione
2571
2621
 
2572
2622
  ## Scope
2573
2623
 
2574
- 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.
2624
+ 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.
2575
2625
 
2576
2626
  ## Tone
2577
2627
 
@@ -2586,51 +2636,62 @@ ${roleSpecific}
2586
2636
  Your memory is stored locally at \`./runtime/memories/\`. Use durable memory deliberately and keep \`memories/MEMORY.md\` current.
2587
2637
  `;
2588
2638
  }
2589
- function renderHermesWrapper(role) {
2590
- return `#!/usr/bin/env bash
2591
- # Launcher for ${role.agentId}. Resolves HERMES_HOME to the local runtime.
2592
-
2593
- set -euo pipefail
2594
-
2595
- ROLE_DIR="$(cd "$(dirname "$0")" && pwd)"
2596
- RUNTIME_HOME="$ROLE_DIR/runtime"
2597
-
2598
- FLEET_ENV="{HERMES_FLEET_ENV:-$HOME/.hermes/fleet.env}"
2599
- if [[ -f "$FLEET_ENV" ]]; then
2600
- # shellcheck disable=SC1090
2601
- source "$FLEET_ENV"
2602
- fi
2603
-
2604
- HERMES_BIN="{HERMES_BIN:-{HERMES_FLEET_BIN:-$HOME/.hermes/hermes-agent/.venv/bin/hermes}}"
2605
- CODEX_HOME="{CODEX_HOME:-{HERMES_FLEET_CODEX_HOME:-$HOME/.codex}}"
2606
-
2607
- FLEET_HOME="{HERMES_FLEET_HOME:-$HOME/.hermes}"
2608
- PROFILE_NAME="{HERMES_PROFILE_NAME:-${role.profileName || role.agentId}}"
2609
-
2610
- # Singleton-runtime contract: HERMES_HOME MUST be the named profile dir, never
2611
- # the raw runtime path. Hermes treats any HERMES_HOME that is neither under
2612
- # ~/.hermes nor a child of a "profiles" dir as its own standalone root, which
2613
- # makes get_active_profile_name() report "default" and _global_auth_file_path()
2614
- # return None -- silently disabling shared fleet auth and giving every agent a
2615
- # divergent config.yaml. The profile dir is a REAL dir whose shared entries
2616
- # (config.yaml, .env, skills) symlink to the fleet root and whose person-owned
2617
- # entries (memories, sessions, state.db, workspace) symlink into $RUNTIME_HOME.
2618
- HERMES_HOME="$FLEET_HOME/profiles/$PROFILE_NAME"
2619
-
2620
- if [[ ! -d "$RUNTIME_HOME" ]]; then
2621
- echo "hermes: local runtime not provisioned at $RUNTIME_HOME" >&2
2622
- echo " fix: run the role's .scripts/20-runtime-repo.sh" >&2
2623
- exit 1
2624
- fi
2625
-
2626
- if [[ ! -d "$HERMES_HOME" ]]; then
2627
- echo "hermes: profile not provisioned at $HERMES_HOME" >&2
2628
- echo " fix: pj migrate hermes.runtime-singleton" >&2
2629
- exit 1
2630
- fi
2631
-
2632
- exec env HERMES_HOME="$HERMES_HOME" HERMES_FLEET_ENV="$FLEET_ENV" CODEX_HOME="$CODEX_HOME" "$HERMES_BIN" "$@"
2633
- `.replace(/\u0010/g, "$");
2639
+ function renderHermesWrapper(role, templateRoleDir) {
2640
+ return readText(join3(templateRoleDir, "hermes.jinja")).replace(/\{\{\s*agent_id\s*\}\}/g, role.agentId);
2641
+ }
2642
+ function templateFiles(sourceDir, current = sourceDir) {
2643
+ if (!existsSync2(current)) return [];
2644
+ const files = [];
2645
+ for (const entry of readdirSync2(current, { withFileTypes: true })) {
2646
+ if (entry.name === "__pycache__" || entry.name.endsWith(".pyc") || entry.name.endsWith(".pyo")) continue;
2647
+ const sourcePath = join3(current, entry.name);
2648
+ if (entry.isDirectory()) files.push(...templateFiles(sourceDir, sourcePath));
2649
+ else if (entry.isFile()) files.push(relative2(sourceDir, sourcePath));
2650
+ }
2651
+ return files.sort();
2652
+ }
2653
+ function managedHermesScaffoldRoles(ctx) {
2654
+ const discovered = discoverRoles(ctx.repoRoot);
2655
+ const declared = readDeclaredAgents(ctx).filter((entry) => entry.role === "pm" || entry.role === "director");
2656
+ if (declared.length === 0) {
2657
+ const orchestrators = discovered.filter((role) => role.role === "pm" || role.role === "director");
2658
+ 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`);
2659
+ return {
2660
+ roles: orchestrators.filter((role) => roleBloodbankEnabled(role) !== null),
2661
+ blockers: blockers2
2662
+ };
2663
+ }
2664
+ const roles = [];
2665
+ const blockers = [];
2666
+ for (const entry of declared) {
2667
+ if (!entry.roleDir) {
2668
+ blockers.push(`agents.${entry.agentId}.role_dir missing`);
2669
+ continue;
2670
+ }
2671
+ const roleDir = resolve3(ctx.repoRoot, entry.roleDir);
2672
+ if (!isContainedBy(ctx.repoRoot, roleDir)) {
2673
+ blockers.push(`agents.${entry.agentId}.role_dir resolves outside the project`);
2674
+ continue;
2675
+ }
2676
+ const role = discovered.find((candidate) => resolve3(candidate.roleDir) === roleDir);
2677
+ if (!role) {
2678
+ blockers.push(`agents.${entry.agentId}.role_dir ${entry.roleDir} missing role.yaml`);
2679
+ continue;
2680
+ }
2681
+ if (role.agentId !== entry.agentId || role.role !== entry.role) {
2682
+ blockers.push(`agents.${entry.agentId} identity does not match ${entry.roleDir}/role.yaml`);
2683
+ continue;
2684
+ }
2685
+ if (roleBloodbankEnabled(role) === null) {
2686
+ blockers.push(`${entry.roleDir}/role.yaml bloodbank.enabled must be the strict YAML boolean true or false`);
2687
+ continue;
2688
+ }
2689
+ roles.push(role);
2690
+ }
2691
+ return { roles, blockers };
2692
+ }
2693
+ function renderSentinelPrompt(role, templateRoleDir) {
2694
+ 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");
2634
2695
  }
2635
2696
  function copyMissingRecursive(sourceDir, targetDir, changedFiles, dryRun, skip) {
2636
2697
  if (!existsSync2(sourceDir)) return;
@@ -2727,6 +2788,8 @@ function upsertRegistryEntry(role, homeDir, changedFiles, dryRun) {
2727
2788
  const path = registryPath(homeDir);
2728
2789
  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";
2729
2790
  if (current.includes(`${role.agentId}:`)) return null;
2791
+ const enabled = roleBloodbankEnabled(role);
2792
+ if (enabled === null) return null;
2730
2793
  const block = ` ${role.agentId}:
2731
2794
  repo: ${role.repo}
2732
2795
  role: ${role.role}
@@ -2742,6 +2805,7 @@ function upsertRegistryEntry(role, homeDir, changedFiles, dryRun) {
2742
2805
  identifier: ${ctxEscape(role.ticketProviderIdentifier)}
2743
2806
  runtime_repo: ${ctxEscape(role.runtimeRepo)}
2744
2807
  bloodbank:
2808
+ enabled: ${enabled ? "true" : "false"}
2745
2809
  gateway_scope: fleet
2746
2810
  target_agent_id: ${role.agentId}
2747
2811
  systemd:
@@ -2754,6 +2818,11 @@ ${block}`) : `${current.replace(/\s*$/, "\n")}${block}`;
2754
2818
  if (!dryRun) writeText(path, next);
2755
2819
  return path;
2756
2820
  }
2821
+ function roleBloodbankEnabled(role) {
2822
+ if (role.bloodbankEnabled === "" || role.bloodbankEnabled === "false") return false;
2823
+ if (role.bloodbankEnabled === "true") return true;
2824
+ return null;
2825
+ }
2757
2826
  function profileMetaInheritsDefault(path) {
2758
2827
  const text3 = safeReadText(path);
2759
2828
  return Boolean(
@@ -3220,8 +3289,30 @@ function dropDeclaredAgent(ctx, agentId, changedFiles, details) {
3220
3289
  if (!ctx.dryRun) writeText(path, `${JSON.stringify(doc, null, 2)}
3221
3290
  `);
3222
3291
  }
3223
- function rewriteLauncher(text3) {
3224
- let next = text3.replace(/^HERMES_HOME="\$RUNTIME_HOME"\s*$/m, 'HERMES_HOME="$FLEET_HOME/profiles/$PROFILE_NAME"');
3292
+ function isProfileHomeExpr(assigned) {
3293
+ const bare = assigned.replace(/^["']|["']$/g, "");
3294
+ return bare === "$FLEET_HOME/profiles/$PROFILE_NAME" || /^\$\{?HERMES_FLEET_HOME.*\}?\/profiles\//.test(bare) || /\/\.hermes\/profiles\/[^/]+$/.test(bare);
3295
+ }
3296
+ function rewriteLauncher(text3, profileName) {
3297
+ let next = text3;
3298
+ const assigned = /^HERMES_HOME=(.*)$/m.exec(next)?.[1]?.trim();
3299
+ if (assigned !== void 0 && !isProfileHomeExpr(assigned)) {
3300
+ const name = profileName ? `\${HERMES_PROFILE_NAME:-${profileName}}` : '${HERMES_PROFILE_NAME:-$(basename "$ROLE_DIR")}';
3301
+ next = next.replace(
3302
+ /^HERMES_HOME=(.*)$/m,
3303
+ [
3304
+ `RUNTIME_HOME=$1`,
3305
+ `FLEET_HOME="\${HERMES_FLEET_HOME:-$HOME/.hermes}"`,
3306
+ `PROFILE_NAME="${name}"`,
3307
+ `# Singleton-runtime contract: HERMES_HOME MUST be the named profile dir.`,
3308
+ `HERMES_HOME="$FLEET_HOME/profiles/$PROFILE_NAME"`
3309
+ ].join("\n")
3310
+ );
3311
+ next = next.replace(
3312
+ /if \[\[ ! -d "\$HERMES_HOME" \]\]; then\n(\s*)echo "hermes: local runtime not provisioned at \$HERMES_HOME"/,
3313
+ 'if [[ ! -d "$RUNTIME_HOME" ]]; then\n$1echo "hermes: local runtime not provisioned at $RUNTIME_HOME"'
3314
+ );
3315
+ }
3225
3316
  next = next.replace(/^HERMES_OAUTH_FILE=.*\n/m, "");
3226
3317
  next = next.replace(/\s*HERMES_OAUTH_FILE="\$HERMES_OAUTH_FILE"/g, "");
3227
3318
  next = next.replace(
@@ -3770,7 +3861,8 @@ function createMiseChecks() {
3770
3861
  if (missingPathValues.length) details.push(`[env]._.path should include ${missingPathValues.join(", ")}`);
3771
3862
  if (!text3.includes("'{{config_root}}/.mise/scripts/link-agentfiles.sh'")) details.push("link-agentfiles hook must use single-quoted {{config_root}} guard");
3772
3863
  if (!text3.includes('patterns = ["AGENTS.md"]')) details.push("watch_files must monitor AGENTS.md");
3773
- if (!text3.includes('task = "link-agentfiles"')) details.push("watch_files must dispatch link-agentfiles task");
3864
+ if (!text3.includes(`task = "${LINK_AGENTFILES_TASK}"`)) details.push(`watch_files must dispatch the ${LINK_AGENTFILES_TASK} task`);
3865
+ details.push(...retiredTaskNameIssues(text3));
3774
3866
  return {
3775
3867
  id: "mise.config-root",
3776
3868
  title: "mise config_root + AGENTS link hooks",
@@ -3818,7 +3910,7 @@ function createMiseChecks() {
3818
3910
  status: changedFiles.length ? "applied" : "noop",
3819
3911
  summary: changedFiles.length ? "Updated mise AGENTS-linking contract" : "No changes required",
3820
3912
  changedFiles,
3821
- details: changedFiles.length ? ["Normalized hooks/watch_files/tasks.link-agentfiles block and script"] : []
3913
+ details: changedFiles.length ? [`Normalized hooks/watch_files/tasks."${LINK_AGENTFILES_TASK}" block and script`] : []
3822
3914
  };
3823
3915
  }
3824
3916
  },
@@ -4029,8 +4121,9 @@ function createAgentHooksChecks() {
4029
4121
  details.push("mise.toml still invokes the missing bare sync-skills.py executable");
4030
4122
  }
4031
4123
  if (!mise?.includes('patterns = [".agents/skills.json"]')) details.push("mise.toml should watch .agents/skills.json");
4032
- if (!mise?.includes("[tasks.skills-sync]")) details.push("mise.toml should define a skills-sync task");
4033
- if (!mise?.includes(`depends = ["${PROVISION_PACKS_TASK}"]`)) details.push(`skills-sync task should depend on ${PROVISION_PACKS_TASK}`);
4124
+ if (!mise?.includes(taskHeader(SKILLS_SYNC_TASK))) details.push(`mise.toml should define a ${SKILLS_SYNC_TASK} task`);
4125
+ if (!mise?.includes(`depends = ["${PROVISION_PACKS_TASK}"]`)) details.push(`${SKILLS_SYNC_TASK} task should depend on ${PROVISION_PACKS_TASK}`);
4126
+ if (mise) details.push(...retiredTaskNameIssues(mise));
4034
4127
  for (const [rel, label] of [
4035
4128
  [PROVISION_PACKS_SCRIPT_REL, "Skillex pack provisioning script"],
4036
4129
  [SYNC_SKILLS_SCRIPT_REL, "Project-local skills sync engine"]
@@ -4923,75 +5016,90 @@ function createHermesChecks() {
4923
5016
  return [
4924
5017
  {
4925
5018
  id: "hermes.pm-scaffold",
4926
- title: "Hermes PM scaffold parity",
5019
+ title: "Hermes orchestrator scaffold parity",
4927
5020
  audit: (ctx) => {
4928
- const roles = discoverRoles(ctx.repoRoot);
4929
- const role = roles.find((item) => item.role === "pm");
4930
- if (!role) {
4931
- return { id: "hermes.pm-scaffold", title: "Hermes PM scaffold parity", status: "skip", summary: "No pm role present", details: [], fixable: false };
4932
- }
4933
- const details = [];
4934
- 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"]) {
4935
- if (!existsSync2(join3(role.roleDir, rel))) details.push(`missing ${relative2(ctx.repoRoot, join3(role.roleDir, rel))}`);
5021
+ const selection = managedHermesScaffoldRoles(ctx);
5022
+ if (selection.roles.length === 0 && selection.blockers.length === 0) {
5023
+ return { id: "hermes.pm-scaffold", title: "Hermes orchestrator scaffold parity", status: "skip", summary: "No provisioned pm or director role present", details: [], fixable: false };
4936
5024
  }
4937
- if (hasRuntimeSubmoduleMapping(ctx.repoRoot, role)) {
4938
- details.push(".gitmodules contains retired pm runtime submodule mapping");
4939
- }
4940
- if (!profileMetaInheritsDefault(join3(role.roleDir, "runtime", "profile.yaml"))) {
4941
- details.push("runtime/profile.yaml missing inherited default config metadata");
5025
+ const details = [...selection.blockers];
5026
+ const templateRoleDir = join3(ctx.pjanglerRoot, "templates", "hermes-agent", "template");
5027
+ const managedScripts = templateFiles(join3(templateRoleDir, ".scripts")).filter((rel) => rel !== "sentinel.prompt.md.jinja");
5028
+ for (const role of selection.roles) {
5029
+ const prefix = role.agentId || role.role;
5030
+ for (const rel of ["role.yaml", "SOUL.md", ".runtime-scaffold/README.md", "runtime/memories/MEMORY.md"]) {
5031
+ if (!existsSync2(join3(role.roleDir, rel))) details.push(`${prefix}: missing ${relative2(ctx.repoRoot, join3(role.roleDir, rel))}`);
5032
+ }
5033
+ const wrapper = join3(role.roleDir, "hermes");
5034
+ const expectedWrapper = renderHermesWrapper(role, templateRoleDir);
5035
+ if (!existsSync2(wrapper)) details.push(`${prefix}: missing ${relative2(ctx.repoRoot, wrapper)}`);
5036
+ else if (safeReadText(wrapper) !== expectedWrapper) details.push(`${prefix}: stale ${relative2(ctx.repoRoot, wrapper)}`);
5037
+ const expectedIgnore = readText(join3(templateRoleDir, ".gitignore.jinja")).replace(/\{\{\s*role\s*\}\}/g, role.role);
5038
+ const ignorePath = join3(role.roleDir, ".gitignore");
5039
+ if (!existsSync2(ignorePath)) details.push(`${prefix}: missing ${relative2(ctx.repoRoot, ignorePath)}`);
5040
+ else if (safeReadText(ignorePath) !== expectedIgnore) details.push(`${prefix}: stale ${relative2(ctx.repoRoot, ignorePath)}`);
5041
+ for (const rel of managedScripts) {
5042
+ const source = join3(templateRoleDir, ".scripts", rel);
5043
+ const target = join3(role.roleDir, ".scripts", rel);
5044
+ if (!existsSync2(target)) details.push(`${prefix}: missing ${relative2(ctx.repoRoot, target)}`);
5045
+ else if (safeReadText(target) !== readText(source)) details.push(`${prefix}: stale ${relative2(ctx.repoRoot, target)}`);
5046
+ }
5047
+ const promptPath = join3(role.roleDir, ".scripts", "sentinel.prompt.md");
5048
+ if (!existsSync2(promptPath)) details.push(`${prefix}: missing ${relative2(ctx.repoRoot, promptPath)}`);
5049
+ else if (safeReadText(promptPath) !== renderSentinelPrompt(role, templateRoleDir)) details.push(`${prefix}: stale ${relative2(ctx.repoRoot, promptPath)}`);
5050
+ if (hasRuntimeSubmoduleMapping(ctx.repoRoot, role)) details.push(`${prefix}: .gitmodules contains retired ${role.role} runtime submodule mapping`);
5051
+ if (!profileMetaInheritsDefault(join3(role.roleDir, "runtime", "profile.yaml"))) details.push(`${prefix}: runtime/profile.yaml missing inherited default config metadata`);
5052
+ const registry = safeReadText(registryPath(ctx.homeDir));
5053
+ if (!registry?.includes(`${role.agentId}:`)) details.push(`fleet registry missing ${role.agentId}`);
4942
5054
  }
4943
- const registry = safeReadText(registryPath(ctx.homeDir));
4944
- if (!registry?.includes(`${role.agentId}:`)) details.push(`fleet registry missing ${role.agentId}`);
4945
5055
  return {
4946
5056
  id: "hermes.pm-scaffold",
4947
- title: "Hermes PM scaffold parity",
5057
+ title: "Hermes orchestrator scaffold parity",
4948
5058
  status: details.length === 0 ? "pass" : "fail",
4949
- summary: details.length === 0 ? "PM scaffold parity verified" : `${details.length} PM scaffold issue(s) detected`,
5059
+ summary: details.length === 0 ? `${selection.roles.length} orchestrator scaffold(s) verified` : `${details.length} orchestrator scaffold issue(s) detected`,
4950
5060
  details,
4951
- fixable: true
5061
+ fixable: selection.blockers.length === 0
4952
5062
  };
4953
5063
  },
4954
5064
  migrate: (ctx, finding) => {
4955
- const role = discoverRoles(ctx.repoRoot).find((item) => item.role === "pm");
5065
+ const selection = managedHermesScaffoldRoles(ctx);
4956
5066
  const changedFiles = [];
4957
5067
  const details = [];
4958
- if (!role) {
4959
- return { id: finding.id, title: finding.title, status: "blocked", summary: "No pm role present", changedFiles, details: [] };
5068
+ if (selection.blockers.length > 0) {
5069
+ return { id: finding.id, title: finding.title, status: "blocked", summary: "Provisioned orchestrator manifest is invalid", changedFiles, details: selection.blockers };
4960
5070
  }
4961
- const retirement = retireRuntimeSubmodule(ctx.repoRoot, role, changedFiles, ctx.dryRun);
4962
- details.push(...retirement.details);
4963
- if (!retirement.ok) {
4964
- return {
4965
- id: finding.id,
4966
- title: finding.title,
4967
- status: "blocked",
4968
- summary: "Failed to retire PM runtime submodule metadata safely",
4969
- changedFiles,
4970
- details: [retirement.error ?? "unknown runtime retirement failure"]
4971
- };
5071
+ if (selection.roles.length === 0) {
5072
+ return { id: finding.id, title: finding.title, status: "blocked", summary: "No provisioned pm or director role present", changedFiles, details: [] };
4972
5073
  }
4973
5074
  const templateRoleDir = join3(ctx.pjanglerRoot, "templates", "hermes-agent", "template");
4974
- writeIfDifferent(join3(role.roleDir, "SOUL.md"), renderSoul(role), ctx.dryRun, changedFiles);
4975
- writeIfDifferent(join3(role.roleDir, "hermes"), renderHermesWrapper(role), ctx.dryRun, changedFiles, 493);
4976
- writeIfDifferent(join3(role.roleDir, ".gitignore"), readText(join3(templateRoleDir, ".gitignore.jinja")).replace(/\{\{ role \}\}/g, role.role), ctx.dryRun, changedFiles);
4977
- copyMissingRecursive(join3(templateRoleDir, ".runtime-scaffold"), join3(role.roleDir, ".runtime-scaffold"), changedFiles, ctx.dryRun);
4978
- copyMissingRecursive(join3(templateRoleDir, ".runtime-scaffold"), join3(role.roleDir, "runtime"), changedFiles, ctx.dryRun);
4979
- copyMissingRecursive(join3(templateRoleDir, ".scripts"), join3(role.roleDir, ".scripts"), changedFiles, ctx.dryRun, (source) => source.endsWith("sentinel.prompt.md.jinja"));
4980
- const promptSource = join3(templateRoleDir, ".scripts", "sentinel.prompt.md.jinja");
4981
- const promptTarget = join3(role.roleDir, ".scripts", "sentinel.prompt.md");
4982
- if (existsSync2(promptSource) && !existsSync2(promptTarget)) {
4983
- 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);
4984
- writeIfDifferent(promptTarget, prompt, ctx.dryRun, changedFiles);
4985
- }
4986
- const profileMetaUpdated = upsertInheritedProfileMeta(join3(role.roleDir, "runtime", "profile.yaml"), changedFiles, ctx.dryRun);
4987
- if (profileMetaUpdated) details.push(`updated ${profileMetaUpdated}`);
4988
- const registryUpdated = upsertRegistryEntry(role, ctx.homeDir, changedFiles, ctx.dryRun);
4989
- if (registryUpdated) details.push(`updated ${registryUpdated}`);
5075
+ const managedScripts = templateFiles(join3(templateRoleDir, ".scripts")).filter((rel) => rel !== "sentinel.prompt.md.jinja");
5076
+ for (const role of selection.roles) {
5077
+ const retirement = retireRuntimeSubmodule(ctx.repoRoot, role, changedFiles, ctx.dryRun);
5078
+ details.push(...retirement.details);
5079
+ if (!retirement.ok) {
5080
+ 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"] };
5081
+ }
5082
+ if (!existsSync2(join3(role.roleDir, "SOUL.md"))) writeIfDifferent(join3(role.roleDir, "SOUL.md"), renderSoul(role), ctx.dryRun, changedFiles);
5083
+ writeIfDifferent(join3(role.roleDir, "hermes"), renderHermesWrapper(role, templateRoleDir), ctx.dryRun, changedFiles, 493);
5084
+ writeIfDifferent(join3(role.roleDir, ".gitignore"), readText(join3(templateRoleDir, ".gitignore.jinja")).replace(/\{\{\s*role\s*\}\}/g, role.role), ctx.dryRun, changedFiles);
5085
+ copyMissingRecursive(join3(templateRoleDir, ".runtime-scaffold"), join3(role.roleDir, ".runtime-scaffold"), changedFiles, ctx.dryRun);
5086
+ copyMissingRecursive(join3(templateRoleDir, ".runtime-scaffold"), join3(role.roleDir, "runtime"), changedFiles, ctx.dryRun);
5087
+ for (const rel of managedScripts) {
5088
+ const source = join3(templateRoleDir, ".scripts", rel);
5089
+ const executable = (lstatSync2(source).mode & 73) !== 0;
5090
+ writeIfDifferent(join3(role.roleDir, ".scripts", rel), readText(source), ctx.dryRun, changedFiles, executable ? 493 : void 0);
5091
+ }
5092
+ writeIfDifferent(join3(role.roleDir, ".scripts", "sentinel.prompt.md"), renderSentinelPrompt(role, templateRoleDir), ctx.dryRun, changedFiles);
5093
+ const profileMetaUpdated = upsertInheritedProfileMeta(join3(role.roleDir, "runtime", "profile.yaml"), changedFiles, ctx.dryRun);
5094
+ if (profileMetaUpdated) details.push(`updated ${profileMetaUpdated}`);
5095
+ const registryUpdated = upsertRegistryEntry(role, ctx.homeDir, changedFiles, ctx.dryRun);
5096
+ if (registryUpdated) details.push(`updated ${registryUpdated}`);
5097
+ }
4990
5098
  return {
4991
5099
  id: finding.id,
4992
5100
  title: finding.title,
4993
5101
  status: changedFiles.length ? "applied" : "noop",
4994
- summary: changedFiles.length ? "PM scaffold normalized" : "No changes required",
5102
+ summary: changedFiles.length ? `${selection.roles.length} orchestrator scaffold(s) normalized` : "No changes required",
4995
5103
  changedFiles,
4996
5104
  details
4997
5105
  };
@@ -5298,8 +5406,9 @@ function createHermesChecks() {
5298
5406
  if (text3 === null) {
5299
5407
  details.push(`launcher missing: ${relative2(ctx.repoRoot, launcher)}`);
5300
5408
  } else {
5301
- if (/^HERMES_HOME="\$RUNTIME_HOME"\s*$/m.test(text3)) {
5302
- details.push(`launcher sets HERMES_HOME to the raw runtime path (disables shared auth + profile identity): ${relative2(ctx.repoRoot, launcher)}`);
5409
+ const assigned = /^HERMES_HOME=(.*)$/m.exec(text3)?.[1]?.trim();
5410
+ if (assigned !== void 0 && !isProfileHomeExpr(assigned)) {
5411
+ details.push(`launcher sets HERMES_HOME=${assigned} instead of the named profile dir (disables shared auth + profile identity): ${relative2(ctx.repoRoot, launcher)}`);
5303
5412
  }
5304
5413
  if (/HERMES_OAUTH_FILE/.test(text3)) {
5305
5414
  details.push(`launcher exports HERMES_OAUTH_FILE, which Hermes does not implement (dead config): ${relative2(ctx.repoRoot, launcher)}`);
@@ -5337,9 +5446,16 @@ function createHermesChecks() {
5337
5446
  const launcher = join3(role.roleDir, "hermes");
5338
5447
  const text3 = safeReadText(launcher);
5339
5448
  if (text3 !== null) {
5340
- const rewritten = rewriteLauncher(text3);
5449
+ const before = /^HERMES_HOME=(.*)$/m.exec(text3)?.[1]?.trim();
5450
+ const rewritten = rewriteLauncher(text3, role.profileName || role.agentId);
5341
5451
  if (rewritten !== text3) {
5342
- details.push(`rewrite launcher HERMES_HOME -> profile path: ${relative2(ctx.repoRoot, launcher)}`);
5452
+ const rel = relative2(ctx.repoRoot, launcher);
5453
+ if (before !== void 0 && !isProfileHomeExpr(before)) {
5454
+ details.push(`rewrite launcher HERMES_HOME ${before} -> ${plan.profileDir}: ${rel}`);
5455
+ }
5456
+ if (/HERMES_OAUTH_FILE/.test(text3)) {
5457
+ details.push(`strip dead HERMES_OAUTH_FILE export: ${rel}`);
5458
+ }
5343
5459
  writeIfDifferent(launcher, rewritten, ctx.dryRun, changedFiles, 493);
5344
5460
  }
5345
5461
  }
@@ -5376,6 +5492,7 @@ function createHermesChecks() {
5376
5492
  audit: (ctx) => {
5377
5493
  const roles = discoverRoles(ctx.repoRoot);
5378
5494
  const details = [];
5495
+ let malformedRoleGate = false;
5379
5496
  const registryPath2 = join3(ctx.homeDir, ".hermes", "agents-registry.yaml");
5380
5497
  const registry = readRegistry(registryPath2);
5381
5498
  if (!registry) {
@@ -5412,6 +5529,11 @@ function createHermesChecks() {
5412
5529
  details.push(`.project.json declares agent "${extra}" that no role.yaml claims`);
5413
5530
  }
5414
5531
  for (const role of roles) {
5532
+ const expectedBloodbankEnabled = roleBloodbankEnabled(role);
5533
+ if (expectedBloodbankEnabled === null) {
5534
+ details.push(`${relative2(ctx.repoRoot, role.roleYamlPath)} bloodbank.enabled must be the strict YAML boolean true or false`);
5535
+ malformedRoleGate = true;
5536
+ }
5415
5537
  const entry = registry[role.agentId];
5416
5538
  if (!entry) {
5417
5539
  details.push(`registry is missing an entry for ${role.agentId}`);
@@ -5426,6 +5548,11 @@ function createHermesChecks() {
5426
5548
  details.push(`registry hermes.bin for ${role.agentId} does not exist: ${bin}`);
5427
5549
  }
5428
5550
  const bloodbank = entry.bloodbank ?? {};
5551
+ if (typeof bloodbank.enabled !== "boolean") {
5552
+ details.push(`registry entry for ${role.agentId} bloodbank.enabled must be a strict boolean`);
5553
+ } else if (expectedBloodbankEnabled !== null && bloodbank.enabled !== expectedBloodbankEnabled) {
5554
+ details.push(`registry entry for ${role.agentId} bloodbank.enabled must match explicit role value ${expectedBloodbankEnabled}`);
5555
+ }
5429
5556
  if (bloodbank.gateway_scope !== "fleet" || bloodbank.target_agent_id !== role.agentId) {
5430
5557
  details.push(`registry entry for ${role.agentId} must advertise bloodbank { gateway_scope: fleet, target_agent_id: ${role.agentId} }`);
5431
5558
  }
@@ -5446,7 +5573,7 @@ function createHermesChecks() {
5446
5573
  status: details.length === 0 ? "pass" : "fail",
5447
5574
  summary: details.length === 0 ? "Fleet registry is in parity" : `${details.length} registry parity issue(s) detected`,
5448
5575
  details,
5449
- fixable: true
5576
+ fixable: !malformedRoleGate
5450
5577
  };
5451
5578
  },
5452
5579
  migrate: (ctx, finding) => {
@@ -5458,6 +5585,19 @@ function createHermesChecks() {
5458
5585
  return { id: finding.id, title: finding.title, status: "blocked", summary: `registry unreadable at ${registryPath2}`, changedFiles, details };
5459
5586
  }
5460
5587
  const roles = discoverRoles(ctx.repoRoot);
5588
+ const malformedRoleGates = roles.filter((role) => roleBloodbankEnabled(role) === null);
5589
+ if (malformedRoleGates.length > 0) {
5590
+ return {
5591
+ id: finding.id,
5592
+ title: finding.title,
5593
+ status: "blocked",
5594
+ summary: "Registry parity is blocked by malformed role Bloodbank gates",
5595
+ changedFiles,
5596
+ details: malformedRoleGates.map(
5597
+ (role) => `${relative2(ctx.repoRoot, role.roleYamlPath)} bloodbank.enabled must be the strict YAML boolean true or false`
5598
+ )
5599
+ };
5600
+ }
5461
5601
  const missingRoles = roles.filter((role) => !raw.includes(`${role.agentId}:`));
5462
5602
  for (const role of missingRoles) {
5463
5603
  const updated = upsertRegistryEntry(role, ctx.homeDir, changedFiles, ctx.dryRun);
@@ -5521,9 +5661,10 @@ function createHermesChecks() {
5521
5661
  dirty = true;
5522
5662
  }
5523
5663
  const bloodbank = entry.bloodbank ?? {};
5524
- if (bloodbank.gateway_scope !== "fleet" || bloodbank.target_agent_id !== role.agentId) {
5525
- details.push(`advertise fleet bloodbank routing for ${role.agentId}`);
5526
- entry.bloodbank = { gateway_scope: "fleet", target_agent_id: role.agentId };
5664
+ const expectedBloodbankEnabled = roleBloodbankEnabled(role) ?? false;
5665
+ if (bloodbank.enabled !== expectedBloodbankEnabled || bloodbank.gateway_scope !== "fleet" || bloodbank.target_agent_id !== role.agentId) {
5666
+ details.push(`normalize fleet bloodbank routing for ${role.agentId} with enabled=${expectedBloodbankEnabled}`);
5667
+ entry.bloodbank = { ...bloodbank, enabled: expectedBloodbankEnabled, gateway_scope: "fleet", target_agent_id: role.agentId };
5527
5668
  dirty = true;
5528
5669
  }
5529
5670
  const systemd = entry.systemd;
@@ -5756,10 +5897,10 @@ var AgentHooksRecipe = class extends Recipe {
5756
5897
  printNextSteps() {
5757
5898
  console.log("\u{1FA9D} Agent-hooks layer installed!");
5758
5899
  console.log(" Next steps:");
5759
- console.log(" 1. mise run skills-sync # sync .agents/skills.json into local CLI dirs");
5760
- console.log(" 2. mise run hooks-sync # generate .claude/settings.json + inject codex/kimi/hermes");
5900
+ console.log(" 1. mise run skills:sync # sync .agents/skills.json into local CLI dirs");
5901
+ console.log(" 2. mise run hooks:sync # generate .claude/settings.json + inject codex/kimi/hermes");
5761
5902
  console.log(" 3. git add .claude/settings.json .agents/hooks .agents/skills.json && commit (codex/kimi/hermes are per-dev)");
5762
- console.log(" 4. mise run hindsight-setup # set HINDSIGHT_OP_KEY_REF to your 1Password item first");
5903
+ console.log(" 4. mise run hindsight:setup # set HINDSIGHT_OP_KEY_REF to your 1Password item first");
5763
5904
  console.log(" 5. Optional per-dev hook opt-out: copy .agents/local.example.json -> .agents/local.json");
5764
5905
  }
5765
5906
  };
@@ -5941,6 +6082,9 @@ var PromptForAgentConfig = class extends Command {
5941
6082
  ctx.soulTone ??= "direct";
5942
6083
  ctx.modelProvider ??= "";
5943
6084
  ctx.modelName ??= "";
6085
+ ctx.modelBaseUrl ??= "";
6086
+ ctx.modelApiMode ??= "";
6087
+ ctx.modelKeyEnv ??= "";
5944
6088
  ctx.ticketProvider ??= detectTicketProvider(ctx.targetDir) ?? "plane";
5945
6089
  ctx.skipEmail ??= true;
5946
6090
  ctx.agentId = deriveAgentId(ctx.targetRepo, ctx.role);
@@ -6007,7 +6151,17 @@ function resolveVendoredTemplate(name) {
6007
6151
  var RunCopierTemplate = class extends Command {
6008
6152
  async invoke() {
6009
6153
  const ctx = this.context;
6010
- const { targetRepo, role, agentPurpose, soulTone, modelProvider, modelName } = ctx;
6154
+ const {
6155
+ targetRepo,
6156
+ role,
6157
+ agentPurpose,
6158
+ soulTone,
6159
+ modelProvider,
6160
+ modelName,
6161
+ modelBaseUrl,
6162
+ modelApiMode,
6163
+ modelKeyEnv
6164
+ } = ctx;
6011
6165
  const ticketProvider = ctx.ticketProvider ?? "plane";
6012
6166
  const profileName = ctx.profileName ?? (targetRepo && role ? deriveProfileName(targetRepo, role) : void 0);
6013
6167
  if (!targetRepo || !role) {
@@ -6074,6 +6228,12 @@ var RunCopierTemplate = class extends Command {
6074
6228
  "--data",
6075
6229
  `model_name=${modelName ?? ""}`,
6076
6230
  "--data",
6231
+ `model_base_url=${modelBaseUrl ?? ""}`,
6232
+ "--data",
6233
+ `model_api_mode=${modelApiMode ?? ""}`,
6234
+ "--data",
6235
+ `model_key_env=${modelKeyEnv ?? ""}`,
6236
+ "--data",
6077
6237
  `profile_name=${profileName ?? ""}`,
6078
6238
  "--data",
6079
6239
  `soul_tone=${soulTone ?? "direct"}`,
@@ -8597,31 +8757,33 @@ ${leaveBlock}`);
8597
8757
  const appended = [
8598
8758
  "",
8599
8759
  _WireMiseAgentHooks.MARKER + " (generated \u2014 see .agents/hooks/README.md)",
8760
+ // PJAN-61: task names use the colon namespace form. A colon is not legal
8761
+ // in a BARE toml key, so every header here MUST stay quoted.
8600
8762
  "[[watch_files]]",
8601
8763
  'patterns = [".agents/skills.json"]',
8602
- 'task = "skills-sync"',
8764
+ 'task = "skills:sync"',
8603
8765
  "",
8604
- "[tasks.skills-sync]",
8766
+ '[tasks."skills:sync"]',
8605
8767
  'description = "Sync skills from manifest to local CLI dirs"',
8606
8768
  'run = "sync-skills.py --scope project"',
8607
8769
  "",
8608
8770
  "[[watch_files]]",
8609
8771
  'patterns = [".agents/hooks/hooks.master.json"]',
8610
- 'task = "hooks-sync"',
8772
+ 'task = "hooks:sync"',
8611
8773
  "",
8612
- "[tasks.hooks-sync]",
8774
+ '[tasks."hooks:sync"]',
8613
8775
  'description = "Fan out hooks.master.json to each agent CLI (claude/codex/kimi/hermes)"',
8614
8776
  `run = "${cr}/.agents/hooks/sync.py --install"`,
8615
8777
  "",
8616
- "[tasks.hooks-check]",
8778
+ '[tasks."hooks:check"]',
8617
8779
  'description = "Drift gate: verify generated hook configs match hooks.master.json"',
8618
8780
  `run = "${cr}/.agents/hooks/sync.py --check"`,
8619
8781
  "",
8620
- "[tasks.hooks-uninstall]",
8782
+ '[tasks."hooks:uninstall"]',
8621
8783
  'description = "Remove per-user agent-hook injections (codex/kimi/hermes)"',
8622
8784
  `run = "${cr}/.agents/hooks/sync.py --uninstall"`,
8623
8785
  "",
8624
- "[tasks.hindsight-setup]",
8786
+ '[tasks."hindsight:setup"]',
8625
8787
  `description = "Provision this dev's shared project Hindsight key from 1Password into .env"`,
8626
8788
  `run = "${cr}/.mise/scripts/hindsight-setup.sh"`,
8627
8789
  "",
@@ -9581,7 +9743,7 @@ program.command("migrate").argument("[rule-id]", "Rule ID to migrate (omit to op
9581
9743
  process.exit(1);
9582
9744
  }
9583
9745
  });
9584
- program.command("hermes-agent").alias("hermes").description("Provision the PM agent for the current repo (defaults everything; only asks about Telegram)").option("-y, --yes", "Non-interactive: accept all defaults (also skips the Telegram prompt)").option("--target-repo <name>", "Target repo name (default: basename of cwd)").option("--role <role>", "Agent role override (default: pm \u2014 the only role in the fleet)").option("--purpose <text>", 'One-line agent purpose (default: "pm agent for <repo>")').option(`--tone <tone>`, `Personality tone (default: direct; ${SOUL_TONES.join(" | ")})`).option("--model-provider <name>", 'Inference provider override ("" = inherit shared default profile)').option("--model-name <name>", 'Model name override ("" = inherit shared default profile)').option("--skip-telegram", "Skip the Telegram wire-up (no BotFather prompt)").option("--email", "Also provision the delo.sh email address (off by default; never prompted)").option("--skip-runtime-repo", "Skip creating the per-agent runtime GH repo").option("--skip-plane", "Skip creating or linking the ticket board").option("--skip-bloodbank", "Deprecated compatibility flag; Bloodbank now uses one fleet-shared Hermes gateway").option("--skip-systemd", "Skip installing systemd --user units").option("--local", "Local-only: skip runtime repo, ticket-board creation, Bloodbank, and systemd (safe for laptops/macOS/non-technical operators)").option("--force-config", "Regenerate ~/.config/hermes-agent-template/config.toml even if it exists").option("--dry-run", "Preview what would run; don't execute copier").option("-f, --force", "Re-render even if agents/hermes/<role>/role.yaml already exists").action(async (options) => {
9746
+ program.command("hermes-agent").alias("hermes").description("Provision the PM agent for the current repo (defaults everything; only asks about Telegram)").option("-y, --yes", "Non-interactive: accept all defaults (also skips the Telegram prompt)").option("--target-repo <name>", "Target repo name (default: basename of cwd)").option("--role <role>", "Agent role override (default: pm \u2014 the only role in the fleet)").option("--purpose <text>", 'One-line agent purpose (default: "pm agent for <repo>")').option(`--tone <tone>`, `Personality tone (default: direct; ${SOUL_TONES.join(" | ")})`).option("--model-provider <name>", 'Inference provider override ("" = inherit shared default profile)').option("--model-name <name>", 'Model name override ("" = inherit shared default profile)').option("--model-base-url <url>", 'Inference API base URL override ("" = inherit shared default profile)').option("--model-api-mode <mode>", 'Inference API mode override ("" = inherit shared default profile)').option("--model-key-env <name>", "Environment variable name holding the scoped model credential").option("--skip-telegram", "Skip the Telegram wire-up (no BotFather prompt)").option("--email", "Also provision the delo.sh email address (off by default; never prompted)").option("--skip-runtime-repo", "Skip creating the per-agent runtime GH repo").option("--skip-plane", "Skip creating or linking the ticket board").option("--skip-bloodbank", "Deprecated compatibility flag; Bloodbank now uses one fleet-shared Hermes gateway").option("--skip-systemd", "Skip installing systemd --user units").option("--local", "Local-only: skip runtime repo, ticket-board creation, Bloodbank, and systemd (safe for laptops/macOS/non-technical operators)").option("--force-config", "Regenerate ~/.config/hermes-agent-template/config.toml even if it exists").option("--dry-run", "Preview what would run; don't execute copier").option("-f, --force", "Re-render even if agents/hermes/<role>/role.yaml already exists").action(async (options) => {
9585
9747
  const isDarwin = process.platform === "darwin";
9586
9748
  const local = options.local ?? false;
9587
9749
  const context = {
@@ -9597,6 +9759,9 @@ program.command("hermes-agent").alias("hermes").description("Provision the PM ag
9597
9759
  soulTone: options.tone,
9598
9760
  modelProvider: options.modelProvider,
9599
9761
  modelName: options.modelName,
9762
+ modelBaseUrl: options.modelBaseUrl,
9763
+ modelApiMode: options.modelApiMode,
9764
+ modelKeyEnv: options.modelKeyEnv,
9600
9765
  skipTelegram: options.skipTelegram,
9601
9766
  // Email is opt-in only: `--email` wires it, otherwise it's never done.
9602
9767
  skipEmail: options.email ? false : void 0,