@delorenj/pjangler 1.2.3 → 1.2.4

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/dist/index.js CHANGED
@@ -9,18 +9,6 @@ import { Command as Command3 } from "commander";
9
9
  // src/commands/hermes/types.ts
10
10
  var HERMES_AGENT_TEMPLATE = "gh:delorenj/hermes-agent-template";
11
11
  var SOUL_TONES = ["direct", "playful", "formal", "terse"];
12
- var ROLE_CHOICES = [
13
- { value: "pm", label: "Project Manager (pm)", hint: "triage, planning, ticket authorship, board reconciliation" },
14
- { value: "dev", label: "Developer (dev)", hint: "implements tickets" },
15
- { value: "review", label: "Reviewer (review)", hint: "adversarial code review" },
16
- { value: "ops", label: "Ops (ops)", hint: "deploy / infra" },
17
- { value: "qa", label: "QA (qa)", hint: "test authorship + verification" }
18
- ];
19
- var TICKET_PROVIDERS = [
20
- { value: "plane", label: "Plane", hint: "self-hosted at plane.delo.sh (default)" },
21
- { value: "linear", label: "Linear", hint: "team board (created in Linear UI)" },
22
- { value: "trello", label: "Trello", hint: "board = project" }
23
- ];
24
12
  function deriveAgentId(repo, role) {
25
13
  return `${repo}-${role}`.toLowerCase();
26
14
  }
