@delorenj/pjangler 1.2.16 → 1.2.18

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
@@ -2122,72 +2122,48 @@ import { fileURLToPath as fileURLToPath3 } from "node:url";
2122
2122
  import { homedir as homedir5 } from "node:os";
2123
2123
  import { spawnSync as spawnSync6 } from "node:child_process";
2124
2124
  import YAML2 from "yaml";
2125
- var LINK_AGENTFILES_BLOCK = `# This block will handle the linking of
2125
+ var LINK_AGENTFILES_SCRIPT = "'{{config_root}}/.mise/scripts/link-agentfiles.sh'";
2126
+ var OP_INJECT_SCRIPT = "op inject -i .env.op > .env";
2127
+ var CODEGRAPH_SCRIPT = "[ -f '{{config_root}}/.mise/scripts/codegraph.sh' ] && '{{config_root}}/.mise/scripts/codegraph.sh' || true";
2128
+ var HOOKS_COMMENT_HEADER = `# This block will handle the linking of
2126
2129
  # agent files to the main AGENTS.md file.
2127
2130
  #
2128
2131
  # TODO: Ensure this works for all levels of nesting.
2129
2132
  # i.e. All linked agent files MUST be siblings at
2130
- # any given level of nesting.
2131
- [hooks]
2132
- enter = [
2133
- "{{config_root}}/.mise/scripts/link-agentfiles.sh",
2134
- "op inject -i .env.op > .env",
2135
- ]
2136
-
2137
- [[watch_files]]
2138
- patterns = ["AGENTS.md"]
2139
- task = "link-agentfiles"
2140
-
2141
- [tasks.link-agentfiles]
2142
- description = "Symlink all agent files to AGENTS.md"
2143
- run = "{{config_root}}/.mise/scripts/link-agentfiles.sh"`;
2144
- var LINK_AGENTFILES_HOOK_ENTRIES = [
2145
- "{{config_root}}/.mise/scripts/link-agentfiles.sh",
2146
- "op inject -i .env.op > .env"
2147
- ];
2148
- var LINK_AGENTFILES_HOOKS_BLOCK = `# This block will handle the linking of
2149
- # agent files to the main AGENTS.md file.
2150
- #
2151
- # TODO: Ensure this works for all levels of nesting.
2152
- # i.e. All linked agent files MUST be siblings at
2153
- # any given level of nesting.
2154
- [hooks]
2155
- enter = [
2156
- "{{config_root}}/.mise/scripts/link-agentfiles.sh",
2157
- "op inject -i .env.op > .env",
2158
- ]`;
2133
+ # any given level of nesting.`;
2134
+ var LINK_AGENTFILES_HOOK_ENTRIES = [LINK_AGENTFILES_SCRIPT, OP_INJECT_SCRIPT];
2159
2135
  var LINK_AGENTFILES_WATCH_TASK_BLOCK = `[[watch_files]]
2160
2136
  patterns = ["AGENTS.md"]
2161
2137
  task = "link-agentfiles"
2162
2138
 
2163
2139
  [tasks.link-agentfiles]
2164
2140
  description = "Symlink all agent files to AGENTS.md"
2165
- run = "{{config_root}}/.mise/scripts/link-agentfiles.sh"`;
2141
+ run = "'{{config_root}}/.mise/scripts/link-agentfiles.sh'"`;
2166
2142
  var VERSIONING_BLOCK = `# >>> mise-versioning >>> (managed block \u2014 do not edit by hand; re-run init to update)
2167
2143
  [tasks."version"]
2168
2144
  description = "Print the current version (vX.Y.Z)"
2169
- run = "{{config_root}}/.mise/scripts/versioning.sh current"
2145
+ run = "'{{config_root}}/.mise/scripts/versioning.sh' current"
2170
2146
 
2171
2147
  [tasks."version:bump"]
2172
2148
  description = "Bump patch version: vX.Y.Z -> vX.Y.(Z+1)"
2173
2149
  alias = "version:bump-patch"
2174
- run = "{{config_root}}/.mise/scripts/versioning.sh bump patch"
2150
+ run = "'{{config_root}}/.mise/scripts/versioning.sh' bump patch"
2175
2151
 
2176
2152
  [tasks."version:bump-minor"]
2177
2153
  description = "Bump minor version: vX.Y.Z -> vX.(Y+1).0"
2178
- run = "{{config_root}}/.mise/scripts/versioning.sh bump minor"
2154
+ run = "'{{config_root}}/.mise/scripts/versioning.sh' bump minor"
2179
2155
 
2180
2156
  [tasks."version:bump-major"]
2181
2157
  description = "Bump major version: vX.Y.Z -> v(X+1).0.0"
2182
- run = "{{config_root}}/.mise/scripts/versioning.sh bump major"
2158
+ run = "'{{config_root}}/.mise/scripts/versioning.sh' bump major"
2183
2159
 
2184
2160
  [tasks."version:check"]
2185
2161
  description = "Verify every versioned file is in parity"
2186
- run = "{{config_root}}/.mise/scripts/versioning.sh check"
2162
+ run = "'{{config_root}}/.mise/scripts/versioning.sh' check"
2187
2163
 
2188
2164
  [tasks."version:sync"]
2189
2165
  description = "Force every versioned file up to the highest version"
2190
- run = "{{config_root}}/.mise/scripts/versioning.sh sync"
2166
+ run = "'{{config_root}}/.mise/scripts/versioning.sh' sync"
2191
2167
  # <<< mise-versioning <<<`;