@@ -748,19 +736,18 @@ var PromptForAgentConfig = class extends Command {
748
736
  async invoke() {
749
737
  const ctx = this.context;
750
738
  const defaultRepo = basename(ctx.targetDir).toLowerCase();
751
- const defaultRole = "pm";
739
+ ctx.targetRepo = (ctx.targetRepo ?? defaultRepo).toLowerCase();
740
+ ctx.role ??= "pm";
741
+ ctx.agentPurpose ??= `${ctx.role} agent for ${ctx.targetRepo}`;
742
+ ctx.soulTone ??= "direct";
743
+ ctx.modelProvider ??= "";
744
+ ctx.modelName ??= "";
745
+ ctx.ticketProvider ??= detectTicketProvider(ctx.targetDir) ?? "plane";
746
+ ctx.skipEmail ??= true;
747
+ ctx.agentId = deriveAgentId(ctx.targetRepo, ctx.role);
748
+ ctx.profileName = deriveProfileName(ctx.targetRepo, ctx.role);
752
749
  if (ctx.yes) {
753
- ctx.targetRepo = (ctx.targetRepo ?? defaultRepo).toLowerCase();
754
- ctx.role ??= defaultRole;
755
- ctx.agentPurpose ??= `${ctx.role} agent for ${ctx.targetRepo}`;
756
- ctx.soulTone ??= "direct";
757
- ctx.modelProvider ??= "";
758
- ctx.modelName ??= "";
759
- ctx.ticketProvider ??= detectTicketProvider(ctx.targetDir) ?? "plane";
760
750
  ctx.skipTelegram ??= true;
761
- ctx.skipEmail ??= true;
762
- ctx.agentId = deriveAgentId(ctx.targetRepo, ctx.role);
763
- ctx.profileName = deriveProfileName(ctx.targetRepo, ctx.role);
764
751
  return {
765
752
  success: true,
766
753
  message: this.formatMessage(
@@ -768,96 +755,19 @@ var PromptForAgentConfig = class extends Command {
768
755
  )
769
756
  };
770
757
  }
771
- p.intro("\u2695 hermes-agent \xB7 add a new agent role to this repo");
772
- if (!ctx.targetRepo) {
773
- const answer = await p.text({
774
- message: "Target repo name",
775
- placeholder: defaultRepo,
776
- initialValue: defaultRepo,
777
- validate: (v) => v && v.trim() ? void 0 : "required"
778
- });
779
- if (p.isCancel(answer)) return this.cancelled();
780
- ctx.targetRepo = String(answer).trim().toLowerCase();
781
- }
782
- if (!ctx.role) {
783
- const answer = await p.select({
784
- message: "Role",
785
- options: ROLE_CHOICES.map((r) => ({ value: r.value, label: r.label, hint: r.hint })),
786
- initialValue: defaultRole
787
- });
788
- if (p.isCancel(answer)) return this.cancelled();
789
- ctx.role = String(answer).trim();
790
- }
791
- if (ctx.ticketProvider === void 0) {
792
- const detected = detectTicketProvider(ctx.targetDir);
793
- const answer = await p.select({
794
- message: "Ticket board provider",
795
- options: TICKET_PROVIDERS.map((t) => ({
796
- value: t.value,
797
- label: t.label,
798
- hint: t.value === detected ? `${t.hint} \u2014 current .project.json` : t.hint
799
- })),
800
- initialValue: detected ?? "plane"
801
- });
802
- if (p.isCancel(answer)) return this.cancelled();
803
- ctx.ticketProvider = answer;
804
- }
805
- if (!ctx.agentPurpose) {
806
- const answer = await p.text({
807
- message: "One-line purpose",
808
- placeholder: `${ctx.role} agent for ${ctx.targetRepo}`,
809
- initialValue: `${ctx.role} agent for ${ctx.targetRepo}`
810
- });
811
- if (p.isCancel(answer)) return this.cancelled();
812
- ctx.agentPurpose = String(answer).trim();
813
- }
814
- if (!ctx.soulTone) {
815
- const answer = await p.select({
816
- message: "Personality tone",
817
- options: SOUL_TONES.map((t) => ({
818
- value: t,
819
- label: t,
820
- hint: t === "direct" ? "decision-forward, no preamble (default)" : t === "terse" ? "minimum words, conclusion-first" : t === "playful" ? "warm, mildly funny" : "precise, structured"
821
- })),
822
- initialValue: "direct"
823
- });
824
- if (p.isCancel(answer)) return this.cancelled();
825
- ctx.soulTone = answer;
826
- }
827
- if (ctx.modelProvider === void 0) {
828
- const answer = await p.text({
829
- message: "Provider override (empty = inherit shared default profile)",
830
- placeholder: ""
831
- });
832
- if (p.isCancel(answer)) return this.cancelled();
833
- ctx.modelProvider = String(answer).trim();
834
- }
835
- if (ctx.modelName === void 0) {
836
- const answer = await p.text({
837
- message: "Model name override (empty = inherit shared default profile)",
838
- placeholder: ""
839
- });
840
- if (p.isCancel(answer)) return this.cancelled();
841
- ctx.modelName = String(answer).trim();
842
- }
758
+ p.intro("\u2695 hermes-agent \xB7 provision the PM agent for this repo");
759
+ p.log.info(
760
+ `agent ${ctx.agentId} \xB7 board ${ctx.ticketProvider} \xB7 tone ${ctx.soulTone}`
761
+ );
843
762
  if (ctx.skipTelegram === void 0) {
763
+ const botHandle = `${ctx.targetRepo.replace(/-/g, "_")}_${ctx.role}_bot`;
844
764
  const wire = await p.confirm({
845
- message: `Wire up the Telegram bot (@${ctx.targetRepo}_${ctx.role}_bot) now?`,
765
+ message: `Wire up the Telegram bot (@${botHandle}) now?`,
846
766
  initialValue: true
847
767
  });
848
768
  if (p.isCancel(wire)) return this.cancelled();
849
769
  ctx.skipTelegram = !wire;
850
770
  }
851
- if (ctx.skipEmail === void 0) {
852
- const wire = await p.confirm({
853
- message: `Provision the delo.sh email address (${ctx.targetRepo}-${ctx.role}@delo.sh) now?`,
854
- initialValue: true
855
- });
856
- if (p.isCancel(wire)) return this.cancelled();
857
- ctx.skipEmail = !wire;
858
- }
859
- ctx.agentId = deriveAgentId(ctx.targetRepo, ctx.role);
860
- ctx.profileName = deriveProfileName(ctx.targetRepo, ctx.role);
861
771
  return {
862
772
  success: true,
863
773
  message: this.formatMessage(
@@ -1132,7 +1042,7 @@ var WireEmail = class extends Command {
1132
1042
  async invoke() {
1133
1043
  const ctx = this.context;
1134
1044
  if (ctx.skipEmail) {
1135
- return { success: true, message: "\u2192 Email wire-up skipped" };
1045
+ return { success: true, message: "" };
1136
1046
  }
1137
1047
  if (ctx.dryRun) {
1138
1048
  return { success: true, message: this.formatMessage("Would create CF Email Routing rule") };
@@ -1240,7 +1150,7 @@ var PrintHermesSummary = class extends Command {
1240
1150
  lines.push(`role dir ${ctx.roleDir}`);
1241
1151
  lines.push(`runtime gh:${runtimeRepo}`);
1242
1152
  lines.push(`telegram @${botHandle}${skipTelegram ? " (NOT yet wired)" : ""}`);
1243
- lines.push(`email ${email}${skipEmail ? " (NOT yet wired)" : ""}`);
1153
+ if (!skipEmail) lines.push(`email ${email}`);
1244
1154
  lines.push("");
1245
1155
  lines.push("Start daemons:");
1246
1156
  lines.push(` systemctl --user start ${csm}`);
@@ -1253,11 +1163,10 @@ var PrintHermesSummary = class extends Command {
1253
1163
  lines.push("");
1254
1164
  lines.push("Talk locally:");
1255
1165
  lines.push(` ${ctx.roleDir}/hermes chat "status"`);
1256
- if (skipTelegram || skipEmail) {
1166
+ if (skipTelegram) {
1257
1167
  lines.push("");
1258
- lines.push("Deferred \u2014 re-run pjangler hermes-agent without --yes (or with explicit flags):");
1259
- if (skipTelegram) lines.push(" pjangler hermes-agent --skip-telegram=false # wire just telegram");
1260
- if (skipEmail) lines.push(" pjangler hermes-agent --skip-email=false # wire just email");
1168
+ lines.push("Wire Telegram later:");
1169
+ lines.push(" pjangler hermes-agent # re-run and answer yes when asked");
1261
1170
  }
1262
1171
  p5.note(lines.join("\n"), `Provisioned ${agentId}`);
1263
1172
  p5.outro("Done.");
@@ -1616,7 +1525,7 @@ function createRecipe(name, context) {
1616
1525
  }
1617
1526
 
1618
1527
  // src/index.ts
1619
- import { cancel as cancel2, multiselect, text as text3, isCancel as isCancel5 } from "@clack/prompts";
1528
+ import { cancel as cancel2, multiselect, text as text2, isCancel as isCancel5 } from "@clack/prompts";
1620
1529
 
1621
1530
  // src/parity/index.ts
1622
1531
  import { existsSync as existsSync7, lstatSync, mkdirSync as mkdirSync5, readFileSync as readFileSync3, readlinkSync, readdirSync, renameSync, symlinkSync, unlinkSync as unlinkSync3, writeFileSync as writeFileSync4, chmodSync as chmodSync2, copyFileSync } from "node:fs";
@@ -1717,10 +1626,10 @@ function writeText(path, content) {
1717
1626
  ensureParent(path);
1718
1627
  writeFileSync4(path, content);
1719
1628
  }
1720
- function tryParseJson(text4) {
1721
- if (!text4) return null;
1629
+ function tryParseJson(text3) {
1630
+ if (!text3) return null;
1722
1631
  try {
1723
- return JSON.parse(text4);
1632
+ return JSON.parse(text3);
1724
1633
  } catch {
1725
1634
  return null;
1726
1635
  }
@@ -1779,9 +1688,9 @@ function bootstrapAgentsFile(repoRoot, dryRun) {
1779
1688
  }
1780
1689
  return { changedFiles: [], details: [], blocked: "AGENTS.md missing and no CLAUDE.md, GEMINI.md, or README.md source exists" };
1781
1690
  }
1782
- function yamlGet(text4, keyPath) {
1691
+ function yamlGet(text3, keyPath) {
1783
1692
  const parts = keyPath.split(".");
1784
- const lines = text4.split("\n");
1693
+ const lines = text3.split("\n");
1785
1694
  let start = 0;
1786
1695
  let indent = 0;
1787
1696
  for (let idx = 0; idx < parts.length; idx += 1) {
@@ -1816,25 +1725,25 @@ function discoverRoles(repoRoot) {
1816
1725
  const roleDir = join9(rolesDir, entry.name);
1817
1726
  const roleYamlPath = join9(roleDir, "role.yaml");
1818
1727
  if (!existsSync7(roleYamlPath)) return null;
1819
- const text4 = readText(roleYamlPath);
1820
- const runtimeRepoRaw = yamlGet(text4, "runtime.github_repo");
1728
+ const text3 = readText(roleYamlPath);
1729
+ const runtimeRepoRaw = yamlGet(text3, "runtime.github_repo");
1821
1730
  return {
1822
- role: yamlGet(text4, "role") || entry.name,
1731
+ role: yamlGet(text3, "role") || entry.name,
1823
1732
  roleDir,
1824
1733
  roleYamlPath,
1825
- repo: yamlGet(text4, "repo"),
1826
- agentId: yamlGet(text4, "agent_id"),
1827
- profileName: yamlGet(text4, "profile") || yamlGet(text4, "agent_id"),
1828
- displayName: yamlGet(text4, "display_name"),
1829
- purpose: yamlGet(text4, "purpose"),
1830
- botHandle: yamlGet(text4, "telegram.bot_username"),
1734
+ repo: yamlGet(text3, "repo"),
1735
+ agentId: yamlGet(text3, "agent_id"),
1736
+ profileName: yamlGet(text3, "profile") || yamlGet(text3, "agent_id"),
1737
+ displayName: yamlGet(text3, "display_name"),
1738
+ purpose: yamlGet(text3, "purpose"),
1739
+ botHandle: yamlGet(text3, "telegram.bot_username"),
1831
1740
  runtimeRepo: runtimeRepoRaw.includes("/") ? runtimeRepoRaw.split("/").slice(-1)[0] ?? runtimeRepoRaw : runtimeRepoRaw,
1832
- runtimeOwner: yamlGet(text4, "runtime.github_owner"),
1833
- planeWorkspace: yamlGet(text4, "ticket_provider.workspace") || yamlGet(text4, "plane.workspace"),
1834
- ticketProviderName: yamlGet(text4, "ticket_provider.name"),
1835
- ticketProviderBoardId: yamlGet(text4, "ticket_provider.board_id"),
1836
- ticketProviderBoardUrl: yamlGet(text4, "ticket_provider.board_url"),
1837
- ticketProviderIdentifier: yamlGet(text4, "plane.identifier")
1741
+ runtimeOwner: yamlGet(text3, "runtime.github_owner"),
1742
+ planeWorkspace: yamlGet(text3, "ticket_provider.workspace") || yamlGet(text3, "plane.workspace"),
1743
+ ticketProviderName: yamlGet(text3, "ticket_provider.name"),
1744
+ ticketProviderBoardId: yamlGet(text3, "ticket_provider.board_id"),
1745
+ ticketProviderBoardUrl: yamlGet(text3, "ticket_provider.board_url"),
1746
+ ticketProviderIdentifier: yamlGet(text3, "plane.identifier")
1838
1747
  };
1839
1748
  }).filter((value) => Boolean(value));
1840
1749
  }
@@ -1879,19 +1788,19 @@ function templateVersionFilesConf(ctx, repoRoot) {
1879
1788
  const packageJson = join9(repoRoot, "package.json");
1880
1789
  return existsSync7(packageJson) ? "# mise-versioning manifest: <type> <path>\n# types: json toml cargo csproj gradle plain gittag\njson package.json\ngittag .\n" : "# mise-versioning manifest: <type> <path>\n# types: json toml cargo csproj gradle plain gittag\ngittag .\n";
1881
1790
  }
1882
- function replaceOrAppendManagedBlock(text4, startMarker, block, beforePattern) {
1883
- if (startMarker.test(text4)) {
1884
- return text4.replace(/# >>> mise-versioning >>>[\s\S]*?# <<< mise-versioning <<</, block);
1791
+ function replaceOrAppendManagedBlock(text3, startMarker, block, beforePattern) {
1792
+ if (startMarker.test(text3)) {
1793
+ return text3.replace(/# >>> mise-versioning >>>[\s\S]*?# <<< mise-versioning <<</, block);
1885
1794
  }
1886
1795
  if (beforePattern) {
1887
- const match = text4.match(beforePattern);
1796
+ const match = text3.match(beforePattern);
1888
1797
  if (match && typeof match.index === "number") {
1889
- return `${text4.slice(0, match.index).replace(/\s*$/, "\n\n")}${block}
1798
+ return `${text3.slice(0, match.index).replace(/\s*$/, "\n\n")}${block}
1890
1799
 
1891
- ${text4.slice(match.index)}`;
1800
+ ${text3.slice(match.index)}`;
1892
1801
  }
1893
1802
  }
1894
- return `${text4.replace(/\s*$/, "")}
1803
+ return `${text3.replace(/\s*$/, "")}
1895
1804
 
1896
1805
  ${block}
1897
1806
  `;
@@ -1905,18 +1814,18 @@ function requiredMisePathEntries(ctx) {
1905
1814
  }
1906
1815
  return required;
1907
1816
  }
1908
- function upsertMisePath(text4, required = BASE_MISE_PATH_ENTRIES) {
1817
+ function upsertMisePath(text3, required = BASE_MISE_PATH_ENTRIES) {
1909
1818
  const render = (values) => `_.path = [${values.map((value) => JSON.stringify(value)).join(", ")}]`;
1910
- const envMatch = text4.match(/(^|\n)(\[env\][\s\S]*?)(?=\n\[[^\]]+\]|$)/);
1819
+ const envMatch = text3.match(/(^|\n)(\[env\][\s\S]*?)(?=\n\[[^\]]+\]|$)/);
1911
1820
  if (!envMatch || typeof envMatch.index !== "number") {
1912
1821
  return `[env]
1913
1822
  ${render(required)}
1914
1823
 
1915
- ${text4.replace(/^\s+/, "")}`;
1824
+ ${text3.replace(/^\s+/, "")}`;
1916
1825
  }
1917
- const prefix = text4.slice(0, envMatch.index + envMatch[1].length);
1826
+ const prefix = text3.slice(0, envMatch.index + envMatch[1].length);
1918
1827
  const section = envMatch[2];
1919
- const suffix = text4.slice(envMatch.index + envMatch[1].length + section.length);
1828
+ const suffix = text3.slice(envMatch.index + envMatch[1].length + section.length);
1920
1829
  const pathLine = section.match(/^_\.path\s*=\s*\[([^\]]*)\]\s*$/m);
1921
1830
  if (!pathLine) {
1922
1831
  return `${prefix}${section.replace(/\n?$/, "\n")}${render(required)}${suffix}`;
@@ -1927,11 +1836,11 @@ ${text4.replace(/^\s+/, "")}`;
1927
1836
  if (!merged.includes(value)) merged.push(value);
1928
1837
  }
1929
1838
  const nextLine = render(merged);
1930
- if (pathLine[0] === nextLine) return text4;
1839
+ if (pathLine[0] === nextLine) return text3;
1931
1840
  return `${prefix}${section.replace(pathLine[0], nextLine)}${suffix}`;
1932
1841
  }
1933
- function removeTomlSection(text4, headerPattern, marker, options) {
1934
- const lines = text4.split("\n");
1842
+ function removeTomlSection(text3, headerPattern, marker, options) {
1843
+ const lines = text3.split("\n");
1935
1844
  let start = -1;
1936
1845
  let end = -1;
1937
1846
  for (let i = 0; i < lines.length; i++) {
@@ -1956,7 +1865,7 @@ function removeTomlSection(text4, headerPattern, marker, options) {
1956
1865
  if (end === -1) end = lines.length;
1957
1866
  break;
1958
1867
  }
1959
- if (start === -1) return text4;
1868
+ if (start === -1) return text3;
1960
1869
  if (options?.includePrecedingComments) {
1961
1870
  while (start > 0 && lines[start - 1].trim().startsWith("#")) {
1962
1871
  start--;
@@ -1965,22 +1874,22 @@ function removeTomlSection(text4, headerPattern, marker, options) {
1965
1874
  const result = lines.slice(0, start).concat(lines.slice(end)).join("\n");
1966
1875
  return result.replace(/\n{3,}/g, "\n\n").replace(/\n+$/, "\n");
1967
1876
  }
1968
- function insertTomlBlockBeforeVersioning(text4, block) {
1969
- const versioningIndex = text4.indexOf("# >>> mise-versioning >>>");
1877
+ function insertTomlBlockBeforeVersioning(text3, block) {
1878
+ const versioningIndex = text3.indexOf("# >>> mise-versioning >>>");
1970
1879
  if (versioningIndex >= 0) {
1971
- return `${text4.slice(0, versioningIndex).replace(/\s*$/, "\n\n")}${block}
1880
+ return `${text3.slice(0, versioningIndex).replace(/\s*$/, "\n\n")}${block}
1972
1881
 
1973
- ${text4.slice(versioningIndex)}`;
1882
+ ${text3.slice(versioningIndex)}`;
1974
1883
  }
1975
- return `${text4.replace(/\s*$/, "")}
1884
+ return `${text3.replace(/\s*$/, "")}
1976
1885
 
1977
1886
  ${block}
1978
1887
  `;
1979
1888
  }
1980
- function extractTomlStrings(text4) {
1889
+ function extractTomlStrings(text3) {
1981
1890
  const values = [];
1982
1891
  const stringPattern = /"((?:\\.|[^"\\])*)"|'([^']*)'/g;
1983
- for (const match of text4.matchAll(stringPattern)) {
1892
+ for (const match of text3.matchAll(stringPattern)) {
1984
1893
  if (match[1] !== void 0) {
1985
1894
  try {
1986
1895
  values.push(JSON.parse(`"${match[1]}"`));
@@ -2004,10 +1913,10 @@ function renderHookEntries(entries, indent = "") {
2004
1913
  `${indent}]`
2005
1914
  ];
2006
1915
  }
2007
- function upsertLinkAgentfilesHooks(text4) {
2008
- const lines = text4.split("\n");
1916
+ function upsertLinkAgentfilesHooks(text3) {
1917
+ const lines = text3.split("\n");
2009
1918
  const hooksStart = lines.findIndex((line) => /^\[hooks\]$/.test(line.trim()));
2010
- if (hooksStart === -1) return insertTomlBlockBeforeVersioning(text4, LINK_AGENTFILES_HOOKS_BLOCK);
1919
+ if (hooksStart === -1) return insertTomlBlockBeforeVersioning(text3, LINK_AGENTFILES_HOOKS_BLOCK);
2011
1920
  let hooksEnd = lines.length;
2012
1921
  for (let i = hooksStart + 1; i < lines.length; i++) {
2013
1922
  if (/^\[[^\]]+\]/.test(lines[i].trim())) {
@@ -2041,8 +1950,8 @@ function upsertLinkAgentfilesHooks(text4) {
2041
1950
  }
2042
1951
  return lines.slice(0, hooksStart + 1).concat(rendered, lines.slice(hooksStart + 1)).join("\n").replace(/\n{3,}/g, "\n\n").replace(/\n+$/, "\n");
2043
1952
  }
2044
- function upsertLinkAgentfilesBlock(text4, ctx) {
2045
- const withPath = upsertMisePath(text4, requiredMisePathEntries(ctx));
1953
+ function upsertLinkAgentfilesBlock(text3, ctx) {
1954
+ const withPath = upsertMisePath(text3, requiredMisePathEntries(ctx));
2046
1955
  if (withPath.includes(LINK_AGENTFILES_BLOCK)) return withPath;
2047
1956
  let cleaned = removeTomlSection(withPath, /^\[tasks\.link-agentfiles\]$/, /link-agentfiles/, { includePrecedingComments: false });
2048
1957
  cleaned = removeTomlSection(cleaned, /^\[\[watch_files\]\]$/, /AGENTS\.md/, { includePrecedingComments: false });
@@ -2269,9 +2178,9 @@ ${block}`) : `${current.replace(/\s*$/, "\n")}${block}`;
2269
2178
  return path;
2270
2179
  }
2271
2180
  function profileMetaInheritsDefault(path) {
2272
- const text4 = safeReadText(path);
2181
+ const text3 = safeReadText(path);
2273
2182
  return Boolean(
2274
- text4 && /^config:\s*$/m.test(text4) && /^\s+inherit_from:\s*default\s*$/m.test(text4) && /^\s+save_mode:\s*delta\s*$/m.test(text4)
2183
+ text3 && /^config:\s*$/m.test(text3) && /^\s+inherit_from:\s*default\s*$/m.test(text3) && /^\s+save_mode:\s*delta\s*$/m.test(text3)
2275
2184
  );
2276
2185
  }
2277
2186
  function upsertInheritedProfileMeta(path, changedFiles, dryRun) {
@@ -2329,17 +2238,17 @@ var RULES = [
2329
2238
  if (!existsSync7(misePath)) {
2330
2239
  return { id: "mise.config-root", title: "mise config_root + AGENTS link hooks", status: "fail", summary: "mise.toml missing", details: [], fixable: true };
2331
2240
  }
2332
- const text4 = readText(misePath);
2241
+ const text3 = readText(misePath);
2333
2242
  const details = [];
2334
2243
  const linkAgentfilesPath = join9(ctx.repoRoot, ".mise", "scripts", "link-agentfiles.sh");
2335
2244
  if (!existsSync7(linkAgentfilesPath)) details.push(".mise/scripts/link-agentfiles.sh missing");
2336
- const pathValues = [...(text4.match(/^_\.path\s*=\s*\[([^\]]*)\]/m)?.[1] ?? "").matchAll(/"([^"]+)"/g)].map((match) => match[1]);
2245
+ const pathValues = [...(text3.match(/^_\.path\s*=\s*\[([^\]]*)\]/m)?.[1] ?? "").matchAll(/"([^"]+)"/g)].map((match) => match[1]);
2337
2246
  const missingPathValues = requiredMisePathEntries(ctx).filter((value) => !pathValues.includes(value));
2338
2247
  if (missingPathValues.length) details.push(`[env]._.path should include ${missingPathValues.join(", ")}`);
2339
- if (!text4.includes('"{{config_root}}/.mise/scripts/link-agentfiles.sh"')) details.push("link-agentfiles must use raw {{config_root}} guard");
2340
- if (!text4.includes("op inject -i .env.op > .env")) details.push("[hooks].enter must materialize .env from .env.op");
2341
- if (!text4.includes('patterns = ["AGENTS.md"]')) details.push("watch_files must monitor AGENTS.md");
2342
- if (!text4.includes('task = "link-agentfiles"')) details.push("watch_files must dispatch link-agentfiles task");
2248
+ if (!text3.includes('"{{config_root}}/.mise/scripts/link-agentfiles.sh"')) details.push("link-agentfiles must use raw {{config_root}} guard");
2249
+ if (!text3.includes("op inject -i .env.op > .env")) details.push("[hooks].enter must materialize .env from .env.op");
2250
+ if (!text3.includes('patterns = ["AGENTS.md"]')) details.push("watch_files must monitor AGENTS.md");
2251
+ if (!text3.includes('task = "link-agentfiles"')) details.push("watch_files must dispatch link-agentfiles task");
2343
2252
  return {
2344
2253
  id: "mise.config-root",
2345
2254
  title: "mise config_root + AGENTS link hooks",
@@ -2362,12 +2271,12 @@ var RULES = [
2362
2271
  return { id: finding.id, title: finding.title, status: "applied", summary: "Would initialize mise.toml from generated-project template", changedFiles, details };
2363
2272
  }
2364
2273
  }
2365
- let text4 = readText(path);
2366
- const next = upsertLinkAgentfilesBlock(text4, ctx);
2367
- if (next !== text4) {
2274
+ let text3 = readText(path);
2275
+ const next = upsertLinkAgentfilesBlock(text3, ctx);
2276
+ if (next !== text3) {
2368
2277
  if (!changedFiles.includes(path)) changedFiles.push(path);
2369
2278
  if (!ctx.dryRun) writeText(path, next);
2370
- text4 = next;
2279
+ text3 = next;
2371
2280
  }
2372
2281
  const linkAgentfilesPath = join9(ctx.repoRoot, ".mise", "scripts", "link-agentfiles.sh");
2373
2282
  const expectedScript = templateLinkAgentfilesScript(ctx);
@@ -2399,8 +2308,8 @@ var RULES = [
2399
2308
  const misePath = join9(ctx.repoRoot, "mise.toml");
2400
2309
  const versioningPath = join9(ctx.repoRoot, ".mise", "scripts", "versioning.sh");
2401
2310
  const manifestPath = join9(ctx.repoRoot, ".mise", "version-files.conf");
2402
- const text4 = safeReadText(misePath);
2403
- if (!text4?.includes("# >>> mise-versioning >>>")) details.push("mise versioning managed block missing");
2311
+ const text3 = safeReadText(misePath);
2312
+ if (!text3?.includes("# >>> mise-versioning >>>")) details.push("mise versioning managed block missing");
2404
2313
  if (!existsSync7(versioningPath)) details.push(".mise/scripts/versioning.sh missing");
2405
2314
  if (!existsSync7(manifestPath)) details.push(".mise/version-files.conf missing");
2406
2315
  return {
@@ -2621,19 +2530,19 @@ var RULES = [
2621
2530
  audit: (ctx) => {
2622
2531
  const details = [];
2623
2532
  const path = join9(ctx.repoRoot, ".copier-answers.yml");
2624
- const text4 = safeReadText(path);
2533
+ const text3 = safeReadText(path);
2625
2534
  const project = readProjectJson(ctx);
2626
- if (!text4) {
2535
+ if (!text3) {
2627
2536
  details.push(".copier-answers.yml missing");
2628
2537
  } else {
2629
- if (!text4.startsWith("# Changes here will be overwritten by Copier; NEVER EDIT MANUALLY")) details.push("missing Copier overwrite warning header");
2630
- if (!text4.includes("_src_path:")) details.push("_src_path missing");
2538
+ if (!text3.startsWith("# Changes here will be overwritten by Copier; NEVER EDIT MANUALLY")) details.push("missing Copier overwrite warning header");
2539
+ if (!text3.includes("_src_path:")) details.push("_src_path missing");
2631
2540
  if (project?.project_name) {
2632
- const nameMatch = text4.match(/project_name:\s*(.+)/);
2541
+ const nameMatch = text3.match(/project_name:\s*(.+)/);
2633
2542
  if (!nameMatch || nameMatch[1]?.trim() !== String(project.project_name)) details.push("project_name drift between .copier-answers.yml and .project.json");
2634
2543
  }
2635
2544
  if (project?.project_description) {
2636
- const descMatch = text4.match(/project_description:\s*([\s\S]*?)(?=\n\w|$)/);
2545
+ const descMatch = text3.match(/project_description:\s*([\s\S]*?)(?=\n\w|$)/);
2637
2546
  const yamlDesc = descMatch?.[1]?.replace(/\n\s+/g, " ").trim() ?? "";
2638
2547
  if (yamlDesc !== String(project.project_description)) details.push("project_description drift between .copier-answers.yml and .project.json");
2639
2548
  }
@@ -2650,16 +2559,16 @@ var RULES = [
2650
2559
  migrate: (ctx, finding) => {
2651
2560
  const changedFiles = [];
2652
2561
  const project = canonicalProjectJson(ctx);
2653
- const text4 = `# Changes here will be overwritten by Copier; NEVER EDIT MANUALLY
2562
+ const text3 = `# Changes here will be overwritten by Copier; NEVER EDIT MANUALLY
2654
2563
  _src_path: ${join9(ctx.pjanglerRoot, "templates", "commonproject")}
2655
2564
  project_description: ${String(project.project_description)}
2656
2565
  project_name: ${String(project.project_name)}
2657
2566
  ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
2658
2567
  `;
2659
2568
  const path = join9(ctx.repoRoot, ".copier-answers.yml");
2660
- if (safeReadText(path) !== text4) {
2569
+ if (safeReadText(path) !== text3) {
2661
2570
  changedFiles.push(path);
2662
- if (!ctx.dryRun) writeText(path, text4);
2571
+ if (!ctx.dryRun) writeText(path, text3);
2663
2572
  }
2664
2573
  return {
2665
2574
  id: finding.id,
@@ -3467,7 +3376,7 @@ function isInteractiveProjectInit(options) {
3467
3376
  return !options.json && !options.yes && options.tui !== false && Boolean(process.stdin.isTTY && process.stdout.isTTY);
3468
3377
  }
3469
3378
  async function promptTextValue(message, initialValue) {
3470
- const value = await text3({
3379
+ const value = await text2({
3471
3380
  message,
3472
3381
  initialValue,
3473
3382
  validate: (input) => input?.trim() ? void 0 : "Required"
@@ -3955,7 +3864,7 @@ program.command("migrate").argument("[rule-id]", "Rule ID to migrate (omit to op
3955
3864
  process.exit(1);
3956
3865
  }
3957
3866
  });
3958
- program.command("hermes-agent").alias("hermes").description("Provision a Hermes agent role into the current repo (TUI; --yes for non-interactive)").option("-y, --yes", "Non-interactive: accept all defaults (skips Telegram + email)").option("--target-repo <name>", "Target repo name (default: basename of cwd)").option("--role <role>", "Agent role (pm | dev | review | ops | qa | ci | ...)").option("--purpose <text>", "One-line agent purpose").option(`--tone <tone>`, `Personality tone (${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 BotFather token capture step").option("--skip-email", "Skip Cloudflare Email Routing step").option("--skip-runtime-repo", "Skip creating the per-agent runtime GH repo").option("--skip-plane", "Skip creating the Plane project").option("--skip-bloodbank", "Skip installing the Bloodbank NATS consumer").option("--skip-systemd", "Skip installing systemd --user units").option("--local", "Local-only: skip runtime repo, Plane, 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) => {
3867
+ 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 the Plane project").option("--skip-bloodbank", "Skip installing the Bloodbank NATS consumer").option("--skip-systemd", "Skip installing systemd --user units").option("--local", "Local-only: skip runtime repo, Plane, 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) => {
3959
3868
  const isDarwin = process.platform === "darwin";
3960
3869
  const local = options.local ?? false;
3961
3870
  const context = {
@@ -3972,7 +3881,8 @@ program.command("hermes-agent").alias("hermes").description("Provision a Hermes
3972
3881
  modelProvider: options.modelProvider,
3973
3882
  modelName: options.modelName,
3974
3883
  skipTelegram: options.skipTelegram,
3975
- skipEmail: options.skipEmail,
3884
+ // Email is opt-in only: `--email` wires it, otherwise it's never done.
3885
+ skipEmail: options.email ? false : void 0,
3976
3886
  // --local (and macOS, for systemd) flip the heavy/irreversible steps off
3977
3887
  // by default so a non-technical operator can't accidentally create cloud
3978
3888
  // resources under the wrong account or hit systemd on a Mac. An explicit
@@ -702,19 +702,6 @@ import * as p from "@clack/prompts";
702
702
 
703
703
  // src/commands/hermes/types.ts
704
704
  var HERMES_AGENT_TEMPLATE = "gh:delorenj/hermes-agent-template";
705
- var SOUL_TONES = ["direct", "playful", "formal", "terse"];
706
- var ROLE_CHOICES = [
707
- { value: "pm", label: "Project Manager (pm)", hint: "triage, planning, ticket authorship, board reconciliation" },
708
- { value: "dev", label: "Developer (dev)", hint: "implements tickets" },
709
- { value: "review", label: "Reviewer (review)", hint: "adversarial code review" },
710
- { value: "ops", label: "Ops (ops)", hint: "deploy / infra" },
711
- { value: "qa", label: "QA (qa)", hint: "test authorship + verification" }
712
- ];
713
- var TICKET_PROVIDERS = [
714
- { value: "plane", label: "Plane", hint: "self-hosted at plane.delo.sh (default)" },
715
- { value: "linear", label: "Linear", hint: "team board (created in Linear UI)" },
716
- { value: "trello", label: "Trello", hint: "board = project" }
717
- ];
718
705
  function deriveAgentId(repo, role) {
719
706
  return `${repo}-${role}`.toLowerCase();
720
707
  }
@@ -735,19 +722,18 @@ var PromptForAgentConfig = class extends Command {
735
722
  async invoke() {
736
723
  const ctx = this.context;
737
724
  const defaultRepo = basename(ctx.targetDir).toLowerCase();
738
- const defaultRole = "pm";
725
+ ctx.targetRepo = (ctx.targetRepo ?? defaultRepo).toLowerCase();
726
+ ctx.role ??= "pm";
727
+ ctx.agentPurpose ??= `${ctx.role} agent for ${ctx.targetRepo}`;
728
+ ctx.soulTone ??= "direct";
729
+ ctx.modelProvider ??= "";
730
+ ctx.modelName ??= "";
731
+ ctx.ticketProvider ??= detectTicketProvider(ctx.targetDir) ?? "plane";
732
+ ctx.skipEmail ??= true;
733
+ ctx.agentId = deriveAgentId(ctx.targetRepo, ctx.role);
734
+ ctx.profileName = deriveProfileName(ctx.targetRepo, ctx.role);
739
735
  if (ctx.yes) {
740
- ctx.targetRepo = (ctx.targetRepo ?? defaultRepo).toLowerCase();
741
- ctx.role ??= defaultRole;
742
- ctx.agentPurpose ??= `${ctx.role} agent for ${ctx.targetRepo}`;
743
- ctx.soulTone ??= "direct";
744
- ctx.modelProvider ??= "";
745
- ctx.modelName ??= "";
746
- ctx.ticketProvider ??= detectTicketProvider(ctx.targetDir) ?? "plane";
747
736
  ctx.skipTelegram ??= true;
748
- ctx.skipEmail ??= true;
749
- ctx.agentId = deriveAgentId(ctx.targetRepo, ctx.role);
750
- ctx.profileName = deriveProfileName(ctx.targetRepo, ctx.role);
751
737
  return {
752
738
  success: true,
753
739
  message: this.formatMessage(
@@ -755,96 +741,19 @@ var PromptForAgentConfig = class extends Command {
755
741
  )
756
742
  };
757
743
  }
758
- p.intro("\u2695 hermes-agent \xB7 add a new agent role to this repo");
759
- if (!ctx.targetRepo) {
760
- const answer = await p.text({
761
- message: "Target repo name",
762
- placeholder: defaultRepo,
763
- initialValue: defaultRepo,
764
- validate: (v) => v && v.trim() ? void 0 : "required"
765
- });
766
- if (p.isCancel(answer)) return this.cancelled();
767
- ctx.targetRepo = String(answer).trim().toLowerCase();
768
- }
769
- if (!ctx.role) {
770
- const answer = await p.select({
771
- message: "Role",
772
- options: ROLE_CHOICES.map((r) => ({ value: r.value, label: r.label, hint: r.hint })),
773
- initialValue: defaultRole
774
- });
775
- if (p.isCancel(answer)) return this.cancelled();
776
- ctx.role = String(answer).trim();
777
- }
778
- if (ctx.ticketProvider === void 0) {
779
- const detected = detectTicketProvider(ctx.targetDir);
780
- const answer = await p.select({
781
- message: "Ticket board provider",
782
- options: TICKET_PROVIDERS.map((t) => ({
783
- value: t.value,
784
- label: t.label,
785
- hint: t.value === detected ? `${t.hint} \u2014 current .project.json` : t.hint
786
- })),
787
- initialValue: detected ?? "plane"
788
- });
789
- if (p.isCancel(answer)) return this.cancelled();
790
- ctx.ticketProvider = answer;
791
- }
792
- if (!ctx.agentPurpose) {
793
- const answer = await p.text({
794
- message: "One-line purpose",
795
- placeholder: `${ctx.role} agent for ${ctx.targetRepo}`,
796
- initialValue: `${ctx.role} agent for ${ctx.targetRepo}`
797
- });
798
- if (p.isCancel(answer)) return this.cancelled();
799
- ctx.agentPurpose = String(answer).trim();
800
- }
801
- if (!ctx.soulTone) {
802
- const answer = await p.select({
803
- message: "Personality tone",
804
- options: SOUL_TONES.map((t) => ({
805
- value: t,
806
- label: t,
807
- hint: t === "direct" ? "decision-forward, no preamble (default)" : t === "terse" ? "minimum words, conclusion-first" : t === "playful" ? "warm, mildly funny" : "precise, structured"
808
- })),
809
- initialValue: "direct"
810
- });
811
- if (p.isCancel(answer)) return this.cancelled();
812
- ctx.soulTone = answer;
813
- }
814
- if (ctx.modelProvider === void 0) {
815
- const answer = await p.text({
816
- message: "Provider override (empty = inherit shared default profile)",
817
- placeholder: ""
818
- });
819
- if (p.isCancel(answer)) return this.cancelled();
820
- ctx.modelProvider = String(answer).trim();
821
- }
822
- if (ctx.modelName === void 0) {
823
- const answer = await p.text({
824
- message: "Model name override (empty = inherit shared default profile)",
825
- placeholder: ""
826
- });
827
- if (p.isCancel(answer)) return this.cancelled();
828
- ctx.modelName = String(answer).trim();
829
- }
744
+ p.intro("\u2695 hermes-agent \xB7 provision the PM agent for this repo");
745
+ p.log.info(
746
+ `agent ${ctx.agentId} \xB7 board ${ctx.ticketProvider} \xB7 tone ${ctx.soulTone}`
747
+ );
830
748
  if (ctx.skipTelegram === void 0) {
749
+ const botHandle = `${ctx.targetRepo.replace(/-/g, "_")}_${ctx.role}_bot`;
831
750
  const wire = await p.confirm({
832
- message: `Wire up the Telegram bot (@${ctx.targetRepo}_${ctx.role}_bot) now?`,
751
+ message: `Wire up the Telegram bot (@${botHandle}) now?`,
833
752
  initialValue: true
834
753
  });
835
754
  if (p.isCancel(wire)) return this.cancelled();
836
755
  ctx.skipTelegram = !wire;
837
756
  }
838
- if (ctx.skipEmail === void 0) {
839
- const wire = await p.confirm({
840
- message: `Provision the delo.sh email address (${ctx.targetRepo}-${ctx.role}@delo.sh) now?`,
841
- initialValue: true
842
- });
843
- if (p.isCancel(wire)) return this.cancelled();
844
- ctx.skipEmail = !wire;
845
- }
846
- ctx.agentId = deriveAgentId(ctx.targetRepo, ctx.role);
847
- ctx.profileName = deriveProfileName(ctx.targetRepo, ctx.role);
848
757
  return {
849
758
  success: true,
850
759
  message: this.formatMessage(
@@ -1119,7 +1028,7 @@ var WireEmail = class extends Command {
1119
1028
  async invoke() {
1120
1029
  const ctx = this.context;
1121
1030
  if (ctx.skipEmail) {
1122
- return { success: true, message: "\u2192 Email wire-up skipped" };
1031
+ return { success: true, message: "" };
1123
1032
  }
1124
1033
  if (ctx.dryRun) {
1125
1034
  return { success: true, message: this.formatMessage("Would create CF Email Routing rule") };
@@ -1227,7 +1136,7 @@ var PrintHermesSummary = class extends Command {
1227
1136
  lines.push(`role dir ${ctx.roleDir}`);
1228
1137
  lines.push(`runtime gh:${runtimeRepo}`);
1229
1138
  lines.push(`telegram @${botHandle}${skipTelegram ? " (NOT yet wired)" : ""}`);
1230
- lines.push(`email ${email}${skipEmail ? " (NOT yet wired)" : ""}`);
1139
+ if (!skipEmail) lines.push(`email ${email}`);
1231
1140
  lines.push("");
1232
1141
  lines.push("Start daemons:");
1233
1142
  lines.push(` systemctl --user start ${csm}`);
@@ -1240,11 +1149,10 @@ var PrintHermesSummary = class extends Command {
1240
1149
  lines.push("");
1241
1150
  lines.push("Talk locally:");
1242
1151
  lines.push(` ${ctx.roleDir}/hermes chat "status"`);
1243
- if (skipTelegram || skipEmail) {
1152
+ if (skipTelegram) {
1244
1153
  lines.push("");
1245
- lines.push("Deferred \u2014 re-run pjangler hermes-agent without --yes (or with explicit flags):");
1246
- if (skipTelegram) lines.push(" pjangler hermes-agent --skip-telegram=false # wire just telegram");
1247
- if (skipEmail) lines.push(" pjangler hermes-agent --skip-email=false # wire just email");
1154
+ lines.push("Wire Telegram later:");
1155
+ lines.push(" pjangler hermes-agent # re-run and answer yes when asked");
1248
1156
  }
1249
1157
  p5.note(lines.join("\n"), `Provisioned ${agentId}`);
1250
1158
  p5.outro("Done.");
@@ -1707,10 +1615,10 @@ function writeText(path, content) {
1707
1615
  ensureParent(path);
1708
1616
  writeFileSync4(path, content);
1709
1617
  }
1710
- function tryParseJson(text3) {
1711
- if (!text3) return null;
1618
+ function tryParseJson(text2) {
1619
+ if (!text2) return null;
1712
1620
  try {
1713
- return JSON.parse(text3);
1621
+ return JSON.parse(text2);
1714
1622
  } catch {
1715
1623
  return null;
1716
1624
  }
@@ -1769,9 +1677,9 @@ function bootstrapAgentsFile(repoRoot, dryRun) {
1769
1677
  }
1770
1678
  return { changedFiles: [], details: [], blocked: "AGENTS.md missing and no CLAUDE.md, GEMINI.md, or README.md source exists" };
1771
1679
  }
1772
- function yamlGet(text3, keyPath) {
1680
+ function yamlGet(text2, keyPath) {
1773
1681
  const parts = keyPath.split(".");
1774
- const lines = text3.split("\n");
1682
+ const lines = text2.split("\n");
1775
1683
  let start = 0;
1776
1684
  let indent = 0;
1777
1685
  for (let idx = 0; idx < parts.length; idx += 1) {
@@ -1806,25 +1714,25 @@ function discoverRoles(repoRoot) {
1806
1714
  const roleDir = join10(rolesDir, entry.name);
1807
1715
  const roleYamlPath = join10(roleDir, "role.yaml");
1808
1716
  if (!existsSync7(roleYamlPath)) return null;
1809
- const text3 = readText(roleYamlPath);
1810
- const runtimeRepoRaw = yamlGet(text3, "runtime.github_repo");
1717
+ const text2 = readText(roleYamlPath);
1718
+ const runtimeRepoRaw = yamlGet(text2, "runtime.github_repo");
1811
1719
  return {
1812
- role: yamlGet(text3, "role") || entry.name,
1720
+ role: yamlGet(text2, "role") || entry.name,
1813
1721
  roleDir,
1814
1722
  roleYamlPath,
1815
- repo: yamlGet(text3, "repo"),
1816
- agentId: yamlGet(text3, "agent_id"),
1817
- profileName: yamlGet(text3, "profile") || yamlGet(text3, "agent_id"),
1818
- displayName: yamlGet(text3, "display_name"),
1819
- purpose: yamlGet(text3, "purpose"),
1820
- botHandle: yamlGet(text3, "telegram.bot_username"),
1723
+ repo: yamlGet(text2, "repo"),
1724
+ agentId: yamlGet(text2, "agent_id"),
1725
+ profileName: yamlGet(text2, "profile") || yamlGet(text2, "agent_id"),
1726
+ displayName: yamlGet(text2, "display_name"),
1727
+ purpose: yamlGet(text2, "purpose"),
1728
+ botHandle: yamlGet(text2, "telegram.bot_username"),
1821
1729
  runtimeRepo: runtimeRepoRaw.includes("/") ? runtimeRepoRaw.split("/").slice(-1)[0] ?? runtimeRepoRaw : runtimeRepoRaw,
1822
- runtimeOwner: yamlGet(text3, "runtime.github_owner"),
1823
- planeWorkspace: yamlGet(text3, "ticket_provider.workspace") || yamlGet(text3, "plane.workspace"),
1824
- ticketProviderName: yamlGet(text3, "ticket_provider.name"),
1825
- ticketProviderBoardId: yamlGet(text3, "ticket_provider.board_id"),
1826
- ticketProviderBoardUrl: yamlGet(text3, "ticket_provider.board_url"),
1827
- ticketProviderIdentifier: yamlGet(text3, "plane.identifier")
1730
+ runtimeOwner: yamlGet(text2, "runtime.github_owner"),
1731
+ planeWorkspace: yamlGet(text2, "ticket_provider.workspace") || yamlGet(text2, "plane.workspace"),
1732
+ ticketProviderName: yamlGet(text2, "ticket_provider.name"),
1733
+ ticketProviderBoardId: yamlGet(text2, "ticket_provider.board_id"),
1734
+ ticketProviderBoardUrl: yamlGet(text2, "ticket_provider.board_url"),
1735
+ ticketProviderIdentifier: yamlGet(text2, "plane.identifier")
1828
1736
  };
1829
1737
  }).filter((value) => Boolean(value));
1830
1738
  }
@@ -1869,19 +1777,19 @@ function templateVersionFilesConf(ctx, repoRoot) {
1869
1777
  const packageJson = join10(repoRoot, "package.json");
1870
1778
  return existsSync7(packageJson) ? "# mise-versioning manifest: <type> <path>\n# types: json toml cargo csproj gradle plain gittag\njson package.json\ngittag .\n" : "# mise-versioning manifest: <type> <path>\n# types: json toml cargo csproj gradle plain gittag\ngittag .\n";
1871
1779
  }
1872
- function replaceOrAppendManagedBlock(text3, startMarker, block, beforePattern) {
1873
- if (startMarker.test(text3)) {
1874
- return text3.replace(/# >>> mise-versioning >>>[\s\S]*?# <<< mise-versioning <<</, block);
1780
+ function replaceOrAppendManagedBlock(text2, startMarker, block, beforePattern) {
1781
+ if (startMarker.test(text2)) {
1782
+ return text2.replace(/# >>> mise-versioning >>>[\s\S]*?# <<< mise-versioning <<</, block);
1875
1783
  }
1876
1784
  if (beforePattern) {
1877
- const match = text3.match(beforePattern);
1785
+ const match = text2.match(beforePattern);
1878
1786
  if (match && typeof match.index === "number") {
1879
- return `${text3.slice(0, match.index).replace(/\s*$/, "\n\n")}${block}
1787
+ return `${text2.slice(0, match.index).replace(/\s*$/, "\n\n")}${block}
1880
1788
 
1881
- ${text3.slice(match.index)}`;
1789
+ ${text2.slice(match.index)}`;
1882
1790
  }
1883
1791
  }
1884
- return `${text3.replace(/\s*$/, "")}
1792
+ return `${text2.replace(/\s*$/, "")}
1885
1793
 
1886
1794
  ${block}
1887
1795
  `;
@@ -1895,18 +1803,18 @@ function requiredMisePathEntries(ctx) {
1895
1803
  }
1896
1804
  return required;
1897
1805
  }
1898
- function upsertMisePath(text3, required = BASE_MISE_PATH_ENTRIES) {
1806
+ function upsertMisePath(text2, required = BASE_MISE_PATH_ENTRIES) {
1899
1807
  const render = (values) => `_.path = [${values.map((value) => JSON.stringify(value)).join(", ")}]`;
1900
- const envMatch = text3.match(/(^|\n)(\[env\][\s\S]*?)(?=\n\[[^\]]+\]|$)/);
1808
+ const envMatch = text2.match(/(^|\n)(\[env\][\s\S]*?)(?=\n\[[^\]]+\]|$)/);
1901
1809
  if (!envMatch || typeof envMatch.index !== "number") {
1902
1810
  return `[env]
1903
1811
  ${render(required)}
1904
1812
 
1905
- ${text3.replace(/^\s+/, "")}`;
1813
+ ${text2.replace(/^\s+/, "")}`;
1906
1814
  }
1907
- const prefix = text3.slice(0, envMatch.index + envMatch[1].length);
1815
+ const prefix = text2.slice(0, envMatch.index + envMatch[1].length);
1908
1816
  const section = envMatch[2];
1909
- const suffix = text3.slice(envMatch.index + envMatch[1].length + section.length);
1817
+ const suffix = text2.slice(envMatch.index + envMatch[1].length + section.length);
1910
1818
  const pathLine = section.match(/^_\.path\s*=\s*\[([^\]]*)\]\s*$/m);
1911
1819
  if (!pathLine) {
1912
1820
  return `${prefix}${section.replace(/\n?$/, "\n")}${render(required)}${suffix}`;
@@ -1917,11 +1825,11 @@ ${text3.replace(/^\s+/, "")}`;
1917
1825
  if (!merged.includes(value)) merged.push(value);
1918
1826
  }
1919
1827
  const nextLine = render(merged);
1920
- if (pathLine[0] === nextLine) return text3;
1828
+ if (pathLine[0] === nextLine) return text2;
1921
1829
  return `${prefix}${section.replace(pathLine[0], nextLine)}${suffix}`;
1922
1830
  }
1923
- function removeTomlSection(text3, headerPattern, marker, options) {
1924
- const lines = text3.split("\n");
1831
+ function removeTomlSection(text2, headerPattern, marker, options) {
1832
+ const lines = text2.split("\n");
1925
1833
  let start = -1;
1926
1834
  let end = -1;
1927
1835
  for (let i = 0; i < lines.length; i++) {
@@ -1946,7 +1854,7 @@ function removeTomlSection(text3, headerPattern, marker, options) {
1946
1854
  if (end === -1) end = lines.length;
1947
1855
  break;
1948
1856
  }
1949
- if (start === -1) return text3;
1857
+ if (start === -1) return text2;
1950
1858
  if (options?.includePrecedingComments) {
1951
1859
  while (start > 0 && lines[start - 1].trim().startsWith("#")) {
1952
1860
  start--;
@@ -1955,22 +1863,22 @@ function removeTomlSection(text3, headerPattern, marker, options) {
1955
1863
  const result = lines.slice(0, start).concat(lines.slice(end)).join("\n");
1956
1864
  return result.replace(/\n{3,}/g, "\n\n").replace(/\n+$/, "\n");
1957
1865
  }
1958
- function insertTomlBlockBeforeVersioning(text3, block) {
1959
- const versioningIndex = text3.indexOf("# >>> mise-versioning >>>");
1866
+ function insertTomlBlockBeforeVersioning(text2, block) {
1867
+ const versioningIndex = text2.indexOf("# >>> mise-versioning >>>");
1960
1868
  if (versioningIndex >= 0) {
1961
- return `${text3.slice(0, versioningIndex).replace(/\s*$/, "\n\n")}${block}
1869
+ return `${text2.slice(0, versioningIndex).replace(/\s*$/, "\n\n")}${block}
1962
1870
 
1963
- ${text3.slice(versioningIndex)}`;
1871
+ ${text2.slice(versioningIndex)}`;
1964
1872
  }
1965
- return `${text3.replace(/\s*$/, "")}
1873
+ return `${text2.replace(/\s*$/, "")}
1966
1874
 
1967
1875
  ${block}
1968
1876
  `;
1969
1877
  }
1970
- function extractTomlStrings(text3) {
1878
+ function extractTomlStrings(text2) {
1971
1879
  const values = [];
1972
1880
  const stringPattern = /"((?:\\.|[^"\\])*)"|'([^']*)'/g;
1973
- for (const match of text3.matchAll(stringPattern)) {
1881
+ for (const match of text2.matchAll(stringPattern)) {
1974
1882
  if (match[1] !== void 0) {
1975
1883
  try {
1976
1884
  values.push(JSON.parse(`"${match[1]}"`));
@@ -1994,10 +1902,10 @@ function renderHookEntries(entries, indent = "") {
1994
1902
  `${indent}]`
1995
1903
  ];
1996
1904
  }
1997
- function upsertLinkAgentfilesHooks(text3) {
1998
- const lines = text3.split("\n");
1905
+ function upsertLinkAgentfilesHooks(text2) {
1906
+ const lines = text2.split("\n");
1999
1907
  const hooksStart = lines.findIndex((line) => /^\[hooks\]$/.test(line.trim()));
2000
- if (hooksStart === -1) return insertTomlBlockBeforeVersioning(text3, LINK_AGENTFILES_HOOKS_BLOCK);
1908
+ if (hooksStart === -1) return insertTomlBlockBeforeVersioning(text2, LINK_AGENTFILES_HOOKS_BLOCK);
2001
1909
  let hooksEnd = lines.length;
2002
1910
  for (let i = hooksStart + 1; i < lines.length; i++) {
2003
1911
  if (/^\[[^\]]+\]/.test(lines[i].trim())) {
@@ -2031,8 +1939,8 @@ function upsertLinkAgentfilesHooks(text3) {
2031
1939
  }
2032
1940
  return lines.slice(0, hooksStart + 1).concat(rendered, lines.slice(hooksStart + 1)).join("\n").replace(/\n{3,}/g, "\n\n").replace(/\n+$/, "\n");
2033
1941
  }
2034
- function upsertLinkAgentfilesBlock(text3, ctx) {
2035
- const withPath = upsertMisePath(text3, requiredMisePathEntries(ctx));
1942
+ function upsertLinkAgentfilesBlock(text2, ctx) {
1943
+ const withPath = upsertMisePath(text2, requiredMisePathEntries(ctx));
2036
1944
  if (withPath.includes(LINK_AGENTFILES_BLOCK)) return withPath;
2037
1945
  let cleaned = removeTomlSection(withPath, /^\[tasks\.link-agentfiles\]$/, /link-agentfiles/, { includePrecedingComments: false });
2038
1946
  cleaned = removeTomlSection(cleaned, /^\[\[watch_files\]\]$/, /AGENTS\.md/, { includePrecedingComments: false });
@@ -2259,9 +2167,9 @@ ${block}`) : `${current.replace(/\s*$/, "\n")}${block}`;
2259
2167
  return path;
2260
2168
  }
2261
2169
  function profileMetaInheritsDefault(path) {
2262
- const text3 = safeReadText(path);
2170
+ const text2 = safeReadText(path);
2263
2171
  return Boolean(
2264
- text3 && /^config:\s*$/m.test(text3) && /^\s+inherit_from:\s*default\s*$/m.test(text3) && /^\s+save_mode:\s*delta\s*$/m.test(text3)
2172
+ text2 && /^config:\s*$/m.test(text2) && /^\s+inherit_from:\s*default\s*$/m.test(text2) && /^\s+save_mode:\s*delta\s*$/m.test(text2)
2265
2173
  );
2266
2174
  }
2267
2175
  function upsertInheritedProfileMeta(path, changedFiles, dryRun) {
@@ -2319,17 +2227,17 @@ var RULES = [
2319
2227
  if (!existsSync7(misePath)) {
2320
2228
  return { id: "mise.config-root", title: "mise config_root + AGENTS link hooks", status: "fail", summary: "mise.toml missing", details: [], fixable: true };
2321
2229
  }
2322
- const text3 = readText(misePath);
2230
+ const text2 = readText(misePath);
2323
2231
  const details = [];
2324
2232
  const linkAgentfilesPath = join10(ctx.repoRoot, ".mise", "scripts", "link-agentfiles.sh");
2325
2233
  if (!existsSync7(linkAgentfilesPath)) details.push(".mise/scripts/link-agentfiles.sh missing");
2326
- const pathValues = [...(text3.match(/^_\.path\s*=\s*\[([^\]]*)\]/m)?.[1] ?? "").matchAll(/"([^"]+)"/g)].map((match) => match[1]);
2234
+ const pathValues = [...(text2.match(/^_\.path\s*=\s*\[([^\]]*)\]/m)?.[1] ?? "").matchAll(/"([^"]+)"/g)].map((match) => match[1]);
2327
2235
  const missingPathValues = requiredMisePathEntries(ctx).filter((value) => !pathValues.includes(value));
2328
2236
  if (missingPathValues.length) details.push(`[env]._.path should include ${missingPathValues.join(", ")}`);
2329
- if (!text3.includes('"{{config_root}}/.mise/scripts/link-agentfiles.sh"')) details.push("link-agentfiles must use raw {{config_root}} guard");
2330
- if (!text3.includes("op inject -i .env.op > .env")) details.push("[hooks].enter must materialize .env from .env.op");
2331
- if (!text3.includes('patterns = ["AGENTS.md"]')) details.push("watch_files must monitor AGENTS.md");
2332
- if (!text3.includes('task = "link-agentfiles"')) details.push("watch_files must dispatch link-agentfiles task");
2237
+ if (!text2.includes('"{{config_root}}/.mise/scripts/link-agentfiles.sh"')) details.push("link-agentfiles must use raw {{config_root}} guard");
2238
+ if (!text2.includes("op inject -i .env.op > .env")) details.push("[hooks].enter must materialize .env from .env.op");
2239
+ if (!text2.includes('patterns = ["AGENTS.md"]')) details.push("watch_files must monitor AGENTS.md");
2240
+ if (!text2.includes('task = "link-agentfiles"')) details.push("watch_files must dispatch link-agentfiles task");
2333
2241
  return {
2334
2242
  id: "mise.config-root",
2335
2243
  title: "mise config_root + AGENTS link hooks",
@@ -2352,12 +2260,12 @@ var RULES = [
2352
2260
  return { id: finding.id, title: finding.title, status: "applied", summary: "Would initialize mise.toml from generated-project template", changedFiles, details };
2353
2261
  }
2354
2262
  }
2355
- let text3 = readText(path);
2356
- const next = upsertLinkAgentfilesBlock(text3, ctx);
2357
- if (next !== text3) {
2263
+ let text2 = readText(path);
2264
+ const next = upsertLinkAgentfilesBlock(text2, ctx);
2265
+ if (next !== text2) {
2358
2266
  if (!changedFiles.includes(path)) changedFiles.push(path);
2359
2267
  if (!ctx.dryRun) writeText(path, next);
2360
- text3 = next;
2268
+ text2 = next;
2361
2269
  }
2362
2270
  const linkAgentfilesPath = join10(ctx.repoRoot, ".mise", "scripts", "link-agentfiles.sh");
2363
2271
  const expectedScript = templateLinkAgentfilesScript(ctx);
@@ -2389,8 +2297,8 @@ var RULES = [
2389
2297
  const misePath = join10(ctx.repoRoot, "mise.toml");
2390
2298
  const versioningPath = join10(ctx.repoRoot, ".mise", "scripts", "versioning.sh");
2391
2299
  const manifestPath = join10(ctx.repoRoot, ".mise", "version-files.conf");
2392
- const text3 = safeReadText(misePath);
2393
- if (!text3?.includes("# >>> mise-versioning >>>")) details.push("mise versioning managed block missing");
2300
+ const text2 = safeReadText(misePath);
2301
+ if (!text2?.includes("# >>> mise-versioning >>>")) details.push("mise versioning managed block missing");
2394
2302
  if (!existsSync7(versioningPath)) details.push(".mise/scripts/versioning.sh missing");
2395
2303
  if (!existsSync7(manifestPath)) details.push(".mise/version-files.conf missing");
2396
2304
  return {
@@ -2611,19 +2519,19 @@ var RULES = [
2611
2519
  audit: (ctx) => {
2612
2520
  const details = [];
2613
2521
  const path = join10(ctx.repoRoot, ".copier-answers.yml");
2614
- const text3 = safeReadText(path);
2522
+ const text2 = safeReadText(path);
2615
2523
  const project = readProjectJson(ctx);
2616
- if (!text3) {
2524
+ if (!text2) {
2617
2525
  details.push(".copier-answers.yml missing");
2618
2526
  } else {
2619
- if (!text3.startsWith("# Changes here will be overwritten by Copier; NEVER EDIT MANUALLY")) details.push("missing Copier overwrite warning header");
2620
- if (!text3.includes("_src_path:")) details.push("_src_path missing");
2527
+ if (!text2.startsWith("# Changes here will be overwritten by Copier; NEVER EDIT MANUALLY")) details.push("missing Copier overwrite warning header");
2528
+ if (!text2.includes("_src_path:")) details.push("_src_path missing");
2621
2529
  if (project?.project_name) {
2622
- const nameMatch = text3.match(/project_name:\s*(.+)/);
2530
+ const nameMatch = text2.match(/project_name:\s*(.+)/);
2623
2531
  if (!nameMatch || nameMatch[1]?.trim() !== String(project.project_name)) details.push("project_name drift between .copier-answers.yml and .project.json");
2624
2532
  }
2625
2533
  if (project?.project_description) {
2626
- const descMatch = text3.match(/project_description:\s*([\s\S]*?)(?=\n\w|$)/);
2534
+ const descMatch = text2.match(/project_description:\s*([\s\S]*?)(?=\n\w|$)/);
2627
2535
  const yamlDesc = descMatch?.[1]?.replace(/\n\s+/g, " ").trim() ?? "";
2628
2536
  if (yamlDesc !== String(project.project_description)) details.push("project_description drift between .copier-answers.yml and .project.json");
2629
2537
  }
@@ -2640,16 +2548,16 @@ var RULES = [
2640
2548
  migrate: (ctx, finding) => {
2641
2549
  const changedFiles = [];
2642
2550
  const project = canonicalProjectJson(ctx);
2643
- const text3 = `# Changes here will be overwritten by Copier; NEVER EDIT MANUALLY
2551
+ const text2 = `# Changes here will be overwritten by Copier; NEVER EDIT MANUALLY
2644
2552
  _src_path: ${join10(ctx.pjanglerRoot, "templates", "commonproject")}
2645
2553
  project_description: ${String(project.project_description)}
2646
2554
  project_name: ${String(project.project_name)}
2647
2555
  ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
2648
2556
  `;
2649
2557
  const path = join10(ctx.repoRoot, ".copier-answers.yml");
2650
- if (safeReadText(path) !== text3) {
2558
+ if (safeReadText(path) !== text2) {
2651
2559
  changedFiles.push(path);
2652
- if (!ctx.dryRun) writeText(path, text3);
2560
+ if (!ctx.dryRun) writeText(path, text2);
2653
2561
  }
2654
2562
  return {
2655
2563
  id: finding.id,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@delorenj/pjangler",
3
- "version": "1.2.3",
3
+ "version": "1.2.4",
4
4
  "description": "Project subsystem bootstrapper CLI",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",