2192
2168
  function resolvePjanglerRoot2() {
2193
2169
  let dir = dirname6(fileURLToPath3(import.meta.url));
@@ -2488,6 +2464,9 @@ function removeTomlSection(text3, headerPattern, marker, options) {
2488
2464
  break;
2489
2465
  }
2490
2466
  if (start === -1) return text3;
2467
+ while (end > start + 1 && (lines[end - 1].trim() === "" || lines[end - 1].trim().startsWith("#"))) {
2468
+ end--;
2469
+ }
2491
2470
  if (options?.includePrecedingComments) {
2492
2471
  while (start > 0 && lines[start - 1].trim().startsWith("#")) {
2493
2472
  start--;
@@ -2529,60 +2508,108 @@ function stripTomlStringsAndComments(line) {
2529
2508
  }
2530
2509
  function isManagedHookEntry(value) {
2531
2510
  const trimmed = value.trim();
2532
- return trimmed === "op inject -i .env.op > .env" || /(^|\/)link-agentfiles\.sh$/.test(trimmed);
2511
+ if (trimmed === OP_INJECT_SCRIPT) return true;
2512
+ return /link-agentfiles\.sh'?\s*$/.test(trimmed);
2533
2513
  }
2534
- function renderHookEntries(entries, indent = "") {
2535
- return [
2536
- `${indent}enter = [`,
2537
- ...entries.map((entry) => `${indent} ${JSON.stringify(entry)},`),
2538
- `${indent}]`
2539
- ];
2514
+ function normalizeHookScript(script) {
2515
+ const trimmed = script.trim();
2516
+ if (/codegraph\.sh/.test(trimmed)) return CODEGRAPH_SCRIPT;
2517
+ return trimmed;
2540
2518
  }
2541
- function upsertLinkAgentfilesHooks(text3) {
2542
- const lines = text3.split("\n");
2543
- const hooksStart = lines.findIndex((line) => /^\[hooks\]$/.test(line.trim()));
2544
- if (hooksStart === -1) return insertTomlBlockBeforeVersioning(text3, LINK_AGENTFILES_HOOKS_BLOCK);
2545
- let hooksEnd = lines.length;
2546
- for (let i = hooksStart + 1; i < lines.length; i++) {
2547
- if (/^\[[^\]]+\]/.test(lines[i].trim())) {
2548
- hooksEnd = i;
2549
- break;
2519
+ function tomlValueSpanEnd(lines, start, limit) {
2520
+ let depth = 0;
2521
+ let j = start;
2522
+ for (; j < limit; j++) {
2523
+ for (const ch of stripTomlStringsAndComments(lines[j])) {
2524
+ if (ch === "[") depth++;
2525
+ else if (ch === "]") depth--;
2550
2526
  }
2527
+ if (depth <= 0) break;
2551
2528
  }
2552
- let enterStart = -1;
2553
- let enterEnd = -1;
2554
- for (let i = hooksStart + 1; i < hooksEnd; i++) {
2555
- if (!/^\s*enter\s*=/.test(lines[i])) continue;
2556
- enterStart = i;
2557
- let depth = 0;
2558
- let j = i;
2559
- for (; j < hooksEnd; j++) {
2560
- for (const ch of stripTomlStringsAndComments(lines[j])) {
2561
- if (ch === "[") depth++;
2562
- else if (ch === "]") depth--;
2529
+ return Math.min(j, limit - 1) + 1;
2530
+ }
2531
+ function stripHookBlocks(text3) {
2532
+ const lines = text3.split("\n");
2533
+ const enter = [];
2534
+ const leave = [];
2535
+ const drop = new Array(lines.length).fill(false);
2536
+ const isHeader = (line) => /^\[/.test(line.trim());
2537
+ const dropPrecedingComments = (idx) => {
2538
+ for (let k = idx - 1; k >= 0 && !drop[k] && lines[k].trim().startsWith("#"); k--) drop[k] = true;
2539
+ };
2540
+ for (let i = 0; i < lines.length; i++) {
2541
+ const trimmed = lines[i].trim();
2542
+ const tableMatch = /^\[\[\s*hooks\.(enter|leave)\s*\]\]$/.exec(trimmed);
2543
+ if (tableMatch) {
2544
+ const bucket = tableMatch[1] === "enter" ? enter : leave;
2545
+ dropPrecedingComments(i);
2546
+ drop[i] = true;
2547
+ let j = i + 1;
2548
+ for (; j < lines.length; j++) {
2549
+ const body = lines[j].trim();
2550
+ if (body === "" || body.startsWith("#") || isHeader(lines[j])) break;
2551
+ drop[j] = true;
2552
+ const scriptMatch = /^\s*script\s*=\s*(.+)$/.exec(lines[j]);
2553
+ if (scriptMatch) {
2554
+ const value = extractTomlStrings(scriptMatch[1])[0];
2555
+ if (value !== void 0) bucket.push(value);
2556
+ }
2563
2557
  }
2564
- if (depth <= 0) break;
2558
+ i = j - 1;
2559
+ continue;
2560
+ }
2561
+ if (trimmed === "[hooks]") {
2562
+ dropPrecedingComments(i);
2563
+ let j = i + 1;
2564
+ let lastDrop = i;
2565
+ while (j < lines.length && !isHeader(lines[j])) {
2566
+ const keyMatch = /^\s*(enter|leave)\s*=/.exec(lines[j]);
2567
+ if (keyMatch) {
2568
+ const bucket = keyMatch[1] === "enter" ? enter : leave;
2569
+ const end = tomlValueSpanEnd(lines, j, lines.length);
2570
+ for (const value of extractTomlStrings(lines.slice(j, end).join("\n"))) bucket.push(value);
2571
+ lastDrop = end - 1;
2572
+ j = end;
2573
+ } else if (/^\s*\]\s*$/.test(lines[j])) {
2574
+ lastDrop = j;
2575
+ j++;
2576
+ } else {
2577
+ j++;
2578
+ }
2579
+ }
2580
+ for (let k = i; k <= lastDrop; k++) drop[k] = true;
2581
+ i = j - 1;
2582
+ continue;
2565
2583
  }
2566
- enterEnd = Math.min(j, hooksEnd - 1) + 1;
2567
- while (enterEnd < hooksEnd && /^\s*\]\s*$/.test(lines[enterEnd])) enterEnd++;
2568
- break;
2569
- }
2570
- const existingBlock = enterStart >= 0 ? lines.slice(enterStart, enterEnd).join("\n") : "";
2571
- const preserved = extractTomlStrings(existingBlock).filter((entry) => !isManagedHookEntry(entry));
2572
- const merged = [...LINK_AGENTFILES_HOOK_ENTRIES];
2573
- for (const entry of preserved) {
2574
- if (!merged.includes(entry)) merged.push(entry);
2575
2584
  }
2576
- const indent = enterStart >= 0 ? lines[enterStart].match(/^\s*/)?.[0] ?? "" : "";
2577
- const rendered = renderHookEntries(merged, indent);
2578
- if (enterStart >= 0) {
2579
- return lines.slice(0, enterStart).concat(rendered, lines.slice(enterEnd)).join("\n").replace(/\n{3,}/g, "\n\n").replace(/\n+$/, "\n");
2585
+ const kept = lines.filter((_, idx) => !drop[idx]).join("\n").replace(/\n{3,}/g, "\n\n").replace(/\n+$/, "\n");
2586
+ return { text: kept, enter, leave };
2587
+ }
2588
+ function renderHookTables(scripts, kind) {
2589
+ return scripts.map((script) => `[[hooks.${kind}]]
2590
+ script = ${JSON.stringify(script)}`);
2591
+ }
2592
+ function dedupePreserve(scripts) {
2593
+ const out = [];
2594
+ for (const script of scripts) {
2595
+ if (script && !out.includes(script)) out.push(script);
2580
2596
  }
2581
- return lines.slice(0, hooksStart + 1).concat(rendered, lines.slice(hooksStart + 1)).join("\n").replace(/\n{3,}/g, "\n\n").replace(/\n+$/, "\n");
2597
+ return out;
2598
+ }
2599
+ function upsertLinkAgentfilesHooks(text3) {
2600
+ const { text: stripped, enter, leave } = stripHookBlocks(text3);
2601
+ const preservedEnter = enter.map(normalizeHookScript).filter((script) => !isManagedHookEntry(script));
2602
+ const enterScripts = dedupePreserve([...LINK_AGENTFILES_HOOK_ENTRIES, ...preservedEnter]);
2603
+ const leaveScripts = dedupePreserve(leave.map(normalizeHookScript));
2604
+ const block = [
2605
+ HOOKS_COMMENT_HEADER,
2606
+ ...renderHookTables(enterScripts, "enter"),
2607
+ ...renderHookTables(leaveScripts, "leave")
2608
+ ].join("\n");
2609
+ return insertTomlBlockBeforeVersioning(stripped, block);
2582
2610
  }
2583
2611
  function upsertLinkAgentfilesBlock(text3, ctx) {
2584
2612
  const withPath = upsertMisePath(text3, requiredMisePathEntries(ctx));
2585
- if (withPath.includes(LINK_AGENTFILES_BLOCK)) return withPath;
2586
2613
  let cleaned = removeTomlSection(withPath, /^\[tasks\.link-agentfiles\]$/, /link-agentfiles/, { includePrecedingComments: false });
2587
2614
  cleaned = removeTomlSection(cleaned, /^\[\[watch_files\]\]$/, /AGENTS\.md/, { includePrecedingComments: false });
2588
2615
  cleaned = upsertLinkAgentfilesHooks(cleaned);
@@ -3082,8 +3109,8 @@ var RULES = [
3082
3109
  const pathValues = [...(text3.match(/^_\.path\s*=\s*\[([^\]]*)\]/m)?.[1] ?? "").matchAll(/"([^"]+)"/g)].map((match) => match[1]);
3083
3110
  const missingPathValues = requiredMisePathEntries(ctx).filter((value) => !pathValues.includes(value));
3084
3111
  if (missingPathValues.length) details.push(`[env]._.path should include ${missingPathValues.join(", ")}`);
3085
- if (!text3.includes('"{{config_root}}/.mise/scripts/link-agentfiles.sh"')) details.push("link-agentfiles must use raw {{config_root}} guard");
3086
- if (!text3.includes("op inject -i .env.op > .env")) details.push("[hooks].enter must materialize .env from .env.op");
3112
+ if (!text3.includes("'{{config_root}}/.mise/scripts/link-agentfiles.sh'")) details.push("link-agentfiles hook must use single-quoted {{config_root}} guard");
3113
+ if (!text3.includes("op inject -i .env.op > .env")) details.push("hooks.enter must materialize .env from .env.op");
3087
3114
  if (!text3.includes('patterns = ["AGENTS.md"]')) details.push("watch_files must monitor AGENTS.md");
3088
3115
  if (!text3.includes('task = "link-agentfiles"')) details.push("watch_files must dispatch link-agentfiles task");
3089
3116
  return {
@@ -2049,72 +2049,48 @@ import { fileURLToPath as fileURLToPath4 } from "node:url";
2049
2049
  import { homedir as homedir5 } from "node:os";
2050
2050
  import { spawnSync as spawnSync6 } from "node:child_process";
2051
2051
  import YAML2 from "yaml";
2052
- var LINK_AGENTFILES_BLOCK = `# This block will handle the linking of
2052
+ var LINK_AGENTFILES_SCRIPT = "'{{config_root}}/.mise/scripts/link-agentfiles.sh'";
2053
+ var OP_INJECT_SCRIPT = "op inject -i .env.op > .env";
2054
+ var CODEGRAPH_SCRIPT = "[ -f '{{config_root}}/.mise/scripts/codegraph.sh' ] && '{{config_root}}/.mise/scripts/codegraph.sh' || true";
2055
+ var HOOKS_COMMENT_HEADER = `# This block will handle the linking of
2053
2056
  # agent files to the main AGENTS.md file.
2054
2057
  #
2055
2058
  # TODO: Ensure this works for all levels of nesting.
2056
2059
  # i.e. All linked agent files MUST be siblings at
2057
- # any given level of nesting.
2058
- [hooks]
2059
- enter = [
2060
- "{{config_root}}/.mise/scripts/link-agentfiles.sh",
2061
- "op inject -i .env.op > .env",
2062
- ]
2063
-
2064
- [[watch_files]]
2065
- patterns = ["AGENTS.md"]
2066
- task = "link-agentfiles"
2067
-
2068
- [tasks.link-agentfiles]
2069
- description = "Symlink all agent files to AGENTS.md"
2070
- run = "{{config_root}}/.mise/scripts/link-agentfiles.sh"`;
2071
- var LINK_AGENTFILES_HOOK_ENTRIES = [
2072
- "{{config_root}}/.mise/scripts/link-agentfiles.sh",
2073
- "op inject -i .env.op > .env"
2074
- ];
2075
- var LINK_AGENTFILES_HOOKS_BLOCK = `# This block will handle the linking of
2076
- # agent files to the main AGENTS.md file.
2077
- #
2078
- # TODO: Ensure this works for all levels of nesting.
2079
- # i.e. All linked agent files MUST be siblings at
2080
- # any given level of nesting.
2081
- [hooks]
2082
- enter = [
2083
- "{{config_root}}/.mise/scripts/link-agentfiles.sh",
2084
- "op inject -i .env.op > .env",
2085
- ]`;
2060
+ # any given level of nesting.`;
2061
+ var LINK_AGENTFILES_HOOK_ENTRIES = [LINK_AGENTFILES_SCRIPT, OP_INJECT_SCRIPT];
2086
2062
  var LINK_AGENTFILES_WATCH_TASK_BLOCK = `[[watch_files]]
2087
2063
  patterns = ["AGENTS.md"]
2088
2064
  task = "link-agentfiles"
2089
2065
 
2090
2066
  [tasks.link-agentfiles]
2091
2067
  description = "Symlink all agent files to AGENTS.md"
2092
- run = "{{config_root}}/.mise/scripts/link-agentfiles.sh"`;
2068
+ run = "'{{config_root}}/.mise/scripts/link-agentfiles.sh'"`;
2093
2069
  var VERSIONING_BLOCK = `# >>> mise-versioning >>> (managed block \u2014 do not edit by hand; re-run init to update)
2094
2070
  [tasks."version"]
2095
2071
  description = "Print the current version (vX.Y.Z)"
2096
- run = "{{config_root}}/.mise/scripts/versioning.sh current"
2072
+ run = "'{{config_root}}/.mise/scripts/versioning.sh' current"
2097
2073
 
2098
2074
  [tasks."version:bump"]
2099
2075
  description = "Bump patch version: vX.Y.Z -> vX.Y.(Z+1)"
2100
2076
  alias = "version:bump-patch"
2101
- run = "{{config_root}}/.mise/scripts/versioning.sh bump patch"
2077
+ run = "'{{config_root}}/.mise/scripts/versioning.sh' bump patch"
2102
2078
 
2103
2079
  [tasks."version:bump-minor"]
2104
2080
  description = "Bump minor version: vX.Y.Z -> vX.(Y+1).0"
2105
- run = "{{config_root}}/.mise/scripts/versioning.sh bump minor"
2081
+ run = "'{{config_root}}/.mise/scripts/versioning.sh' bump minor"
2106
2082
 
2107
2083
  [tasks."version:bump-major"]
2108
2084
  description = "Bump major version: vX.Y.Z -> v(X+1).0.0"
2109
- run = "{{config_root}}/.mise/scripts/versioning.sh bump major"
2085
+ run = "'{{config_root}}/.mise/scripts/versioning.sh' bump major"
2110
2086
 
2111
2087
  [tasks."version:check"]
2112
2088
  description = "Verify every versioned file is in parity"
2113
- run = "{{config_root}}/.mise/scripts/versioning.sh check"
2089
+ run = "'{{config_root}}/.mise/scripts/versioning.sh' check"
2114
2090
 
2115
2091
  [tasks."version:sync"]
2116
2092
  description = "Force every versioned file up to the highest version"
2117
- run = "{{config_root}}/.mise/scripts/versioning.sh sync"
2093
+ run = "'{{config_root}}/.mise/scripts/versioning.sh' sync"
2118
2094
  # <<< mise-versioning <<<`;
2119
2095
  function resolvePjanglerRoot2() {
2120
2096
  let dir = dirname7(fileURLToPath4(import.meta.url));
@@ -2415,6 +2391,9 @@ function removeTomlSection(text2, headerPattern, marker, options) {
2415
2391
  break;
2416
2392
  }
2417
2393
  if (start === -1) return text2;
2394
+ while (end > start + 1 && (lines[end - 1].trim() === "" || lines[end - 1].trim().startsWith("#"))) {
2395
+ end--;
2396
+ }
2418
2397
  if (options?.includePrecedingComments) {
2419
2398
  while (start > 0 && lines[start - 1].trim().startsWith("#")) {
2420
2399
  start--;
@@ -2456,60 +2435,108 @@ function stripTomlStringsAndComments(line) {
2456
2435
  }
2457
2436
  function isManagedHookEntry(value) {
2458
2437
  const trimmed = value.trim();
2459
- return trimmed === "op inject -i .env.op > .env" || /(^|\/)link-agentfiles\.sh$/.test(trimmed);
2438
+ if (trimmed === OP_INJECT_SCRIPT) return true;
2439
+ return /link-agentfiles\.sh'?\s*$/.test(trimmed);
2460
2440
  }
2461
- function renderHookEntries(entries, indent = "") {
2462
- return [
2463
- `${indent}enter = [`,
2464
- ...entries.map((entry) => `${indent} ${JSON.stringify(entry)},`),
2465
- `${indent}]`
2466
- ];
2441
+ function normalizeHookScript(script) {
2442
+ const trimmed = script.trim();
2443
+ if (/codegraph\.sh/.test(trimmed)) return CODEGRAPH_SCRIPT;
2444
+ return trimmed;
2467
2445
  }
2468
- function upsertLinkAgentfilesHooks(text2) {
2469
- const lines = text2.split("\n");
2470
- const hooksStart = lines.findIndex((line) => /^\[hooks\]$/.test(line.trim()));
2471
- if (hooksStart === -1) return insertTomlBlockBeforeVersioning(text2, LINK_AGENTFILES_HOOKS_BLOCK);
2472
- let hooksEnd = lines.length;
2473
- for (let i = hooksStart + 1; i < lines.length; i++) {
2474
- if (/^\[[^\]]+\]/.test(lines[i].trim())) {
2475
- hooksEnd = i;
2476
- break;
2446
+ function tomlValueSpanEnd(lines, start, limit) {
2447
+ let depth = 0;
2448
+ let j = start;
2449
+ for (; j < limit; j++) {
2450
+ for (const ch of stripTomlStringsAndComments(lines[j])) {
2451
+ if (ch === "[") depth++;
2452
+ else if (ch === "]") depth--;
2477
2453
  }
2454
+ if (depth <= 0) break;
2478
2455
  }
2479
- let enterStart = -1;
2480
- let enterEnd = -1;
2481
- for (let i = hooksStart + 1; i < hooksEnd; i++) {
2482
- if (!/^\s*enter\s*=/.test(lines[i])) continue;
2483
- enterStart = i;
2484
- let depth = 0;
2485
- let j = i;
2486
- for (; j < hooksEnd; j++) {
2487
- for (const ch of stripTomlStringsAndComments(lines[j])) {
2488
- if (ch === "[") depth++;
2489
- else if (ch === "]") depth--;
2456
+ return Math.min(j, limit - 1) + 1;
2457
+ }
2458
+ function stripHookBlocks(text2) {
2459
+ const lines = text2.split("\n");
2460
+ const enter = [];
2461
+ const leave = [];
2462
+ const drop = new Array(lines.length).fill(false);
2463
+ const isHeader = (line) => /^\[/.test(line.trim());
2464
+ const dropPrecedingComments = (idx) => {
2465
+ for (let k = idx - 1; k >= 0 && !drop[k] && lines[k].trim().startsWith("#"); k--) drop[k] = true;
2466
+ };
2467
+ for (let i = 0; i < lines.length; i++) {
2468
+ const trimmed = lines[i].trim();
2469
+ const tableMatch = /^\[\[\s*hooks\.(enter|leave)\s*\]\]$/.exec(trimmed);
2470
+ if (tableMatch) {
2471
+ const bucket = tableMatch[1] === "enter" ? enter : leave;
2472
+ dropPrecedingComments(i);
2473
+ drop[i] = true;
2474
+ let j = i + 1;
2475
+ for (; j < lines.length; j++) {
2476
+ const body = lines[j].trim();
2477
+ if (body === "" || body.startsWith("#") || isHeader(lines[j])) break;
2478
+ drop[j] = true;
2479
+ const scriptMatch = /^\s*script\s*=\s*(.+)$/.exec(lines[j]);
2480
+ if (scriptMatch) {
2481
+ const value = extractTomlStrings(scriptMatch[1])[0];
2482
+ if (value !== void 0) bucket.push(value);
2483
+ }
2490
2484
  }
2491
- if (depth <= 0) break;
2485
+ i = j - 1;
2486
+ continue;
2487
+ }
2488
+ if (trimmed === "[hooks]") {
2489
+ dropPrecedingComments(i);
2490
+ let j = i + 1;
2491
+ let lastDrop = i;
2492
+ while (j < lines.length && !isHeader(lines[j])) {
2493
+ const keyMatch = /^\s*(enter|leave)\s*=/.exec(lines[j]);
2494
+ if (keyMatch) {
2495
+ const bucket = keyMatch[1] === "enter" ? enter : leave;
2496
+ const end = tomlValueSpanEnd(lines, j, lines.length);
2497
+ for (const value of extractTomlStrings(lines.slice(j, end).join("\n"))) bucket.push(value);
2498
+ lastDrop = end - 1;
2499
+ j = end;
2500
+ } else if (/^\s*\]\s*$/.test(lines[j])) {
2501
+ lastDrop = j;
2502
+ j++;
2503
+ } else {
2504
+ j++;
2505
+ }
2506
+ }
2507
+ for (let k = i; k <= lastDrop; k++) drop[k] = true;
2508
+ i = j - 1;
2509
+ continue;
2492
2510
  }
2493
- enterEnd = Math.min(j, hooksEnd - 1) + 1;
2494
- while (enterEnd < hooksEnd && /^\s*\]\s*$/.test(lines[enterEnd])) enterEnd++;
2495
- break;
2496
- }
2497
- const existingBlock = enterStart >= 0 ? lines.slice(enterStart, enterEnd).join("\n") : "";
2498
- const preserved = extractTomlStrings(existingBlock).filter((entry) => !isManagedHookEntry(entry));
2499
- const merged = [...LINK_AGENTFILES_HOOK_ENTRIES];
2500
- for (const entry of preserved) {
2501
- if (!merged.includes(entry)) merged.push(entry);
2502
2511
  }
2503
- const indent = enterStart >= 0 ? lines[enterStart].match(/^\s*/)?.[0] ?? "" : "";
2504
- const rendered = renderHookEntries(merged, indent);
2505
- if (enterStart >= 0) {
2506
- return lines.slice(0, enterStart).concat(rendered, lines.slice(enterEnd)).join("\n").replace(/\n{3,}/g, "\n\n").replace(/\n+$/, "\n");
2512
+ const kept = lines.filter((_, idx) => !drop[idx]).join("\n").replace(/\n{3,}/g, "\n\n").replace(/\n+$/, "\n");
2513
+ return { text: kept, enter, leave };
2514
+ }
2515
+ function renderHookTables(scripts, kind) {
2516
+ return scripts.map((script) => `[[hooks.${kind}]]
2517
+ script = ${JSON.stringify(script)}`);
2518
+ }
2519
+ function dedupePreserve(scripts) {
2520
+ const out = [];
2521
+ for (const script of scripts) {
2522
+ if (script && !out.includes(script)) out.push(script);
2507
2523
  }
2508
- return lines.slice(0, hooksStart + 1).concat(rendered, lines.slice(hooksStart + 1)).join("\n").replace(/\n{3,}/g, "\n\n").replace(/\n+$/, "\n");
2524
+ return out;
2525
+ }
2526
+ function upsertLinkAgentfilesHooks(text2) {
2527
+ const { text: stripped, enter, leave } = stripHookBlocks(text2);
2528
+ const preservedEnter = enter.map(normalizeHookScript).filter((script) => !isManagedHookEntry(script));
2529
+ const enterScripts = dedupePreserve([...LINK_AGENTFILES_HOOK_ENTRIES, ...preservedEnter]);
2530
+ const leaveScripts = dedupePreserve(leave.map(normalizeHookScript));
2531
+ const block = [
2532
+ HOOKS_COMMENT_HEADER,
2533
+ ...renderHookTables(enterScripts, "enter"),
2534
+ ...renderHookTables(leaveScripts, "leave")
2535
+ ].join("\n");
2536
+ return insertTomlBlockBeforeVersioning(stripped, block);
2509
2537
  }
2510
2538
  function upsertLinkAgentfilesBlock(text2, ctx) {
2511
2539
  const withPath = upsertMisePath(text2, requiredMisePathEntries(ctx));
2512
- if (withPath.includes(LINK_AGENTFILES_BLOCK)) return withPath;
2513
2540
  let cleaned = removeTomlSection(withPath, /^\[tasks\.link-agentfiles\]$/, /link-agentfiles/, { includePrecedingComments: false });
2514
2541
  cleaned = removeTomlSection(cleaned, /^\[\[watch_files\]\]$/, /AGENTS\.md/, { includePrecedingComments: false });
2515
2542
  cleaned = upsertLinkAgentfilesHooks(cleaned);
@@ -3009,8 +3036,8 @@ var RULES = [
3009
3036
  const pathValues = [...(text2.match(/^_\.path\s*=\s*\[([^\]]*)\]/m)?.[1] ?? "").matchAll(/"([^"]+)"/g)].map((match) => match[1]);
3010
3037
  const missingPathValues = requiredMisePathEntries(ctx).filter((value) => !pathValues.includes(value));
3011
3038
  if (missingPathValues.length) details.push(`[env]._.path should include ${missingPathValues.join(", ")}`);
3012
- if (!text2.includes('"{{config_root}}/.mise/scripts/link-agentfiles.sh"')) details.push("link-agentfiles must use raw {{config_root}} guard");
3013
- if (!text2.includes("op inject -i .env.op > .env")) details.push("[hooks].enter must materialize .env from .env.op");
3039
+ if (!text2.includes("'{{config_root}}/.mise/scripts/link-agentfiles.sh'")) details.push("link-agentfiles hook must use single-quoted {{config_root}} guard");
3040
+ if (!text2.includes("op inject -i .env.op > .env")) details.push("hooks.enter must materialize .env from .env.op");
3014
3041
  if (!text2.includes('patterns = ["AGENTS.md"]')) details.push("watch_files must monitor AGENTS.md");
3015
3042
  if (!text2.includes('task = "link-agentfiles"')) details.push("watch_files must dispatch link-agentfiles task");
3016
3043
  return {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@delorenj/pjangler",
3
- "version": "1.2.16",
3
+ "version": "1.2.18",
4
4
  "description": "Project subsystem bootstrapper CLI",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -10,7 +10,7 @@ CODEX_HOME = { value = ".codex", path = true }
10
10
  # i.e. All linked agent files MUST be siblings at
11
11
  # any given level of nesting.
12
12
  [hooks]
13
- enter = '{{config_root}}/.mise/scripts/link-agentfiles.sh'
13
+ enter = "'{{config_root}}/.mise/scripts/link-agentfiles.sh'"
14
14
 
15
15
  [[watch_files]]
16
16
  patterns = ["AGENTS.md"]
@@ -11,21 +11,27 @@ _.file = [".env"]
11
11
  # TODO: Ensure this works for all levels of nesting.
12
12
  # i.e. All linked agent files MUST be siblings at
13
13
  # any given level of nesting.
14
- [hooks]
15
- enter = [
16
- "{% raw %}{{config_root}}{% endraw %}/.mise/scripts/link-agentfiles.sh",
17
- "op inject -i .env.op > .env",
18
- "[ -f {% raw %}{{config_root}}{% endraw %}/.mise/scripts/codegraph.sh ] && {% raw %}{{config_root}}{% endraw %}/.mise/scripts/codegraph.sh || true",
14
+ #
15
+ # NOTE: hooks are an array of [[hooks.enter]] tables (NOT enter = [ ... ]) and
16
+ # every {{config_root}} path is single-quoted so it survives a space in the
17
+ # project path mise runs each `script` via `sh -c`.
18
+ [[hooks.enter]]
19
+ script = "'{% raw %}{{config_root}}{% endraw %}/.mise/scripts/link-agentfiles.sh'"
20
+ [[hooks.enter]]
21
+ script = "op inject -i .env.op > .env"
22
+ [[hooks.enter]]
23
+ script = "[ -f '{% raw %}{{config_root}}{% endraw %}/.mise/scripts/codegraph.sh' ] && '{% raw %}{{config_root}}{% endraw %}/.mise/scripts/codegraph.sh' || true"
19
24
  {%- if agent_hooks_layer %}
20
- "{% raw %}{{config_root}}{% endraw %}/.mise/scripts/link-project-skills-to-clis.sh",
21
- "{% raw %}{{config_root}}{% endraw %}/.agents/hooks/sync.py --install --quiet",
25
+ [[hooks.enter]]
26
+ script = "'{% raw %}{{config_root}}{% endraw %}/.mise/scripts/link-project-skills-to-clis.sh'"
27
+ [[hooks.enter]]
28
+ script = "'{% raw %}{{config_root}}{% endraw %}/.agents/hooks/sync.py' --install --quiet"
22
29
  {%- endif %}
23
- ]
24
30
  {%- if agent_hooks_layer %}
25
- leave = [
26
- "{% raw %}{{config_root}}{% endraw %}/.mise/scripts/unlink-project-skills-from-clis.sh",
27
- "{% raw %}{{config_root}}{% endraw %}/.agents/hooks/sync.py --uninstall --quiet",
28
- ]
31
+ [[hooks.leave]]
32
+ script = "'{% raw %}{{config_root}}{% endraw %}/.mise/scripts/unlink-project-skills-from-clis.sh'"
33
+ [[hooks.leave]]
34
+ script = "'{% raw %}{{config_root}}{% endraw %}/.agents/hooks/sync.py' --uninstall --quiet"
29
35
  {%- endif %}
30
36
 
31
37
  [[watch_files]]
@@ -39,34 +45,34 @@ task = "hooks-sync"
39
45
  {% endif %}
40
46
  [tasks.link-agentfiles]
41
47
  description = "Symlink all agent files to AGENTS.md"
42
- run = "{% raw %}{{config_root}}{% endraw %}/.mise/scripts/link-agentfiles.sh"
48
+ run = "'{% raw %}{{config_root}}{% endraw %}/.mise/scripts/link-agentfiles.sh'"
43
49
  {% if agent_hooks_layer %}
44
50
  # --- Project-scoped agent hooks + skill fan-out (see .agents/hooks/README.md) ---
45
51
 
46
52
  [tasks.hooks-sync]
47
53
  description = "Fan out .agents/hooks/hooks.master.json to each agent CLI (claude committed; codex/kimi injected; hermes adapter)"
48
- run = "{% raw %}{{config_root}}{% endraw %}/.agents/hooks/sync.py --install"
54
+ run = "'{% raw %}{{config_root}}{% endraw %}/.agents/hooks/sync.py' --install"
49
55
 
50
56
  [tasks.hooks-check]
51
57
  description = "Drift gate: verify generated hook configs match hooks.master.json (CI-safe, read-only)"
52
- run = "{% raw %}{{config_root}}{% endraw %}/.agents/hooks/sync.py --check"
58
+ run = "'{% raw %}{{config_root}}{% endraw %}/.agents/hooks/sync.py' --check"
53
59
 
54
60
  [tasks.hooks-uninstall]
55
61
  description = "Remove per-user agent-hook injections (codex/kimi/hermes)"
56
- run = "{% raw %}{{config_root}}{% endraw %}/.agents/hooks/sync.py --uninstall"
62
+ run = "'{% raw %}{{config_root}}{% endraw %}/.agents/hooks/sync.py' --uninstall"
57
63
 
58
64
  [tasks.link-project-skills-to-clis]
59
65
  description = "Fan .agents/skills out to each agent CLI; honors local.json defer_to_global + disabled"
60
- run = "{% raw %}{{config_root}}{% endraw %}/.mise/scripts/link-project-skills-to-clis.sh"
66
+ run = "'{% raw %}{{config_root}}{% endraw %}/.mise/scripts/link-project-skills-to-clis.sh'"
61
67
 
62
68
  [tasks.unlink-project-skills-from-clis]
63
69
  description = "Remove project skill symlinks from shared per-CLI dirs"
64
- run = "{% raw %}{{config_root}}{% endraw %}/.mise/scripts/unlink-project-skills-from-clis.sh"
70
+ run = "'{% raw %}{{config_root}}{% endraw %}/.mise/scripts/unlink-project-skills-from-clis.sh'"
65
71
 
66
72
  [tasks.skills-relink]
67
73
  description = "Re-fan the project skill set to all CLIs"
68
- run = "{% raw %}{{config_root}}{% endraw %}/.mise/scripts/link-project-skills-to-clis.sh"
74
+ run = "'{% raw %}{{config_root}}{% endraw %}/.mise/scripts/link-project-skills-to-clis.sh'"
69
75
  {% endif %}
70
76
  [tasks.hindsight-setup]
71
77
  description = "Provision this dev's shared project Hindsight key from 1Password into .env (op inject)"
72
- run = "{% raw %}{{config_root}}{% endraw %}/.mise/scripts/hindsight-setup.sh"
78
+ run = "'{% raw %}{{config_root}}{% endraw %}/.mise/scripts/hindsight-setup.sh'"
@@ -73,3 +73,35 @@ The loop never ends a pass with work parked waiting on the operator.
73
73
  prerequisite of the dependent and record the rollback.
74
74
 
75
75
  Board status is not proof. Repository evidence and the close gate are proof.
76
+
77
+ ## Post-loop improvement (end-of-batch retro)
78
+
79
+ When the board-clearing loop reaches a resting state — no ready or actionable
80
+ ticket to move and no live worker to monitor (a stop condition above, not a
81
+ mid-flight pause) — the sentinel runs a short end-of-batch retro before the pass
82
+ ends. Because it fires only at rest, it never competes with live ticket flow
83
+ (anti-stall).
84
+
85
+ Answer three questions about the batch just cleared:
86
+
87
+ 1. What hurt this batch?
88
+ 2. What should change?
89
+ 3. Is the fix repo-local or external/template/fleet?
90
+
91
+ ALWAYS record the reflection as a run artifact under
92
+ `_bmad-output/implementation-artifacts/`: the three answers, any repo-local fix
93
+ applied, and a clear description of each proposed external/template/fleet
94
+ improvement. Nothing is silently dropped — the artifact is the durable record.
95
+
96
+ Then act on the answer — do not just note it:
97
+
98
+ - Repo-local, small, safe, and within this pass's scope → implement it now as a
99
+ minor follow-up. If it is larger or riskier, do not start it here; treat it as
100
+ external and surface it below.
101
+ - External / template / fleet (hermes-agent template, fleet config, another repo,
102
+ or an undecided product/architecture decision) → the adapter cannot open issues,
103
+ so surface it loudly instead: post a `tp comment` on the current tracking/active
104
+ issue proposing the follow-up, and flag the operator (the human-facing control
105
+ plane) to file it. Do not attempt to create a ticket through the adapter.
106
+
107
+ The retro is evidence like any other pass output.
@@ -96,5 +96,26 @@ on Plane or Trello.
96
96
  `last_activity_at`, `log_path`).
97
97
  10. Run or follow the session-end ritual; report board status, issues touched,
98
98
  evidence touched, and the active worker issue or blocker.
99
+ 11. **Post-loop improvement (end-of-batch retro).** ONLY when the board-clearing
100
+ loop reaches a resting state this pass — no ready/actionable ticket to move and
101
+ no live worker to monitor (idle, or only backlog / external-blocked work
102
+ remains). This is a retro AT REST: it never runs while a ready ticket or a
103
+ healthy worker exists, and it must not block or delay live ticket flow
104
+ (anti-stall, step 8). Answer three questions about the batch just cleared:
105
+ (1) What hurt this batch? (2) What should change? (3) Is the fix repo-local or
106
+ external/template/fleet? ALWAYS record the reflection as a run artifact
107
+ (evidence) under `_bmad-output/implementation-artifacts/`: the three answers,
108
+ any repo-local fix applied, and a clear description of each proposed
109
+ external/template/fleet improvement. Nothing is silently dropped — the artifact
110
+ is the durable record. Then act on the answer autonomously:
111
+ - **Repo-local** and small, safe, and within this pass's scope: implement it now
112
+ as a minor follow-up inside **{{ target_repo }}**. If it is larger or riskier
113
+ than that, do NOT start it here — treat it as external and surface it.
114
+ - **External / template / fleet** (hermes-agent template, fleet config, another
115
+ repo, or an undecided product/architecture decision): the adapter cannot open
116
+ issues, so surface it loudly instead — post a `tp comment` on the current
117
+ tracking/active issue proposing the follow-up, and flag the operator (the
118
+ human-facing control plane) to file it. Do not attempt to create a ticket
119
+ through the adapter.
99
120
 
100
121
  Do not rely on a hard-coded seed ticket. Query the board every pass.