@delorenj/pjangler 1.4.2 → 1.4.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/dist/index.js +3336 -1359
  2. package/dist/index.js.map +7 -0
  3. package/dist/mcp-server.js +1520 -838
  4. package/dist/mcp-server.js.map +7 -0
  5. package/dist/prompt.js +2 -1
  6. package/dist/prompt.js.map +7 -0
  7. package/package.json +8 -4
  8. package/templates/hermes-agent/copier.yml +16 -3
  9. package/templates/hermes-agent/template/.runtime-scaffold/memories/MEMORY.md +7 -4
  10. package/templates/hermes-agent/template/.scripts/10-hermes-profile.sh +88 -94
  11. package/templates/hermes-agent/template/.scripts/20-runtime-repo.sh +44 -21
  12. package/templates/hermes-agent/template/.scripts/30-telegram.sh +182 -171
  13. package/templates/hermes-agent/template/.scripts/31-slack.sh +260 -165
  14. package/templates/hermes-agent/template/.scripts/40-plane.sh +45 -36
  15. package/templates/hermes-agent/template/.scripts/42-ticket-provider.sh +210 -41
  16. package/templates/hermes-agent/template/.scripts/70-systemd.sh +129 -15
  17. package/templates/hermes-agent/template/.scripts/80-registry.sh +42 -6
  18. package/templates/hermes-agent/template/.scripts/99-summary.sh +69 -16
  19. package/templates/hermes-agent/template/.scripts/_lib.sh +773 -0
  20. package/templates/hermes-agent/template/.scripts/channel-transaction.py +2340 -0
  21. package/templates/hermes-agent/template/.scripts/config.example.toml +8 -2
  22. package/templates/hermes-agent/template/.scripts/credential-launch.sh +5 -1
  23. package/templates/hermes-agent/template/.scripts/heartbeat.sh +2 -3
  24. package/templates/hermes-agent/template/.scripts/lib/profile-config-lock.py +182 -0
  25. package/templates/hermes-agent/template/.scripts/lib/profile-config-seed.py +108 -0
  26. package/templates/hermes-agent/template/.scripts/lib/ticket-provider.sh +9 -0
  27. package/templates/hermes-agent/template/.scripts/lib/voice-config.py +546 -0
  28. package/templates/hermes-agent/template/.scripts/providers/linear.sh +24 -1
  29. package/templates/hermes-agent/template/.scripts/providers/plane.sh +54 -8
  30. package/templates/hermes-agent/template/.scripts/providers/trello.sh +25 -2
  31. package/templates/hermes-agent/template/.scripts/sentinel/bin/issue-autonomous-review.sh +5 -16
  32. package/templates/hermes-agent/template/.scripts/sentinel/bin/issue-close-gate.sh +2 -16
  33. package/templates/hermes-agent/template/.scripts/sentinel/docs/autonomous-delegated-review.md +13 -19
  34. package/templates/hermes-agent/template/.scripts/sentinel/docs/bloodbank-events.md +29 -36
  35. package/templates/hermes-agent/template/.scripts/sentinel.prompt.md.jinja +7 -8
  36. package/templates/hermes-agent/template/.scripts/store-onepassword-secret.py +260 -0
  37. package/templates/hermes-agent/template/SOUL.md.jinja +14 -16
  38. package/templates/hermes-agent/template/hermes.jinja +1 -1
  39. package/templates/hermes-agent/template/role.yaml.jinja +11 -4
@@ -193,17 +193,17 @@ var init_types = __esm({
193
193
 
194
194
  // src/utils/tree-diff.ts
195
195
  import { createHash as createHash3 } from "node:crypto";
196
- import { existsSync as existsSync4, lstatSync as lstatSync3, readFileSync as readFileSync4, readdirSync as readdirSync3, readlinkSync as readlinkSync2 } from "node:fs";
196
+ import { existsSync as existsSync4, lstatSync as lstatSync4, readFileSync as readFileSync5, readdirSync as readdirSync3, readlinkSync as readlinkSync2 } from "node:fs";
197
197
  import { join as join6, relative as relative3 } from "node:path";
198
198
  function snapshotTree(root, current = root, snapshot = /* @__PURE__ */ new Map()) {
199
199
  if (!existsSync4(current)) return snapshot;
200
200
  const rel = relative3(root, current) || ".";
201
201
  if (rel === ".git" || rel.startsWith(`.git${process.platform === "win32" ? "\\" : "/"}`)) return snapshot;
202
- const stat = lstatSync3(current);
202
+ const stat = lstatSync4(current);
203
203
  if (stat.isSymbolicLink()) {
204
204
  snapshot.set(rel, `link:${readlinkSync2(current)}`);
205
205
  } else if (stat.isFile()) {
206
- snapshot.set(rel, `file:${createHash3("sha256").update(readFileSync4(current)).digest("hex")}:${stat.mode & 511}`);
206
+ snapshot.set(rel, `file:${createHash3("sha256").update(readFileSync5(current)).digest("hex")}:${stat.mode & 511}`);
207
207
  } else if (stat.isDirectory()) {
208
208
  snapshot.set(rel, `dir:${stat.mode & 511}`);
209
209
  for (const name of readdirSync3(current)) snapshotTree(root, join6(current, name), snapshot);
@@ -227,14 +227,14 @@ import {
227
227
  accessSync,
228
228
  constants as constants2,
229
229
  existsSync as existsSync5,
230
- lstatSync as lstatSync4,
231
- readFileSync as readFileSync5,
230
+ lstatSync as lstatSync5,
231
+ readFileSync as readFileSync6,
232
232
  readdirSync as readdirSync4,
233
- realpathSync as realpathSync2,
233
+ realpathSync as realpathSync3,
234
234
  statSync
235
235
  } from "node:fs";
236
236
  import { tmpdir, userInfo } from "node:os";
237
- import { basename as basename4, delimiter, isAbsolute, join as join7, relative as relative4, resolve as resolve4 } from "node:path";
237
+ import { basename as basename5, delimiter, isAbsolute, join as join7, relative as relative4, resolve as resolve4 } from "node:path";
238
238
  import YAML2 from "yaml";
239
239
  function containedBy(parent, candidate) {
240
240
  const rel = relative4(resolve4(parent), resolve4(candidate));
@@ -246,7 +246,7 @@ function firstExecutableOnPath(env2) {
246
246
  const candidate = resolve4(entry, process.platform === "win32" ? "copier.exe" : "copier");
247
247
  try {
248
248
  accessSync(candidate, constants2.X_OK);
249
- const stat = lstatSync4(candidate);
249
+ const stat = lstatSync5(candidate);
250
250
  if (stat.isFile() || stat.isSymbolicLink()) return candidate;
251
251
  } catch {
252
252
  }
@@ -254,11 +254,11 @@ function firstExecutableOnPath(env2) {
254
254
  return void 0;
255
255
  }
256
256
  function sha2562(path) {
257
- return createHash4("sha256").update(readFileSync5(path)).digest("base64url");
257
+ return createHash4("sha256").update(readFileSync6(path)).digest("base64url");
258
258
  }
259
259
  function fingerprint(path) {
260
260
  const absolute = resolve4(path);
261
- const realPath = realpathSync2(absolute);
261
+ const realPath = realpathSync3(absolute);
262
262
  const stat = statSync(realPath);
263
263
  if (!stat.isFile()) throw new Error(`${absolute} is not a regular file`);
264
264
  return {
@@ -292,7 +292,7 @@ function sameFingerprint(expected) {
292
292
  function consoleScriptContract(path) {
293
293
  let text2;
294
294
  try {
295
- text2 = readFileSync5(path, "utf8").slice(0, 32 * 1024);
295
+ text2 = readFileSync6(path, "utf8").slice(0, 32 * 1024);
296
296
  } catch (error) {
297
297
  return { ok: false, error: `cannot read Copier launcher: ${error instanceof Error ? error.message : String(error)}` };
298
298
  }
@@ -303,7 +303,7 @@ function consoleScriptContract(path) {
303
303
  return { ok: false, error: "Copier launcher must use one absolute Python interpreter" };
304
304
  }
305
305
  const interpreterPath = resolve4(shebang[0]);
306
- const interpreter = basename4(interpreterPath);
306
+ const interpreter = basename5(interpreterPath);
307
307
  if (!/^python(?:\d+(?:\.\d+)*)?$/.test(interpreter)) {
308
308
  return { ok: false, error: "Copier launcher is not an absolute Python console script" };
309
309
  }
@@ -327,7 +327,7 @@ function locateUvSitePackages(toolRoot) {
327
327
  if (existsSync5(sitePackages)) candidates.push(sitePackages);
328
328
  }
329
329
  if (candidates.length !== 1) throw new Error(`expected one UV Copier site-packages directory, found ${candidates.length}`);
330
- return realpathSync2(candidates[0]);
330
+ return realpathSync3(candidates[0]);
331
331
  }
332
332
  function parseRecordLine(line) {
333
333
  if (!line || line.includes('"')) return void 0;
@@ -356,11 +356,11 @@ function attestUvCopier(candidate, realCandidate, home) {
356
356
  if (!launcher.ok || !launcher.interpreter) {
357
357
  return { ...launcher, executable: realCandidate, realExecutable: realCandidate };
358
358
  }
359
- const expectedInterpreter = join7(toolRoot, "bin", basename4(launcher.interpreter));
359
+ const expectedInterpreter = join7(toolRoot, "bin", basename5(launcher.interpreter));
360
360
  if (resolve4(launcher.interpreter) !== resolve4(expectedInterpreter)) {
361
361
  return { ok: false, error: "UV Copier launcher interpreter is outside the attested tool environment" };
362
362
  }
363
- const interpreterReal = realpathSync2(launcher.interpreter);
363
+ const interpreterReal = realpathSync3(launcher.interpreter);
364
364
  const uvPythonRoots = [
365
365
  join7(home, ".local", "share", "uv", "python"),
366
366
  join7(home, "Library", "Application Support", "uv", "python")
@@ -369,7 +369,7 @@ function attestUvCopier(candidate, realCandidate, home) {
369
369
  return { ok: false, error: "UV Copier interpreter is not managed by the canonical UV Python installation" };
370
370
  }
371
371
  const receiptPath = join7(toolRoot, "uv-receipt.toml");
372
- const receipt = readFileSync5(receiptPath, "utf8");
372
+ const receipt = readFileSync6(receiptPath, "utf8");
373
373
  if (!/requirements\s*=\s*\[[\s\S]*?name\s*=\s*["']copier["']/.test(receipt) || !/entrypoints\s*=\s*\[[\s\S]*?name\s*=\s*["']copier["'][\s\S]*?from\s*=\s*["']copier["']/.test(receipt)) {
374
374
  return { ok: false, error: "UV tool receipt does not bind the copier entry point to the Copier package" };
375
375
  }
@@ -378,24 +378,24 @@ function attestUvCopier(candidate, realCandidate, home) {
378
378
  if (distInfos.length !== 1) {
379
379
  return { ok: false, error: `expected one installed Copier distribution, found ${distInfos.length}` };
380
380
  }
381
- const distInfo = realpathSync2(distInfos[0]);
381
+ const distInfo = realpathSync3(distInfos[0]);
382
382
  const metadataPath = join7(distInfo, "METADATA");
383
383
  const entryPointsPath = join7(distInfo, "entry_points.txt");
384
384
  const recordPath = join7(distInfo, "RECORD");
385
- const metadata = readFileSync5(metadataPath, "utf8");
385
+ const metadata = readFileSync6(metadataPath, "utf8");
386
386
  const name = metadata.match(/^Name:\s*(.+)$/mi)?.[1]?.trim();
387
387
  const version = metadata.match(/^Version:\s*(.+)$/mi)?.[1]?.trim();
388
388
  if (name?.toLowerCase() !== "copier" || !version || !/^9(?:\.|$)/.test(version)) {
389
389
  return { ok: false, error: "installed distribution is not a Copier 9 package" };
390
390
  }
391
- const entryPoints = readFileSync5(entryPointsPath, "utf8");
391
+ const entryPoints = readFileSync6(entryPointsPath, "utf8");
392
392
  if (!/^copier\s*=\s*copier\.__main__:CopierApp\.run\s*$/m.test(entryPoints)) {
393
393
  return { ok: false, error: "installed Copier distribution has an unexpected console entry point" };
394
394
  }
395
- const recordEntries = readFileSync5(recordPath, "utf8").split(/\r?\n/).map(parseRecordLine).filter((entry) => Boolean(entry));
395
+ const recordEntries = readFileSync6(recordPath, "utf8").split(/\r?\n/).map(parseRecordLine).filter((entry) => Boolean(entry));
396
396
  const selected = recordEntries.filter((entry) => {
397
397
  const normalized = entry.relativePath.replaceAll("\\", "/");
398
- return normalized.startsWith("copier/") || normalized === `${basename4(distInfo)}/METADATA` || normalized === `${basename4(distInfo)}/entry_points.txt` || normalized === "../../../bin/copier";
398
+ return normalized.startsWith("copier/") || normalized === `${basename5(distInfo)}/METADATA` || normalized === `${basename5(distInfo)}/entry_points.txt` || normalized === "../../../bin/copier";
399
399
  });
400
400
  if (!selected.some((entry) => entry.relativePath === "../../../bin/copier") || !selected.some((entry) => entry.relativePath.replaceAll("\\", "/") === "copier/__main__.py")) {
401
401
  return { ok: false, error: "Copier RECORD does not bind its launcher and package entry point" };
@@ -456,7 +456,7 @@ function preflightTrustedCopier(options) {
456
456
  if (!candidate) return { ok: false, error: "copier not found on PATH" };
457
457
  let realCandidate;
458
458
  try {
459
- realCandidate = realpathSync2(candidate);
459
+ realCandidate = realpathSync3(candidate);
460
460
  } catch (error) {
461
461
  return { ok: false, error: `cannot resolve Copier launcher: ${error instanceof Error ? error.message : String(error)}` };
462
462
  }
@@ -469,10 +469,10 @@ function preflightTrustedCopier(options) {
469
469
  }
470
470
  function regularContainedFile(root, path, label) {
471
471
  try {
472
- const rootReal = realpathSync2(root);
473
- const fileReal = realpathSync2(path);
472
+ const rootReal = realpathSync3(root);
473
+ const fileReal = realpathSync3(path);
474
474
  if (!containedBy(rootReal, fileReal)) return { ok: false, error: `${label} escapes its vendored template root` };
475
- if (!lstatSync4(path).isFile()) return { ok: false, error: `${label} is not a regular file` };
475
+ if (!lstatSync5(path).isFile()) return { ok: false, error: `${label} is not a regular file` };
476
476
  return { ok: true };
477
477
  } catch (error) {
478
478
  return { ok: false, error: `${label} is unavailable: ${error instanceof Error ? error.message : String(error)}` };
@@ -483,7 +483,7 @@ function parseCopierConfig(templateRoot, label) {
483
483
  const file = regularContainedFile(templateRoot, configPath, `${label} copier.yml`);
484
484
  if (!file.ok) return { result: file };
485
485
  try {
486
- const parsed = YAML2.parse(readFileSync5(configPath, "utf8"));
486
+ const parsed = YAML2.parse(readFileSync6(configPath, "utf8"));
487
487
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
488
488
  return { result: { ok: false, error: `${label} copier.yml must contain a mapping` } };
489
489
  }
@@ -518,7 +518,7 @@ function preflightCommonProjectTemplate(pjanglerRoot) {
518
518
  "template/mise.toml.jinja"
519
519
  ], "CommonProject template");
520
520
  if (!files.ok) return files;
521
- const projectJson = readFileSync5(join7(templateRoot, "template", ".project.json.jinja"), "utf8");
521
+ const projectJson = readFileSync6(join7(templateRoot, "template", ".project.json.jinja"), "utf8");
522
522
  for (const key of ["project_name", "project_slug", "repo_path", "ticket_provider", "agents"]) {
523
523
  if (!projectJson.includes(`"${key}"`)) return { ok: false, error: `CommonProject projection is missing ${key}` };
524
524
  }
@@ -529,7 +529,7 @@ function preflightHermesTemplate(pjanglerRoot, env2 = process.env) {
529
529
  const explicit = env2.PJANGLER_HERMES_TEMPLATE?.trim();
530
530
  if (explicit) {
531
531
  try {
532
- if (realpathSync2(resolve4(explicit)) !== realpathSync2(templateRoot)) {
532
+ if (realpathSync3(resolve4(explicit)) !== realpathSync3(templateRoot)) {
533
533
  return { ok: false, error: "MCP Hermes apply requires the version-locked vendored template" };
534
534
  }
535
535
  } catch (error) {
@@ -547,27 +547,29 @@ function preflightHermesTemplate(pjanglerRoot, env2 = process.env) {
547
547
  "template/.scripts/05-fleet-env.sh",
548
548
  "template/.scripts/10-hermes-profile.sh",
549
549
  "template/.scripts/20-runtime-repo.sh",
550
+ "template/.scripts/30-telegram.sh",
551
+ "template/.scripts/31-slack.sh",
550
552
  "template/.scripts/42-ticket-provider.sh",
551
553
  "template/.scripts/70-systemd.sh",
552
554
  "template/.scripts/80-registry.sh"
553
555
  ], "Hermes template");
554
556
  if (!required.ok) return required;
555
- const role = readFileSync5(join7(templateRoot, "template", "role.yaml.jinja"), "utf8");
557
+ const role = readFileSync6(join7(templateRoot, "template", "role.yaml.jinja"), "utf8");
556
558
  if (!/^bloodbank:\s*$[\s\S]*?^\s+enabled:\s+(?:true|false)\s*$/m.test(role)) {
557
559
  return { ok: false, error: "Hermes role projection must declare bloodbank.enabled as a strict boolean" };
558
560
  }
559
- const library = readFileSync5(join7(templateRoot, "template", ".scripts", "_lib.sh"), "utf8");
561
+ const library = readFileSync6(join7(templateRoot, "template", ".scripts", "_lib.sh"), "utf8");
560
562
  if (!library.includes("PJANGLER_PROJECT_ROOT") || !library.includes('"$explicit"/agents/hermes/*')) {
561
563
  return { ok: false, error: "Hermes project-root resolver must honor the explicitly contained MCP target" };
562
564
  }
563
- const skipPlane = readFileSync5(join7(templateRoot, "template", ".scripts", "42-ticket-provider.sh"), "utf8");
565
+ const skipPlane = readFileSync6(join7(templateRoot, "template", ".scripts", "42-ticket-provider.sh"), "utf8");
564
566
  const guard = skipPlane.indexOf('if [[ "${SKIP_PLANE:-0}" == "1" ]]');
565
567
  const firstSource = skipPlane.search(/^source\s/m);
566
568
  if (guard < 0 || firstSource < 0 || guard > firstSource) {
567
569
  return { ok: false, error: "Hermes ticket-provider skip guard must precede all sourced provider/config logic" };
568
570
  }
569
- for (const script of ["01-config.sh", "05-fleet-env.sh", "10-hermes-profile.sh", "80-registry.sh"]) {
570
- const text2 = readFileSync5(join7(templateRoot, "template", ".scripts", script), "utf8");
571
+ for (const script of ["01-config.sh", "05-fleet-env.sh", "10-hermes-profile.sh", "30-telegram.sh", "31-slack.sh", "80-registry.sh"]) {
572
+ const text2 = readFileSync6(join7(templateRoot, "template", ".scripts", script), "utf8");
571
573
  const hostGuard = text2.indexOf('if [[ "${SKIP_HOST_STATE:-0}" == "1" ]]');
572
574
  const source = text2.search(/^source\s/m);
573
575
  if (hostGuard < 0 || source < 0 || hostGuard > source) {
@@ -585,7 +587,7 @@ function preflightRenderedHermes(options) {
585
587
  const roleDir = resolve4(options.roleDir);
586
588
  if (!containedBy(target, roleDir)) return { ok: false, error: "rendered Hermes role escapes its project target" };
587
589
  try {
588
- const stat = lstatSync4(roleDir);
590
+ const stat = lstatSync5(roleDir);
589
591
  if (!stat.isDirectory() || stat.isSymbolicLink()) {
590
592
  return { ok: false, error: "rendered Hermes role must be a real directory" };
591
593
  }
@@ -606,7 +608,7 @@ function preflightRenderedHermes(options) {
606
608
  if (!required.ok) return required;
607
609
  for (const script of ["_lib.sh", "01-config.sh", "05-fleet-env.sh", "10-hermes-profile.sh", "20-runtime-repo.sh", "42-ticket-provider.sh", "70-systemd.sh", "80-registry.sh"]) {
608
610
  try {
609
- if (readFileSync5(join7(renderedScripts, script), "utf8") !== readFileSync5(join7(templateScripts, script), "utf8")) {
611
+ if (readFileSync6(join7(renderedScripts, script), "utf8") !== readFileSync6(join7(templateScripts, script), "utf8")) {
610
612
  return { ok: false, error: `rendered Hermes script differs from the attested template: ${script}` };
611
613
  }
612
614
  } catch (error) {
@@ -615,7 +617,7 @@ function preflightRenderedHermes(options) {
615
617
  }
616
618
  let role;
617
619
  try {
618
- const parsed = YAML2.parse(readFileSync5(join7(roleDir, "role.yaml"), "utf8"));
620
+ const parsed = YAML2.parse(readFileSync6(join7(roleDir, "role.yaml"), "utf8"));
619
621
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
620
622
  return { ok: false, error: "rendered Hermes role.yaml must contain a mapping" };
621
623
  }
@@ -637,7 +639,7 @@ function preflightRenderedHermes(options) {
637
639
  const manifestPath = join7(target, ".project.json");
638
640
  if (existsSync5(manifestPath)) {
639
641
  try {
640
- const manifest = JSON.parse(readFileSync5(manifestPath, "utf8"));
642
+ const manifest = JSON.parse(readFileSync6(manifestPath, "utf8"));
641
643
  const agents = manifest.agents;
642
644
  const declared = agents?.[options.agentId];
643
645
  if (!declared || declared.role !== options.role || declared.role_dir !== relative4(target, roleDir) || declared.provisioning_state !== "provisioned") {
@@ -828,7 +830,7 @@ var init_RegistryStore = __esm({
828
830
  async getByRepoPath(repoPath) {
829
831
  const registry = await this.load();
830
832
  return Object.values(registry.projects).find(
831
- (p6) => p6.repo_path === repoPath
833
+ (p4) => p4.repo_path === repoPath
832
834
  );
833
835
  }
834
836
  async close() {
@@ -905,7 +907,7 @@ var init_RegistryStore = __esm({
905
907
  }
906
908
  async loadTicketProvider(client, projectId) {
907
909
  const { rows } = await client.query(
908
- `SELECT provider_type, workspace, identifier, board_id, state
910
+ `SELECT provider_type, workspace, identifier, identifier_source, identifier_fetched_at, board_id, board_confirmed_at, state
909
911
  FROM public.project_ticket_boards
910
912
  WHERE project_id = $1
911
913
  LIMIT 1`,
@@ -915,11 +917,20 @@ var init_RegistryStore = __esm({
915
917
  return { type: "plane", workspace: "33god", identifier: "", board_id: "", state: "planned" };
916
918
  }
917
919
  const row = rows[0];
920
+ const fetchedAt = row.identifier_fetched_at;
921
+ const confirmedAt = row.board_confirmed_at;
918
922
  return {
919
923
  type: row.provider_type,
920
924
  workspace: row.workspace ?? void 0,
921
925
  identifier: row.identifier ?? void 0,
926
+ // Provenance is a column, not an extension blob: dropping it here would
927
+ // silently demote every provider-confirmed board back to a guess.
928
+ identifier_source: row.identifier_source ?? void 0,
929
+ identifier_fetched_at: fetchedAt ? fetchedAt instanceof Date ? fetchedAt.toISOString() : String(fetchedAt) : void 0,
922
930
  board_id: row.board_id ?? void 0,
931
+ // Board-binding provenance is its own column for the same reason
932
+ // identifier provenance is: dropping it demotes every honest link.
933
+ board_confirmed_at: confirmedAt ? confirmedAt instanceof Date ? confirmedAt.toISOString() : String(confirmedAt) : void 0,
923
934
  state: row.state ?? void 0
924
935
  };
925
936
  }
@@ -947,15 +958,18 @@ var init_RegistryStore = __esm({
947
958
  );
948
959
  await client.query(
949
960
  `INSERT INTO public.project_ticket_boards
950
- (repo_id, project_id, provider_type, workspace, identifier, board_id, state)
951
- VALUES ($1, $2, $3, $4, $5, $6, $7)`,
961
+ (repo_id, project_id, provider_type, workspace, identifier, identifier_source, identifier_fetched_at, board_id, board_confirmed_at, state)
962
+ VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)`,
952
963
  [
953
964
  repoId,
954
965
  projectId,
955
966
  tp.type,
956
967
  tp.workspace ?? null,
957
968
  tp.identifier ?? null,
969
+ tp.identifier_source ?? null,
970
+ tp.identifier_fetched_at ?? null,
958
971
  tp.board_id ?? null,
972
+ tp.board_confirmed_at ?? null,
959
973
  tp.state ?? null
960
974
  ]
961
975
  );
@@ -965,18 +979,69 @@ var init_RegistryStore = __esm({
965
979
  }
966
980
  });
967
981
 
982
+ // src/project/boardUrl.ts
983
+ var init_boardUrl = __esm({
984
+ "src/project/boardUrl.ts"() {
985
+ "use strict";
986
+ }
987
+ });
988
+
989
+ // src/utils/version.ts
990
+ import { readFileSync as readFileSync7 } from "node:fs";
991
+ import { dirname as dirname5, join as join8 } from "node:path";
992
+ import { fileURLToPath as fileURLToPath2 } from "node:url";
993
+ var PJANGLER_VERSION;
994
+ var init_version = __esm({
995
+ "src/utils/version.ts"() {
996
+ "use strict";
997
+ PJANGLER_VERSION = (() => {
998
+ try {
999
+ let dir = dirname5(fileURLToPath2(import.meta.url));
1000
+ for (let i = 0; i < 4; i++) {
1001
+ try {
1002
+ const raw = readFileSync7(join8(dir, "package.json"), "utf8");
1003
+ return JSON.parse(raw).version ?? "0.0.0";
1004
+ } catch {
1005
+ const parent = dirname5(dir);
1006
+ if (parent === dir) break;
1007
+ dir = parent;
1008
+ }
1009
+ }
1010
+ } catch {
1011
+ }
1012
+ return "0.0.0";
1013
+ })();
1014
+ }
1015
+ });
1016
+
1017
+ // src/project/boardQuery.ts
1018
+ function workspaceEnvKey(workspace) {
1019
+ const key = (workspace ?? "default").toUpperCase().replace(/[^A-Z0-9]/g, "_");
1020
+ return `PLANE_${key || "DEFAULT"}_API_KEY`;
1021
+ }
1022
+ var init_boardQuery = __esm({
1023
+ "src/project/boardQuery.ts"() {
1024
+ "use strict";
1025
+ init_boardUrl();
1026
+ init_version();
1027
+ }
1028
+ });
1029
+
968
1030
  // src/project/index.ts
969
- import { spawnSync as spawnSync2 } from "node:child_process";
1031
+ import { spawnSync as spawnSync3 } from "node:child_process";
970
1032
  import { isIP } from "node:net";
971
- import { chmodSync as chmodSync2, closeSync as closeSync2, copyFileSync as copyFileSync2, existsSync as existsSync6, fchmodSync, fsyncSync, lstatSync as lstatSync5, mkdirSync as mkdirSync4, mkdtempSync as mkdtempSync2, openSync as openSync2, readFileSync as readFileSync6, realpathSync as realpathSync3, renameSync as renameSync2, rmSync as rmSync2, statSync as statSync2, unlinkSync as unlinkSync2, writeFileSync as writeFileSync4 } from "node:fs";
1033
+ import { chmodSync as chmodSync2, closeSync as closeSync2, copyFileSync as copyFileSync2, existsSync as existsSync6, fchmodSync, fsyncSync, lstatSync as lstatSync6, mkdirSync as mkdirSync4, mkdtempSync as mkdtempSync2, openSync as openSync2, readFileSync as readFileSync8, realpathSync as realpathSync4, renameSync as renameSync3, rmSync as rmSync3, statSync as statSync2, unlinkSync as unlinkSync2, writeFileSync as writeFileSync4 } from "node:fs";
972
1034
  import { homedir as homedir4, tmpdir as tmpdir2 } from "node:os";
973
- import { basename as basename5, delimiter as delimiter2, dirname as dirname5, isAbsolute as isAbsolute2, join as join8, relative as relative5, resolve as resolve5, sep as sep2, win32 } from "node:path";
974
- import { fileURLToPath as fileURLToPath2 } from "node:url";
1035
+ import { basename as basename6, delimiter as delimiter2, dirname as dirname6, isAbsolute as isAbsolute2, join as join9, relative as relative5, resolve as resolve5, sep as sep2, win32 } from "node:path";
1036
+ import { fileURLToPath as fileURLToPath3 } from "node:url";
975
1037
  import YAML3 from "yaml";
1038
+ function providerAssignsIdentifiers(type) {
1039
+ return IDENTIFIER_ASSIGNING_PROVIDERS.includes((type ?? "").trim());
1040
+ }
976
1041
  function synchronizeCopierIdentity(manifestPath, manifest) {
977
- const answersPath = join8(dirname5(manifestPath), ".copier-answers.yml");
1042
+ const answersPath = join9(dirname6(manifestPath), ".copier-answers.yml");
978
1043
  if (!existsSync6(answersPath)) return [];
979
- const current = readFileSync6(answersPath, "utf8");
1044
+ const current = readFileSync8(answersPath, "utf8");
980
1045
  const document = YAML3.parseDocument(current);
981
1046
  if (document.errors.length) return [];
982
1047
  const name = String(document.get("project_name") ?? "");
@@ -990,7 +1055,7 @@ function synchronizeCopierIdentity(manifestPath, manifest) {
990
1055
  return [answersPath];
991
1056
  }
992
1057
  function projectRegistryPath(env2 = process.env) {
993
- return expandHome(env2[PROJECT_REGISTRY_ENV] || join8(homedir4(), ".config", "pjangler", "projects.yaml"));
1058
+ return expandHome(env2[PROJECT_REGISTRY_ENV] || join9(homedir4(), ".config", "pjangler", "projects.yaml"));
994
1059
  }
995
1060
  function createSafeRecord(entries = []) {
996
1061
  const record = /* @__PURE__ */ Object.create(null);
@@ -1005,7 +1070,7 @@ function emptyProjectRegistry() {
1005
1070
  }
1006
1071
  function loadProjectRegistry(path = projectRegistryPath()) {
1007
1072
  if (!existsSync6(path)) return emptyProjectRegistry();
1008
- const raw = YAML3.parse(readFileSync6(path, "utf8"));
1073
+ const raw = YAML3.parse(readFileSync8(path, "utf8"));
1009
1074
  if (raw == null) return emptyProjectRegistry();
1010
1075
  if (!isRecord(raw)) throw new Error(`Project registry must be a mapping: ${path}`);
1011
1076
  const registry = raw;
@@ -1084,14 +1149,14 @@ function fsyncDirectory(path) {
1084
1149
  }
1085
1150
  function saveProjectRegistry(registry, path = projectRegistryPath()) {
1086
1151
  validateProjectRegistry(registry);
1087
- mkdirSync4(dirname5(path), { recursive: true });
1152
+ mkdirSync4(dirname6(path), { recursive: true });
1088
1153
  let text2;
1089
1154
  let mode = 420;
1090
1155
  if (existsSync6(path)) {
1091
- const stat = lstatSync5(path);
1156
+ const stat = lstatSync6(path);
1092
1157
  if (!stat.isFile() || stat.isSymbolicLink()) throw new Error(`Project registry must be a regular file: ${path}`);
1093
1158
  mode = stat.mode & 511;
1094
- const current = readFileSync6(path, "utf8");
1159
+ const current = readFileSync8(path, "utf8");
1095
1160
  const document = YAML3.parseDocument(current);
1096
1161
  if (document.errors.length) throw new Error(`Project registry YAML is invalid: ${path}`);
1097
1162
  setYamlLeaf(document, ["schema_version"], registry.schema_version);
@@ -1118,9 +1183,9 @@ function saveProjectRegistry(registry, path = projectRegistryPath()) {
1118
1183
  fsyncSync(fd);
1119
1184
  closeSync2(fd);
1120
1185
  fd = void 0;
1121
- renameSync2(temp, path);
1186
+ renameSync3(temp, path);
1122
1187
  chmodSync2(path, mode);
1123
- fsyncDirectory(dirname5(path));
1188
+ fsyncDirectory(dirname6(path));
1124
1189
  } catch (error) {
1125
1190
  if (fd !== void 0) try {
1126
1191
  closeSync2(fd);
@@ -1142,6 +1207,7 @@ function validateProjectRegistry(registry) {
1142
1207
  const slugs = /* @__PURE__ */ new Set();
1143
1208
  const repoPaths = /* @__PURE__ */ new Map();
1144
1209
  const identifiers = /* @__PURE__ */ new Map();
1210
+ const boardIds = /* @__PURE__ */ new Map();
1145
1211
  const notebookIds = /* @__PURE__ */ new Map();
1146
1212
  const overviewNoteIds = /* @__PURE__ */ new Map();
1147
1213
  for (const [slug, project] of Object.entries(registry.projects)) {
@@ -1154,13 +1220,24 @@ function validateProjectRegistry(registry) {
1154
1220
  throw new Error(`Duplicate project repo_path: ${project.repo_path} used by ${existingRepoSlug} and ${slug}`);
1155
1221
  }
1156
1222
  repoPaths.set(repoKey, slug);
1223
+ const scope = ticketProviderScope(project.ticket_provider);
1224
+ const boardId = project.ticket_provider.board_id?.trim();
1225
+ if (boardId) {
1226
+ const boardKey = `${scope}\0${boardId}`;
1227
+ const existingBoardSlug = boardIds.get(boardKey);
1228
+ if (existingBoardSlug && existingBoardSlug !== slug) {
1229
+ throw new Error(`Duplicate project board_id: ${boardId} in ${scope.replace("\0", "/")} used by ${existingBoardSlug} and ${slug}`);
1230
+ }
1231
+ boardIds.set(boardKey, slug);
1232
+ }
1157
1233
  const identifier = project.ticket_provider.identifier?.toUpperCase();
1158
1234
  if (identifier) {
1159
- const existingIdentifierSlug = identifiers.get(identifier);
1235
+ const identifierKey = `${scope}\0${identifier}`;
1236
+ const existingIdentifierSlug = identifiers.get(identifierKey);
1160
1237
  if (existingIdentifierSlug && existingIdentifierSlug !== slug) {
1161
- throw new Error(`Duplicate project identifier: ${identifier} used by ${existingIdentifierSlug} and ${slug}`);
1238
+ throw new Error(`Duplicate project identifier: ${identifier} in ${scope.replace("\0", "/")} used by ${existingIdentifierSlug} and ${slug}`);
1162
1239
  }
1163
- identifiers.set(identifier, slug);
1240
+ identifiers.set(identifierKey, slug);
1164
1241
  }
1165
1242
  const notebookId = project.notebook?.notebook_id;
1166
1243
  if (notebookId) {
@@ -1176,6 +1253,9 @@ function validateProjectRegistry(registry) {
1176
1253
  }
1177
1254
  }
1178
1255
  }
1256
+ function ticketProviderScope(provider) {
1257
+ return `${provider.type ?? ""}\0${(provider.workspace ?? "").toLowerCase()}`;
1258
+ }
1179
1259
  function validateGlobalNotebookConfig(value) {
1180
1260
  if (value === void 0) return;
1181
1261
  if (!isRecord(value)) throw new Error("Project registry notebook must be a mapping");
@@ -1241,40 +1321,41 @@ function normalizeTicketProvider(value) {
1241
1321
  function buildTicketProviderBlock(input) {
1242
1322
  const type = normalizeTicketProvider(input.type);
1243
1323
  const boardId = input.boardId ?? "";
1244
- if (type === "trello") {
1245
- return {
1246
- type,
1247
- workspace: input.workspace ?? "",
1248
- identifier: input.identifier,
1249
- board_id: boardId,
1250
- state: boardId ? "linked" : "planned"
1251
- };
1252
- }
1253
- const workspace = input.workspace ?? "33god";
1324
+ const identifierSource = input.identifierSource ?? "proposed";
1325
+ const fetchedAt = identifierSource === "provider" ? input.identifierFetchedAt : void 0;
1326
+ const confirmedAt = input.boardConfirmedAt ?? (boardId ? fetchedAt : void 0);
1327
+ const provenClaim = !providerAssignsIdentifiers(type) || identifierSource === "provider";
1254
1328
  return {
1255
1329
  type,
1256
- workspace,
1330
+ workspace: input.workspace ?? (type === "trello" ? "" : "33god"),
1257
1331
  identifier: input.identifier,
1332
+ identifier_source: identifierSource,
1333
+ ...fetchedAt ? { identifier_fetched_at: fetchedAt } : {},
1258
1334
  board_id: boardId,
1259
- state: boardId ? "linked" : "planned"
1335
+ ...confirmedAt ? { board_confirmed_at: confirmedAt } : {},
1336
+ state: boardId && confirmedAt && provenClaim ? "linked" : "planned"
1260
1337
  };
1261
1338
  }
1262
- function ticketProviderKeyVar(provider) {
1263
- return provider === "trello" ? "TRELLO_KEY" : "PLANE_API_KEY";
1339
+ function ticketProviderKeyGroups(provider, workspace) {
1340
+ if (provider === "trello") return [["TRELLO_KEY"], ["TRELLO_TOKEN"]];
1341
+ return [[.../* @__PURE__ */ new Set(["PLANE_API_KEY", workspaceEnvKey(workspace)])]];
1264
1342
  }
1265
- function ticketProviderKeyVars(provider) {
1266
- return provider === "trello" ? ["TRELLO_KEY", "TRELLO_TOKEN"] : ["PLANE_API_KEY"];
1343
+ function ticketProviderKeyVars(provider, workspace) {
1344
+ return ticketProviderKeyGroups(provider, workspace).flat();
1267
1345
  }
1268
1346
  function ticketProviderSecretsPath(env2 = process.env) {
1269
- const base = env2.XDG_CONFIG_HOME || join8(env2.HOME || homedir4(), ".config");
1270
- return join8(base, "zshyzsh", "secrets.zsh");
1347
+ const base = env2.XDG_CONFIG_HOME || join9(env2.HOME || homedir4(), ".config");
1348
+ return join9(base, "zshyzsh", "secrets.zsh");
1349
+ }
1350
+ function ticketProviderFleetEnvPath(env2 = process.env) {
1351
+ return env2.HERMES_FLEET_ENV?.trim() || join9(env2.HOME || homedir4(), ".hermes", "fleet.env");
1271
1352
  }
1272
1353
  function readShellAssignments(path, keys) {
1273
1354
  const found = {};
1274
1355
  if (!existsSync6(path)) return found;
1275
1356
  let text2;
1276
1357
  try {
1277
- text2 = readFileSync6(path, "utf8");
1358
+ text2 = readFileSync8(path, "utf8");
1278
1359
  } catch {
1279
1360
  return found;
1280
1361
  }
@@ -1287,8 +1368,8 @@ function readShellAssignments(path, keys) {
1287
1368
  const key = match[1];
1288
1369
  if (!wanted.has(key) || found[key] !== void 0) continue;
1289
1370
  let value = match[2].trim();
1290
- const quote = value[0];
1291
- if ((quote === '"' || quote === "'") && value.length > 1 && value.endsWith(quote)) {
1371
+ const quote2 = value[0];
1372
+ if ((quote2 === '"' || quote2 === "'") && value.length > 1 && value.endsWith(quote2)) {
1292
1373
  value = value.slice(1, -1);
1293
1374
  } else {
1294
1375
  value = value.split(/\s+#/)[0].trim();
@@ -1310,16 +1391,16 @@ function resolveTicketProviderCredentials(input) {
1310
1391
  }
1311
1392
  }
1312
1393
  const candidates = [];
1313
- if (input.repoPath) candidates.push({ path: join8(input.repoPath, ".env"), label: join8(input.repoPath, ".env") });
1314
- const secrets = ticketProviderSecretsPath(env2);
1315
- candidates.push({ path: secrets, label: secrets });
1394
+ if (input.repoPath) candidates.push(join9(input.repoPath, ".env"));
1395
+ candidates.push(ticketProviderFleetEnvPath(env2));
1396
+ candidates.push(ticketProviderSecretsPath(env2));
1316
1397
  for (const candidate of candidates) {
1317
1398
  const outstanding = missing();
1318
1399
  if (!outstanding.length) break;
1319
- const assignments = readShellAssignments(candidate.path, outstanding);
1400
+ const assignments = readShellAssignments(candidate, outstanding);
1320
1401
  for (const [key, value] of Object.entries(assignments)) {
1321
1402
  values[key] = value;
1322
- sources[key] = candidate.label;
1403
+ sources[key] = candidate;
1323
1404
  }
1324
1405
  }
1325
1406
  return { values, sources };
@@ -1328,40 +1409,42 @@ function resolveTicketProviderAdapter(provider, env2 = process.env) {
1328
1409
  const file = `${provider}.sh`;
1329
1410
  const candidates = [];
1330
1411
  const override = env2[TICKET_PROVIDER_ADAPTERS_ENV];
1331
- if (override) candidates.push(join8(override, file));
1412
+ if (override) candidates.push(join9(override, file));
1332
1413
  const relativeRoots = [
1333
- join8("templates", "hermes-agent", "template", ".scripts", "providers"),
1334
- join8("agents", "hermes", "pm", ".scripts", "providers")
1414
+ join9("templates", "hermes-agent", "template", ".scripts", "providers"),
1415
+ join9("agents", "hermes", "pm", ".scripts", "providers")
1335
1416
  ];
1336
1417
  try {
1337
- let dir = dirname5(fileURLToPath2(import.meta.url));
1418
+ let dir = dirname6(fileURLToPath3(import.meta.url));
1338
1419
  for (let depth = 0; depth < 8; depth++) {
1339
- for (const relativeRoot of relativeRoots) candidates.push(join8(dir, relativeRoot, file));
1340
- const parent = dirname5(dir);
1420
+ for (const relativeRoot of relativeRoots) candidates.push(join9(dir, relativeRoot, file));
1421
+ const parent = dirname6(dir);
1341
1422
  if (parent === dir) break;
1342
1423
  dir = parent;
1343
1424
  }
1344
1425
  } catch {
1345
1426
  }
1346
1427
  for (const relativeRoot of relativeRoots) {
1347
- candidates.push(join8(homedir4(), "code", "pjangler", relativeRoot, file));
1428
+ candidates.push(join9(homedir4(), "code", "pjangler", relativeRoot, file));
1348
1429
  }
1349
1430
  return candidates.find((candidate) => existsSync6(candidate));
1350
1431
  }
1351
1432
  function provisionTicketProviderBoard(action, env2 = process.env) {
1352
1433
  const provider = action.provider;
1353
- const keyVar = ticketProviderKeyVar(provider);
1434
+ const groups = ticketProviderKeyGroups(provider, action.workspace);
1354
1435
  const { values } = resolveTicketProviderCredentials({
1355
- keys: ticketProviderKeyVars(provider),
1436
+ keys: ticketProviderKeyVars(provider, action.workspace),
1356
1437
  repoPath: action.repoPath,
1357
1438
  env: env2
1358
1439
  });
1359
- if (!values[keyVar]) {
1440
+ const unsatisfied = groups.filter((group) => !group.some((key) => values[key]));
1441
+ if (unsatisfied.length) {
1442
+ const names = unsatisfied.map((group) => group.join(" or ")).join(" and ");
1360
1443
  return {
1361
1444
  ok: true,
1362
1445
  skipped: true,
1363
1446
  logs: [
1364
- `ticket-provider: ${keyVar} not set; skipping ${provider} board creation (state stays "planned"). Set it in the environment, ${join8(action.repoPath, ".env")}, or ${ticketProviderSecretsPath(env2)}, then re-run with --live \u2014 or pass --board-id to link an existing board.`
1447
+ `ticket-provider: ${names} not set; skipping ${provider} board creation (state stays "planned"). Set it in the environment, ${join9(action.repoPath, ".env")}, ${ticketProviderFleetEnvPath(env2)}, or ${ticketProviderSecretsPath(env2)} \u2014 or pass --board-id to link an existing board.`
1365
1448
  ]
1366
1449
  };
1367
1450
  }
@@ -1375,12 +1458,12 @@ function provisionTicketProviderBoard(action, env2 = process.env) {
1375
1458
  };
1376
1459
  }
1377
1460
  const redact = (text2) => Object.values(values).reduce((acc, secret) => secret ? acc.split(secret).join("***") : acc, text2);
1378
- const staging = mkdtempSync2(join8(tmpdir2(), "pjangler-tp-"));
1461
+ const staging = mkdtempSync2(join9(tmpdir2(), "pjangler-tp-"));
1379
1462
  try {
1380
- const providersDir = join8(staging, "agents", "hermes", "pm", ".scripts", "providers");
1463
+ const providersDir = join9(staging, "agents", "hermes", "pm", ".scripts", "providers");
1381
1464
  mkdirSync4(providersDir, { recursive: true });
1382
1465
  writeFileSync4(
1383
- join8(staging, ".project.json"),
1466
+ join9(staging, ".project.json"),
1384
1467
  `${JSON.stringify(
1385
1468
  {
1386
1469
  project_name: action.boardName,
@@ -1399,11 +1482,11 @@ function provisionTicketProviderBoard(action, env2 = process.env) {
1399
1482
  `,
1400
1483
  "utf8"
1401
1484
  );
1402
- const staged = join8(providersDir, `${provider}.sh`);
1485
+ const staged = join9(providersDir, `${provider}.sh`);
1403
1486
  copyFileSync2(adapter, staged);
1404
1487
  const childEnv = { ...env2, ...values, TICKET_PROVIDER: provider };
1405
1488
  if (provider === "plane" && action.workspace) childEnv.PLANE_WORKSPACE = action.workspace;
1406
- const result2 = spawnSync2("sh", [staged, "create_board", action.boardName, action.identifier, action.description], {
1489
+ const result2 = spawnSync3("sh", [staged, "create_board", action.boardName, action.identifier, action.description], {
1407
1490
  cwd: existsSync6(action.repoPath) ? action.repoPath : staging,
1408
1491
  encoding: "utf8",
1409
1492
  env: childEnv
@@ -1447,16 +1530,26 @@ function provisionTicketProviderBoard(action, env2 = process.env) {
1447
1530
  error: `ticket-provider: ${provider} create_board returned no board_id`
1448
1531
  };
1449
1532
  }
1450
- const boardUrl = isRecord(parsed) && typeof parsed.board_url === "string" ? parsed.board_url : void 0;
1533
+ const identifier = isRecord(parsed) && typeof parsed.identifier === "string" ? parsed.identifier.trim() : "";
1534
+ if (!identifier) {
1535
+ return {
1536
+ ok: false,
1537
+ skipped: false,
1538
+ logs: [],
1539
+ error: `ticket-provider: ${provider} create_board returned no identifier. The adapter must echo the identifier the provider assigned; pjangler no longer invents one.`
1540
+ };
1541
+ }
1542
+ const boardUrl2 = isRecord(parsed) && typeof parsed.board_url === "string" ? parsed.board_url : void 0;
1451
1543
  return {
1452
1544
  ok: true,
1453
1545
  skipped: false,
1454
1546
  boardId,
1455
- boardUrl,
1456
- logs: [`ticket-provider: ${provider} board linked (${action.identifier} \u2192 ${boardId})`]
1547
+ identifier,
1548
+ boardUrl: boardUrl2,
1549
+ logs: [`ticket-provider: ${provider} board linked (${identifier} \u2192 ${boardId})`]
1457
1550
  };
1458
1551
  } finally {
1459
- rmSync2(staging, { recursive: true, force: true });
1552
+ rmSync3(staging, { recursive: true, force: true });
1460
1553
  }
1461
1554
  }
1462
1555
  function defaultProjectAutomation() {
@@ -1485,12 +1578,12 @@ function prospectiveRealPath(path) {
1485
1578
  let cursor = resolve5(path);
1486
1579
  const suffix = [];
1487
1580
  while (!existsSync6(cursor)) {
1488
- const parent = dirname5(cursor);
1581
+ const parent = dirname6(cursor);
1489
1582
  if (parent === cursor) return resolve5(path);
1490
- suffix.unshift(basename5(cursor));
1583
+ suffix.unshift(basename6(cursor));
1491
1584
  cursor = parent;
1492
1585
  }
1493
- return resolve5(realpathSync3(cursor), ...suffix);
1586
+ return resolve5(realpathSync4(cursor), ...suffix);
1494
1587
  }
1495
1588
  function resolveContainedPath(parentDir, candidate, label) {
1496
1589
  const physicalParent = prospectiveRealPath(parentDir);
@@ -1501,7 +1594,7 @@ function resolveContainedPath(parentDir, candidate, label) {
1501
1594
  }
1502
1595
  return resolve5(candidate);
1503
1596
  }
1504
- function deriveProjectIdentifier(value) {
1597
+ function proposeProjectIdentifier(value) {
1505
1598
  const compact = value.replace(/[^A-Za-z0-9]/g, "").toUpperCase();
1506
1599
  const identifier = compact.slice(0, 4) || "PROJ";
1507
1600
  return identifier.length >= 2 ? identifier : `${identifier}XX`.slice(0, 4);
@@ -1514,7 +1607,7 @@ function resolveAgentHooksLayer2(input, env2 = process.env) {
1514
1607
  const override = env2.PJ_AGENT_HOOKS_LAYER;
1515
1608
  if (override === "0" || override === "false") return false;
1516
1609
  if (override === "1" || override === "true") return true;
1517
- return !existsSync6(join8(homedir4(), ".agents", "hooks"));
1610
+ return !existsSync6(join9(homedir4(), ".agents", "hooks"));
1518
1611
  }
1519
1612
  function jsonStable(value) {
1520
1613
  return JSON.stringify(value);
@@ -1528,7 +1621,7 @@ function projectRecordEquivalent(a, b) {
1528
1621
  function defaultProjectTargetDir(name, cwd = process.cwd()) {
1529
1622
  const compactName = name.replace(/[^A-Za-z0-9._-]/g, "");
1530
1623
  const safeName = SAFE_PATH_SEGMENT.test(compactName) ? compactName : slugifyProjectName(name);
1531
- return resolve5(dirname5(resolve5(cwd)), validateSafePathSegment(safeName, "Generated project directory"));
1624
+ return resolve5(dirname6(resolve5(cwd)), validateSafePathSegment(safeName, "Generated project directory"));
1532
1625
  }
1533
1626
  function sourceSkillRoots(env2 = process.env) {
1534
1627
  const configuredRoots = (env2[PROJECT_SOURCE_SKILL_ROOTS_ENV] || "").split(delimiter2).map((root) => root.trim()).filter(Boolean);
@@ -1547,10 +1640,10 @@ function resolveSourceSkillPath(sourceSkill, env2 = process.env) {
1547
1640
  const expanded = expandHome(sourceSkill);
1548
1641
  const direct = resolve5(expanded);
1549
1642
  if (existsSync6(direct)) return direct;
1550
- const name = basename5(sourceSkill);
1643
+ const name = basename6(sourceSkill);
1551
1644
  const roots = sourceSkillRoots(env2);
1552
1645
  for (const root of roots) {
1553
- const candidate = join8(root, name);
1646
+ const candidate = join9(root, name);
1554
1647
  if (existsSync6(candidate)) return candidate;
1555
1648
  }
1556
1649
  const searched = roots.length ? ` Searched roots: ${roots.join(", ")}.` : "";
@@ -1565,8 +1658,14 @@ function planProjectInit(input) {
1565
1658
  const registry = loadProjectRegistry(registryPath2);
1566
1659
  const now = (input.now ?? /* @__PURE__ */ new Date()).toISOString();
1567
1660
  const targetDir = resolve5(input.targetDir ?? defaultProjectTargetDir(input.name, input.cwd));
1568
- const identifier = (input.projectIdentifier ?? deriveProjectIdentifier(input.name)).toUpperCase();
1661
+ const identifier = (input.projectIdentifier ?? proposeProjectIdentifier(input.name)).toUpperCase();
1569
1662
  const existing = getOwnRecordValue(registry.projects, slug);
1663
+ const resolvedBoardId = input.boardId ?? input.planeProjectId ?? (existing?.ticket_provider?.board_id || void 0);
1664
+ const inheritedProvenance = existing?.ticket_provider?.identifier_source === "provider" && existing.ticket_provider.identifier?.toUpperCase() === identifier && (existing.ticket_provider.board_id || void 0) === resolvedBoardId ? {
1665
+ identifierSource: "provider",
1666
+ ...existing.ticket_provider.identifier_fetched_at ? { identifierFetchedAt: existing.ticket_provider.identifier_fetched_at } : {}
1667
+ } : void 0;
1668
+ const inheritedBoardConfirmation = resolvedBoardId && (existing?.ticket_provider?.board_id || void 0) === resolvedBoardId && existing?.ticket_provider?.board_confirmed_at ? { boardConfirmedAt: existing.ticket_provider.board_confirmed_at } : void 0;
1570
1669
  const sourceSkillPath = resolveSourceSkillPath(input.sourceSkill);
1571
1670
  const overwrite = input.overwrite ?? input.force ?? false;
1572
1671
  const agents = createSafeRecord(Object.entries(existing?.agents ?? {}));
@@ -1601,11 +1700,13 @@ function planProjectInit(input) {
1601
1700
  ticket_provider: buildTicketProviderBlock({
1602
1701
  type: input.ticketProvider ?? "plane",
1603
1702
  identifier,
1604
- // A board provisioned by an earlier run lives in the registry, not in the
1605
- // CLI flags — inherit it so re-running init re-links instead of minting a
1606
- // second board.
1607
- boardId: input.boardId ?? input.planeProjectId ?? (existing?.ticket_provider?.board_id || void 0),
1608
- workspace: input.boardWorkspace ?? input.planeWorkspace
1703
+ boardId: resolvedBoardId,
1704
+ workspace: input.boardWorkspace ?? input.planeWorkspace,
1705
+ // Provenance survives a re-plan. Without this, re-running init on an
1706
+ // already-confirmed board would demote it back to "planned" because the
1707
+ // CLI has no way to re-derive where the identifier came from.
1708
+ ...inheritedProvenance ?? {},
1709
+ ...inheritedBoardConfirmation ?? {}
1609
1710
  }),
1610
1711
  agents,
1611
1712
  automation: existing?.automation ?? defaultProjectAutomation(),
@@ -1622,14 +1723,13 @@ function planProjectInit(input) {
1622
1723
  const manifest = projectManifestFromRegistryProject(project);
1623
1724
  const apply = input.apply ?? false;
1624
1725
  const live = input.live ?? false;
1625
- const provisionRuntimeRepo = input.provisionRuntimeRepo ?? live;
1626
- const provisionTicketBoard = input.provisionTicketBoard ?? live;
1726
+ const provisionTicketBoard = input.provisionTicketBoard ?? true;
1627
1727
  const enableSystemd = input.enableSystemd ?? live;
1628
1728
  const skipPlane = input.skipPlane ?? false;
1629
- const boardEnabled = live && provisionTicketBoard && !skipPlane;
1630
- const runtimeRepoEnabled = live && provisionRuntimeRepo;
1729
+ const boardEnabled = provisionTicketBoard && !skipPlane;
1631
1730
  const systemdEnabled = live && enableSystemd && process.platform !== "darwin";
1632
- const anyExternalAgentEffect = runtimeRepoEnabled || boardEnabled || systemdEnabled;
1731
+ const agentBoardEffect = live && boardEnabled;
1732
+ const anyExternalAgentEffect = agentBoardEffect || systemdEnabled;
1633
1733
  const actions = [
1634
1734
  { kind: "registry.upsert", registryPath: registryPath2, slug, project }
1635
1735
  ];
@@ -1652,7 +1752,7 @@ function planProjectInit(input) {
1652
1752
  }));
1653
1753
  }
1654
1754
  actions.push(
1655
- { kind: "project.write-manifest", path: join8(targetDir, ".project.json"), manifest },
1755
+ { kind: "project.write-manifest", path: join9(targetDir, ".project.json"), manifest },
1656
1756
  {
1657
1757
  kind: "ticket-provider.create-or-link",
1658
1758
  enabled: boardEnabled,
@@ -1665,7 +1765,7 @@ function planProjectInit(input) {
1665
1765
  description: project.description || `Ticket board for ${project.slug}`,
1666
1766
  boardId: project.ticket_provider.board_id ?? "",
1667
1767
  state: project.ticket_provider.board_id ? "linked" : "planned",
1668
- reason: skipPlane ? "ticket-provider action disabled by skipPlane=true" : project.ticket_provider.board_id ? "board already linked; no provider call" : !live ? "network/cloud actions require --live" : !provisionTicketBoard ? "ticket-provider action requires explicit provisionTicketBoard=true" : `create or link the ${project.ticket_provider.type} board "${project.name}" (${identifier}) via the ticket-provider adapter`
1768
+ reason: skipPlane ? "ticket-provider action disabled by skipPlane=true (--skip-board)" : project.ticket_provider.board_id ? "board already linked; no provider call" : !provisionTicketBoard ? "ticket-provider action requires explicit provisionTicketBoard=true" : `create or link the ${project.ticket_provider.type} board "${project.name}" (${identifier}) via the ticket-provider adapter`
1669
1769
  },
1670
1770
  {
1671
1771
  kind: "hermes.provision-agent",
@@ -1675,8 +1775,7 @@ function planProjectInit(input) {
1675
1775
  targetRepo: slug,
1676
1776
  role: agentRole,
1677
1777
  context: {
1678
- skipRuntimeRepo: !runtimeRepoEnabled,
1679
- skipPlane: !boardEnabled,
1778
+ skipPlane: !agentBoardEffect,
1680
1779
  // Per-agent Bloodbank consumers are retired. Agent ingress always
1681
1780
  // stays on the fleet-shared gateway, regardless of live/local mode.
1682
1781
  skipBloodbank: true,
@@ -1696,30 +1795,39 @@ function planProjectInit(input) {
1696
1795
  ...input.boardUrl !== void 0 ? { warnings: [BOARD_URL_DEPRECATION_WARNING] } : {}
1697
1796
  };
1698
1797
  }
1699
- function linkTicketProviderBoard(plan, action, boardId) {
1798
+ function linkTicketProviderBoard(plan, action, boardId, identifier, now = /* @__PURE__ */ new Date()) {
1700
1799
  const block = buildTicketProviderBlock({
1701
1800
  type: action.provider,
1702
- identifier: action.identifier,
1801
+ // The PROVIDER's identifier, not the one we proposed on the way in.
1802
+ identifier,
1803
+ identifierSource: "provider",
1804
+ identifierFetchedAt: now.toISOString(),
1703
1805
  boardId,
1704
1806
  workspace: action.workspace
1705
1807
  });
1808
+ action.identifier = identifier;
1706
1809
  plan.project.ticket_provider = block;
1707
1810
  const manifestProvider = {
1708
1811
  type: block.type,
1709
1812
  workspace: block.workspace ?? "",
1710
1813
  identifier: block.identifier ?? "",
1814
+ identifier_source: block.identifier_source ?? "proposed",
1815
+ ...block.identifier_fetched_at ? { identifier_fetched_at: block.identifier_fetched_at } : {},
1711
1816
  board_id: block.board_id ?? "",
1817
+ // The provider just handed this board back, and the manifest is where that
1818
+ // confirmation lives for every later reader of the repo.
1819
+ ...block.board_confirmed_at ? { board_confirmed_at: block.board_confirmed_at } : {},
1712
1820
  state: block.state ?? "linked"
1713
1821
  };
1714
1822
  plan.manifest.ticket_provider = manifestProvider;
1715
1823
  action.boardId = boardId;
1716
1824
  action.state = manifestProvider.state;
1717
- const manifestPath = join8(action.repoPath, ".project.json");
1825
+ const manifestPath = join9(action.repoPath, ".project.json");
1718
1826
  let next;
1719
1827
  if (existsSync6(manifestPath)) {
1720
1828
  let existing = {};
1721
1829
  try {
1722
- const parsed = JSON.parse(readFileSync6(manifestPath, "utf8"));
1830
+ const parsed = JSON.parse(readFileSync8(manifestPath, "utf8"));
1723
1831
  if (isRecord(parsed)) existing = parsed;
1724
1832
  } catch {
1725
1833
  existing = {};
@@ -1727,12 +1835,12 @@ function linkTicketProviderBoard(plan, action, boardId) {
1727
1835
  const existingProvider = isRecord(existing.ticket_provider) ? existing.ticket_provider : {};
1728
1836
  next = { ...existing, ticket_provider: { ...existingProvider, ...manifestProvider } };
1729
1837
  } else {
1730
- mkdirSync4(dirname5(manifestPath), { recursive: true });
1838
+ mkdirSync4(dirname6(manifestPath), { recursive: true });
1731
1839
  next = plan.manifest;
1732
1840
  }
1733
1841
  const text2 = `${JSON.stringify(next, null, 2)}
1734
1842
  `;
1735
- if (!existsSync6(manifestPath) || readFileSync6(manifestPath, "utf8") !== text2) {
1843
+ if (!existsSync6(manifestPath) || readFileSync8(manifestPath, "utf8") !== text2) {
1736
1844
  writeFileSync4(manifestPath, text2, "utf8");
1737
1845
  return [manifestPath];
1738
1846
  }
@@ -1761,7 +1869,7 @@ async function executeProjectInitPlan(plan, options = {}) {
1761
1869
  logs.push(
1762
1870
  action.data.agent_hooks_layer === "false" ? "commonproject: agent-hooks layer skipped (global ~/.agents/hooks detected \u2014 no per-user CLI injection)" : "commonproject: agent-hooks layer included"
1763
1871
  );
1764
- mkdirSync4(dirname5(action.targetDir), { recursive: true });
1872
+ mkdirSync4(dirname6(action.targetDir), { recursive: true });
1765
1873
  const before = snapshotTree(action.targetDir);
1766
1874
  const copierExecutable = options.trustedCopier?.executable ?? action.command[0];
1767
1875
  const copierEnv = options.trustedCopier ? { ...process.env } : void 0;
@@ -1771,7 +1879,7 @@ async function executeProjectInitPlan(plan, options = {}) {
1771
1879
  copierEnv.PYTHONNOUSERSITE = "1";
1772
1880
  copierEnv.PYTHONSAFEPATH = "1";
1773
1881
  }
1774
- const result2 = spawnSync2(copierExecutable, action.command.slice(1), {
1882
+ const result2 = spawnSync3(copierExecutable, action.command.slice(1), {
1775
1883
  encoding: "utf8",
1776
1884
  cwd: action.cwd,
1777
1885
  ...copierEnv ? { env: copierEnv } : {}
@@ -1792,11 +1900,11 @@ async function executeProjectInitPlan(plan, options = {}) {
1792
1900
  break;
1793
1901
  }
1794
1902
  } else if (action.kind === "project.write-manifest") {
1795
- mkdirSync4(dirname5(action.path), { recursive: true });
1903
+ mkdirSync4(dirname6(action.path), { recursive: true });
1796
1904
  let value = action.manifest;
1797
1905
  if (existsSync6(action.path)) {
1798
1906
  try {
1799
- const currentValue = JSON.parse(readFileSync6(action.path, "utf8"));
1907
+ const currentValue = JSON.parse(readFileSync8(action.path, "utf8"));
1800
1908
  if (isRecord(currentValue)) {
1801
1909
  const currentNotebook = isRecord(currentValue.notebook) ? currentValue.notebook : {};
1802
1910
  const desiredNotebook = isRecord(value.notebook) ? value.notebook : {};
@@ -1820,7 +1928,7 @@ async function executeProjectInitPlan(plan, options = {}) {
1820
1928
  }
1821
1929
  const next = `${JSON.stringify(value, null, 2)}
1822
1930
  `;
1823
- const current = existsSync6(action.path) ? readFileSync6(action.path, "utf8") : void 0;
1931
+ const current = existsSync6(action.path) ? readFileSync8(action.path, "utf8") : void 0;
1824
1932
  if (current !== next) {
1825
1933
  writeFileSync4(action.path, next, "utf8");
1826
1934
  changedFiles.push(action.path);
@@ -1838,8 +1946,8 @@ async function executeProjectInitPlan(plan, options = {}) {
1838
1946
  logs.push(...outcome.logs);
1839
1947
  if (!outcome.ok) {
1840
1948
  errors.push(outcome.error ?? `ticket-provider: ${action.provider} board provisioning failed`);
1841
- } else if (outcome.boardId) {
1842
- changedFiles.push(...linkTicketProviderBoard(plan, action, outcome.boardId));
1949
+ } else if (outcome.boardId && outcome.identifier) {
1950
+ changedFiles.push(...linkTicketProviderBoard(plan, action, outcome.boardId, outcome.identifier));
1843
1951
  pendingRegistryAction ??= {
1844
1952
  kind: "registry.upsert",
1845
1953
  registryPath: plan.registryPath,
@@ -1905,7 +2013,7 @@ function getProject(registry, slug) {
1905
2013
  return project;
1906
2014
  }
1907
2015
  function buildCommonProjectCopierAction(input) {
1908
- const templateDir = join8(input.pjanglerRoot, "templates", "commonproject");
2016
+ const templateDir = join9(input.pjanglerRoot, "templates", "commonproject");
1909
2017
  const data = {
1910
2018
  project_name: input.projectName,
1911
2019
  project_description: input.projectDescription ?? "",
@@ -1932,10 +2040,10 @@ function buildCommonProjectCopierAction(input) {
1932
2040
  };
1933
2041
  }
1934
2042
  function resolvePjanglerRoot() {
1935
- let dir = dirname5(fileURLToPath2(import.meta.url));
1936
- while (dir !== dirname5(dir)) {
1937
- if (existsSync6(join8(dir, "package.json")) && existsSync6(join8(dir, "templates", "commonproject", "copier.yml"))) return dir;
1938
- dir = dirname5(dir);
2043
+ let dir = dirname6(fileURLToPath3(import.meta.url));
2044
+ while (dir !== dirname6(dir)) {
2045
+ if (existsSync6(join9(dir, "package.json")) && existsSync6(join9(dir, "templates", "commonproject", "copier.yml"))) return dir;
2046
+ dir = dirname6(dir);
1939
2047
  }
1940
2048
  return resolve5(process.cwd());
1941
2049
  }
@@ -1949,6 +2057,10 @@ function validateNoDuplicateProject(registry, project, overwrite) {
1949
2057
  if (resolve5(existing.repo_path) === resolve5(project.repo_path)) {
1950
2058
  throw new Error(`Project repo_path already registered by ${slug}: ${project.repo_path}`);
1951
2059
  }
2060
+ if (ticketProviderScope(existing.ticket_provider) !== ticketProviderScope(project.ticket_provider)) continue;
2061
+ if (existing.ticket_provider.board_id?.trim() && existing.ticket_provider.board_id === project.ticket_provider.board_id) {
2062
+ throw new Error(`Project board_id already registered by ${slug}: ${project.ticket_provider.board_id}`);
2063
+ }
1952
2064
  if (existing.ticket_provider.identifier && existing.ticket_provider.identifier.toUpperCase() === project.ticket_provider.identifier?.toUpperCase()) {
1953
2065
  throw new Error(`Project identifier already registered by ${slug}: ${project.ticket_provider.identifier}`);
1954
2066
  }
@@ -1964,6 +2076,28 @@ function validateProjectRecord(project, key) {
1964
2076
  if (!project.repo_path) throw new Error(`Project ${key} missing repo_path`);
1965
2077
  if (!Array.isArray(project.source_artifacts)) throw new Error(`Project ${key} source_artifacts must be a list`);
1966
2078
  if (!isRecord(project.ticket_provider)) throw new Error(`Project ${key} ticket_provider must be a mapping`);
2079
+ const provider = project.ticket_provider;
2080
+ if (provider.state !== void 0 && !TICKET_PROVIDER_STATES.includes(provider.state)) {
2081
+ throw new Error(`Project ${key} ticket_provider.state must be one of ${TICKET_PROVIDER_STATES.join(" | ")}; got ${JSON.stringify(provider.state)}`);
2082
+ }
2083
+ if (provider.identifier_source !== void 0 && !PROJECT_IDENTIFIER_SOURCES.includes(provider.identifier_source)) {
2084
+ throw new Error(`Project ${key} ticket_provider.identifier_source must be one of ${PROJECT_IDENTIFIER_SOURCES.join(" | ")}; got ${JSON.stringify(provider.identifier_source)}`);
2085
+ }
2086
+ if (provider.state === "linked" && !(provider.board_id && provider.board_confirmed_at)) {
2087
+ throw new Error(
2088
+ `Project ${key} ticket_provider.state is "linked" but its board binding is not provider-confirmed (board_id=${JSON.stringify(provider.board_id ?? "")}, board_confirmed_at=${JSON.stringify(provider.board_confirmed_at ?? "")}). Run \`${IDENTIFIER_REPAIR_COMMAND}\`.`
2089
+ );
2090
+ }
2091
+ if (provider.identifier_source === "provider" && !(provider.identifier && provider.identifier_fetched_at)) {
2092
+ throw new Error(
2093
+ `Project ${key} ticket_provider.identifier_source is "provider" but no identifier was read back (identifier=${JSON.stringify(provider.identifier ?? "")}, identifier_fetched_at=${JSON.stringify(provider.identifier_fetched_at ?? "")}). Run \`${IDENTIFIER_REPAIR_COMMAND}\`.`
2094
+ );
2095
+ }
2096
+ if (provider.state === "linked" && providerAssignsIdentifiers(provider.type) && provider.identifier_source !== "provider") {
2097
+ throw new Error(
2098
+ `Project ${key} ticket_provider.state is "linked" on ${provider.type}, which assigns its own identifiers, but identifier_source=${JSON.stringify(provider.identifier_source ?? "")} (identifier=${JSON.stringify(provider.identifier ?? "")}). Run \`${IDENTIFIER_REPAIR_COMMAND}\`.`
2099
+ );
2100
+ }
1967
2101
  if (!isRecord(project.agents)) throw new Error(`Project ${key} agents must be a mapping`);
1968
2102
  if (project.notebook !== void 0) {
1969
2103
  if (!isRecord(project.notebook)) throw new Error(`Project ${key} notebook must be a mapping`);
@@ -1985,13 +2119,13 @@ function validateProjectRecord(project, key) {
1985
2119
  }
1986
2120
  function expandHome(path) {
1987
2121
  if (path === "~") return homedir4();
1988
- if (path.startsWith("~/")) return join8(homedir4(), path.slice(2));
2122
+ if (path.startsWith("~/")) return join9(homedir4(), path.slice(2));
1989
2123
  return path;
1990
2124
  }
1991
2125
  function isRecord(value) {
1992
2126
  return typeof value === "object" && value !== null && !Array.isArray(value);
1993
2127
  }
1994
- var PROJECT_REGISTRY_ENV, PROJECT_SOURCE_SKILL_ROOTS_ENV, TICKET_PROVIDER_ADAPTERS_ENV, PROJECT_REGISTRY_SCHEMA_VERSION, DEFAULT_NEW_PROJECT_STATUS, BOARD_URL_DEPRECATION_WARNING, DEFAULT_SOURCE_SKILL_ROOTS, PROJECT_REGISTRY_OWNED_KEYS, PROJECT_NOTEBOOK_OWNED_KEYS, TICKET_PROVIDER_OWNED_KEYS, GLOBAL_NOTEBOOK_OWNED_KEYS, GLOBAL_NOTEBOOK_AUTH_OWNED_KEYS, GLOBAL_NOTEBOOK_DEFAULTS_OWNED_KEYS, GLOBAL_NOTEBOOK_LIMITS_OWNED_KEYS, GLOBAL_NOTEBOOK_SUMMARIZER_OWNED_KEYS, SAFE_PATH_SEGMENT;
2128
+ var PROJECT_REGISTRY_ENV, PROJECT_SOURCE_SKILL_ROOTS_ENV, TICKET_PROVIDER_ADAPTERS_ENV, PROJECT_REGISTRY_SCHEMA_VERSION, DEFAULT_NEW_PROJECT_STATUS, BOARD_URL_DEPRECATION_WARNING, PROJECT_IDENTIFIER_SOURCES, IDENTIFIER_ASSIGNING_PROVIDERS, TICKET_PROVIDER_STATES, IDENTIFIER_REPAIR_COMMAND, DEFAULT_SOURCE_SKILL_ROOTS, PROJECT_REGISTRY_OWNED_KEYS, PROJECT_NOTEBOOK_OWNED_KEYS, TICKET_PROVIDER_OWNED_KEYS, GLOBAL_NOTEBOOK_OWNED_KEYS, GLOBAL_NOTEBOOK_AUTH_OWNED_KEYS, GLOBAL_NOTEBOOK_DEFAULTS_OWNED_KEYS, GLOBAL_NOTEBOOK_LIMITS_OWNED_KEYS, GLOBAL_NOTEBOOK_SUMMARIZER_OWNED_KEYS, SAFE_PATH_SEGMENT;
1995
2129
  var init_project = __esm({
1996
2130
  "src/project/index.ts"() {
1997
2131
  "use strict";
@@ -2000,16 +2134,21 @@ var init_project = __esm({
2000
2134
  init_tree_diff();
2001
2135
  init_preflight();
2002
2136
  init_RegistryStore();
2137
+ init_boardQuery();
2003
2138
  PROJECT_REGISTRY_ENV = "PJ_PROJECT_REGISTRY";
2004
2139
  PROJECT_SOURCE_SKILL_ROOTS_ENV = "PJ_SOURCE_SKILL_ROOTS";
2005
2140
  TICKET_PROVIDER_ADAPTERS_ENV = "PJ_TICKET_PROVIDER_ADAPTERS";
2006
2141
  PROJECT_REGISTRY_SCHEMA_VERSION = 1;
2007
2142
  DEFAULT_NEW_PROJECT_STATUS = "active";
2008
2143
  BOARD_URL_DEPRECATION_WARNING = "boardUrl is deprecated and ignored; board URLs are derived at runtime and are never persisted.";
2144
+ PROJECT_IDENTIFIER_SOURCES = ["provider", "proposed"];
2145
+ IDENTIFIER_ASSIGNING_PROVIDERS = ["plane", "linear"];
2146
+ TICKET_PROVIDER_STATES = ["planned", "linked", "skipped"];
2147
+ IDENTIFIER_REPAIR_COMMAND = "pj project identity --all --apply";
2009
2148
  DEFAULT_SOURCE_SKILL_ROOTS = [
2010
2149
  "/home/delorenj/code/skillex/all-skills",
2011
- join8(homedir4(), ".agents", "skills"),
2012
- join8(homedir4(), ".codex", "skills")
2150
+ join9(homedir4(), ".agents", "skills"),
2151
+ join9(homedir4(), ".codex", "skills")
2013
2152
  ];
2014
2153
  PROJECT_REGISTRY_OWNED_KEYS = [
2015
2154
  "name",
@@ -2027,7 +2166,7 @@ var init_project = __esm({
2027
2166
  "updated_at"
2028
2167
  ];
2029
2168
  PROJECT_NOTEBOOK_OWNED_KEYS = ["state", "notebook_id", "notebook_name", "overview_note_id", "blocked_reason"];
2030
- TICKET_PROVIDER_OWNED_KEYS = ["type", "workspace", "identifier", "board_id", "board_url", "state"];
2169
+ TICKET_PROVIDER_OWNED_KEYS = ["type", "workspace", "identifier", "identifier_source", "identifier_fetched_at", "board_id", "board_confirmed_at", "board_url", "state"];
2031
2170
  GLOBAL_NOTEBOOK_OWNED_KEYS = ["base_url", "auth", "defaults", "limits", "summarizer"];
2032
2171
  GLOBAL_NOTEBOOK_AUTH_OWNED_KEYS = ["mode", "env_var"];
2033
2172
  GLOBAL_NOTEBOOK_DEFAULTS_OWNED_KEYS = ["enabled", "session_start_enabled", "session_capture_enabled", "overview_max_chars", "documentation_globs", "overview_references", "excluded_globs"];
@@ -2040,21 +2179,21 @@ var init_project = __esm({
2040
2179
  // src/notebook/config.ts
2041
2180
  import { randomUUID } from "node:crypto";
2042
2181
  import { isIP as isIP2 } from "node:net";
2043
- import { closeSync as closeSync3, existsSync as existsSync13, fchmodSync as fchmodSync2, fsyncSync as fsyncSync2, lstatSync as lstatSync6, openSync as openSync3, readFileSync as readFileSync10, realpathSync as realpathSync4, renameSync as renameSync3, writeFileSync as writeFileSync8 } from "node:fs";
2044
- import { dirname as dirname7, resolve as resolve7 } from "node:path";
2182
+ import { closeSync as closeSync3, existsSync as existsSync14, fchmodSync as fchmodSync2, fsyncSync as fsyncSync2, lstatSync as lstatSync8, openSync as openSync3, readFileSync as readFileSync14, realpathSync as realpathSync5, renameSync as renameSync4, writeFileSync as writeFileSync8 } from "node:fs";
2183
+ import { dirname as dirname8, resolve as resolve7 } from "node:path";
2045
2184
  function isRecord2(value) {
2046
2185
  return Boolean(value) && typeof value === "object" && !Array.isArray(value);
2047
2186
  }
2048
2187
  function realOrResolved(path) {
2049
2188
  const absolute = resolve7(path);
2050
- return existsSync13(absolute) ? realpathSync4(absolute) : absolute;
2189
+ return existsSync14(absolute) ? realpathSync5(absolute) : absolute;
2051
2190
  }
2052
2191
  function readManifest(repoPath) {
2053
2192
  const path = resolve7(repoPath, ".project.json");
2054
- if (!existsSync13(path)) return null;
2193
+ if (!existsSync14(path)) return null;
2055
2194
  let parsed;
2056
2195
  try {
2057
- parsed = JSON.parse(readFileSync10(path, "utf8"));
2196
+ parsed = JSON.parse(readFileSync14(path, "utf8"));
2058
2197
  } catch {
2059
2198
  throw new NotebookError("INVALID_INPUT", `${path} is not valid JSON`);
2060
2199
  }
@@ -2279,7 +2418,7 @@ function requireRemoteNotebookConfig(config) {
2279
2418
  function persistProjectNotebookBinding(resolved, binding, policy) {
2280
2419
  const changed = [];
2281
2420
  const manifestPath = resolve7(resolved.project.repo_path, ".project.json");
2282
- const manifestRaw = existsSync13(manifestPath) ? JSON.parse(readFileSync10(manifestPath, "utf8")) : {};
2421
+ const manifestRaw = existsSync14(manifestPath) ? JSON.parse(readFileSync14(manifestPath, "utf8")) : {};
2283
2422
  if (!isRecord2(manifestRaw)) throw new NotebookError("INVALID_INPUT", `${manifestPath} must contain a JSON object`);
2284
2423
  validateManifestNotebookSurface(manifestRaw.notebook);
2285
2424
  const manifestNotebook2 = isRecord2(manifestRaw.notebook) ? manifestRaw.notebook : {};
@@ -2296,14 +2435,14 @@ function persistProjectNotebookBinding(resolved, binding, policy) {
2296
2435
  };
2297
2436
  const manifestText = `${JSON.stringify(manifestNext, null, 2)}
2298
2437
  `;
2299
- if (!existsSync13(manifestPath) || readFileSync10(manifestPath, "utf8") !== manifestText) {
2438
+ if (!existsSync14(manifestPath) || readFileSync14(manifestPath, "utf8") !== manifestText) {
2300
2439
  let mode = 420;
2301
- if (existsSync13(manifestPath)) {
2302
- const current2 = lstatSync6(manifestPath);
2440
+ if (existsSync14(manifestPath)) {
2441
+ const current2 = lstatSync8(manifestPath);
2303
2442
  if (!current2.isFile() || current2.isSymbolicLink()) throw new NotebookError("CONFLICT", `${manifestPath} must be a regular non-symlink file`);
2304
2443
  mode = current2.mode & 511;
2305
2444
  }
2306
- const temp = resolve7(dirname7(manifestPath), `.${process.pid}.${randomUUID()}.project.json.tmp`);
2445
+ const temp = resolve7(dirname8(manifestPath), `.${process.pid}.${randomUUID()}.project.json.tmp`);
2307
2446
  const fd = openSync3(temp, "wx", mode);
2308
2447
  try {
2309
2448
  writeFileSync8(fd, manifestText, "utf8");
@@ -2312,9 +2451,9 @@ function persistProjectNotebookBinding(resolved, binding, policy) {
2312
2451
  } finally {
2313
2452
  closeSync3(fd);
2314
2453
  }
2315
- renameSync3(temp, manifestPath);
2454
+ renameSync4(temp, manifestPath);
2316
2455
  try {
2317
- const directory = openSync3(dirname7(manifestPath), "r");
2456
+ const directory = openSync3(dirname8(manifestPath), "r");
2318
2457
  try {
2319
2458
  fsyncSync2(directory);
2320
2459
  } finally {
@@ -2326,9 +2465,9 @@ function persistProjectNotebookBinding(resolved, binding, policy) {
2326
2465
  }
2327
2466
  const current = resolved.project.notebook;
2328
2467
  resolved.project.notebook = { ...isRecord2(current) ? current : { state: "planned" }, ...binding };
2329
- const registryBefore = existsSync13(resolved.registry_path) ? readFileSync10(resolved.registry_path, "utf8") : null;
2468
+ const registryBefore = existsSync14(resolved.registry_path) ? readFileSync14(resolved.registry_path, "utf8") : null;
2330
2469
  saveProjectRegistry(resolved.registry, resolved.registry_path);
2331
- const registryAfter = existsSync13(resolved.registry_path) ? readFileSync10(resolved.registry_path, "utf8") : null;
2470
+ const registryAfter = existsSync14(resolved.registry_path) ? readFileSync14(resolved.registry_path, "utf8") : null;
2332
2471
  if (registryBefore !== registryAfter) changed.push(resolved.registry_path);
2333
2472
  return changed;
2334
2473
  }
@@ -2474,26 +2613,26 @@ function truncateUtf8(value, maxBytes) {
2474
2613
  }
2475
2614
  return result2.join("");
2476
2615
  }
2477
- function noteDetail(note2, maxBytes) {
2478
- const parsed = parseNoteEnvelope(note2.content);
2616
+ function noteDetail(note, maxBytes) {
2617
+ const parsed = parseNoteEnvelope(note.content);
2479
2618
  return {
2480
- id: note2.id,
2481
- title: note2.title,
2482
- note_type: note2.note_type,
2483
- created_at: note2.created_at,
2484
- updated_at: note2.updated_at,
2485
- content: truncateUtf8(parsed?.body ?? note2.content, maxBytes)
2619
+ id: note.id,
2620
+ title: note.title,
2621
+ note_type: note.note_type,
2622
+ created_at: note.created_at,
2623
+ updated_at: note.updated_at,
2624
+ content: truncateUtf8(parsed?.body ?? note.content, maxBytes)
2486
2625
  };
2487
2626
  }
2488
- function noteSummary(note2, excerptMaxChars) {
2489
- const body = parseNoteEnvelope(note2.content)?.body ?? note2.content;
2627
+ function noteSummary(note, excerptMaxChars) {
2628
+ const body = parseNoteEnvelope(note.content)?.body ?? note.content;
2490
2629
  const excerpt = Array.from(body.replace(/\s+/gu, " ").trim()).slice(0, excerptMaxChars).join("");
2491
2630
  return {
2492
- id: note2.id,
2493
- title: note2.title,
2494
- note_type: note2.note_type,
2495
- created_at: note2.created_at,
2496
- updated_at: note2.updated_at,
2631
+ id: note.id,
2632
+ title: note.title,
2633
+ note_type: note.note_type,
2634
+ created_at: note.created_at,
2635
+ updated_at: note.updated_at,
2497
2636
  excerpt
2498
2637
  };
2499
2638
  }
@@ -2510,13 +2649,13 @@ function searchNotesLocally(notes, query, limit, excerptMaxChars) {
2510
2649
  const queryTokens = tokenizeSearch(query);
2511
2650
  if (!queryTokens.length) throw new NotebookError("INVALID_INPUT", "Search query must contain at least one letter or number");
2512
2651
  if (!Number.isSafeInteger(limit) || limit < 1) throw new NotebookError("INVALID_INPUT", "Search limit must be a positive integer");
2513
- const scored = notes.flatMap((note2) => {
2514
- const body = parseNoteEnvelope(note2.content)?.body ?? note2.content;
2515
- const titleTokens = tokenizeSearch(note2.title);
2652
+ const scored = notes.flatMap((note) => {
2653
+ const body = parseNoteEnvelope(note.content)?.body ?? note.content;
2654
+ const titleTokens = tokenizeSearch(note.title);
2516
2655
  const bodyTokens = tokenizeSearch(body);
2517
2656
  if (!queryTokens.every((token) => titleTokens.includes(token) || bodyTokens.includes(token))) return [];
2518
2657
  const score = queryTokens.reduce((sum, token) => sum + 10 * countTokens(titleTokens, token) + countTokens(bodyTokens, token), 0);
2519
- return [{ note: note2, body, score }];
2658
+ return [{ note, body, score }];
2520
2659
  });
2521
2660
  const timestamp2 = (value) => {
2522
2661
  const parsed = Date.parse(value ?? "");
@@ -2524,12 +2663,12 @@ function searchNotesLocally(notes, query, limit, excerptMaxChars) {
2524
2663
  };
2525
2664
  scored.sort((left, right) => right.score - left.score || timestamp2(right.note.updated_at) - timestamp2(left.note.updated_at) || left.note.id.localeCompare(right.note.id, "en"));
2526
2665
  return {
2527
- items: scored.slice(0, limit).map(({ note: note2, body }) => {
2666
+ items: scored.slice(0, limit).map(({ note, body }) => {
2528
2667
  const normalizedBody = body.replace(/\s+/gu, " ").trim().normalize("NFKC");
2529
2668
  const lower = normalizedBody.toLocaleLowerCase("und");
2530
2669
  const starts = queryTokens.map((token) => lower.indexOf(token)).filter((index) => index >= 0);
2531
2670
  const start = starts.length ? Math.min(...starts) : 0;
2532
- return { ...noteSummary(note2, excerptMaxChars), excerpt: Array.from(normalizedBody.slice(start)).slice(0, excerptMaxChars).join("") };
2671
+ return { ...noteSummary(note, excerptMaxChars), excerpt: Array.from(normalizedBody.slice(start)).slice(0, excerptMaxChars).join("") };
2533
2672
  }),
2534
2673
  next_cursor: null,
2535
2674
  query_tokens: queryTokens
@@ -2634,23 +2773,23 @@ import { createHash as createHash6, randomUUID as randomUUID3 } from "node:crypt
2634
2773
  import {
2635
2774
  closeSync as closeSync4,
2636
2775
  constants as constants3,
2637
- existsSync as existsSync14,
2776
+ existsSync as existsSync15,
2638
2777
  fchmodSync as fchmodSync3,
2639
2778
  fstatSync as fstatSync2,
2640
2779
  fsyncSync as fsyncSync3,
2641
- lstatSync as lstatSync7,
2780
+ lstatSync as lstatSync9,
2642
2781
  mkdirSync as mkdirSync6,
2643
2782
  openSync as openSync4,
2644
2783
  readSync,
2645
- readdirSync as readdirSync6,
2646
- renameSync as renameSync4,
2647
- unlinkSync as unlinkSync5,
2784
+ readdirSync as readdirSync7,
2785
+ renameSync as renameSync5,
2786
+ unlinkSync as unlinkSync4,
2648
2787
  writeFileSync as writeFileSync9
2649
2788
  } from "node:fs";
2650
2789
  import { homedir as homedir6 } from "node:os";
2651
- import { basename as basename6, dirname as dirname8, join as join15, parse, relative as relative7, resolve as resolve8, sep as sep3 } from "node:path";
2790
+ import { basename as basename7, dirname as dirname9, join as join18, parse, relative as relative8, resolve as resolve8, sep as sep3 } from "node:path";
2652
2791
  function notebookStateRoot(env2 = process.env) {
2653
- const base = env2.XDG_STATE_HOME || join15(env2.HOME || homedir6(), ".local", "state");
2792
+ const base = env2.XDG_STATE_HOME || join18(env2.HOME || homedir6(), ".local", "state");
2654
2793
  return resolve8(base, "pjangler", "notebook", NOTEBOOK_STATE_VERSION);
2655
2794
  }
2656
2795
  function assertDigest(value, label) {
@@ -2658,10 +2797,10 @@ function assertDigest(value, label) {
2658
2797
  }
2659
2798
  function projectStateDir(root, projectSlug) {
2660
2799
  if (!/^[a-z0-9][a-z0-9._-]{0,127}$/iu.test(projectSlug)) throw new NotebookError("INVALID_INPUT", "Invalid project slug for Notebook state");
2661
- return join15(resolve8(root), "projects", sha256Hex(`pjangler-project-state-v1\0${projectSlug}`));
2800
+ return join18(resolve8(root), "projects", sha256Hex(`pjangler-project-state-v1\0${projectSlug}`));
2662
2801
  }
2663
2802
  function assertContained(root, candidate) {
2664
- const rel = relative7(resolve8(root), resolve8(candidate));
2803
+ const rel = relative8(resolve8(root), resolve8(candidate));
2665
2804
  if (!rel || !rel.startsWith(`..${sep3}`) && rel !== ".." && !rel.startsWith(sep3)) return;
2666
2805
  throw new NotebookError("INTERNAL_ERROR", "Notebook state path escaped its root");
2667
2806
  }
@@ -2669,14 +2808,14 @@ function openPinnedDirectory(path, root, create) {
2669
2808
  const absolute = resolve8(path);
2670
2809
  const absoluteRoot = resolve8(root);
2671
2810
  assertContained(absoluteRoot, absolute);
2672
- if (!existsSync14("/proc/self/fd")) throw new NotebookError("INTERNAL_ERROR", "Descriptor-pinned Notebook state requires procfs");
2811
+ if (!existsSync15("/proc/self/fd")) throw new NotebookError("INTERNAL_ERROR", "Descriptor-pinned Notebook state requires procfs");
2673
2812
  const parsed = parse(absolute);
2674
2813
  let fd = openSync4(parsed.root, DIRECTORY_OPEN_FLAGS);
2675
2814
  let cursor = parsed.root;
2676
2815
  try {
2677
2816
  for (const part of absolute.slice(parsed.root.length).split(sep3).filter(Boolean)) {
2678
2817
  const child = `/proc/self/fd/${fd}/${part}`;
2679
- cursor = join15(cursor, part);
2818
+ cursor = join18(cursor, part);
2680
2819
  let childFd;
2681
2820
  try {
2682
2821
  childFd = openSync4(child, DIRECTORY_OPEN_FLAGS);
@@ -2693,7 +2832,7 @@ function openPinnedDirectory(path, root, create) {
2693
2832
  fd = childFd;
2694
2833
  const stat = fstatSync2(fd);
2695
2834
  if (!stat.isDirectory()) throw new NotebookError("INTERNAL_ERROR", "Notebook state path component is not a directory");
2696
- if (cursor === absoluteRoot || relative7(absoluteRoot, cursor).startsWith("..") === false) assertOwned(stat);
2835
+ if (cursor === absoluteRoot || relative8(absoluteRoot, cursor).startsWith("..") === false) assertOwned(stat);
2697
2836
  }
2698
2837
  return fd;
2699
2838
  } catch (error) {
@@ -2705,7 +2844,7 @@ function openPinnedDirectory(path, root, create) {
2705
2844
  }
2706
2845
  }
2707
2846
  function pinnedLeaf(parentFd, path) {
2708
- return `/proc/self/fd/${parentFd}/${basename6(path)}`;
2847
+ return `/proc/self/fd/${parentFd}/${basename7(path)}`;
2709
2848
  }
2710
2849
  function assertOwned(stat) {
2711
2850
  if (typeof process.getuid === "function" && stat.uid !== process.getuid()) {
@@ -2733,18 +2872,18 @@ function notebookStatePaths(root, projectSlug) {
2733
2872
  return {
2734
2873
  root: absoluteRoot,
2735
2874
  project,
2736
- baselines: join15(project, "baselines"),
2737
- claims: join15(project, "claims"),
2738
- receipts: join15(project, "receipts"),
2739
- refusals: join15(project, "refusals"),
2740
- journals: join15(project, "journals"),
2741
- locks: join15(project, "locks")
2875
+ baselines: join18(project, "baselines"),
2876
+ claims: join18(project, "claims"),
2877
+ receipts: join18(project, "receipts"),
2878
+ refusals: join18(project, "refusals"),
2879
+ journals: join18(project, "journals"),
2880
+ locks: join18(project, "locks")
2742
2881
  };
2743
2882
  }
2744
2883
  function ensureNotebookState(root, projectSlug) {
2745
2884
  const paths = notebookStatePaths(root, projectSlug);
2746
2885
  const { root: absoluteRoot, project } = paths;
2747
- for (const path of [absoluteRoot, join15(absoluteRoot, "projects"), project, paths.baselines, paths.claims, paths.receipts, paths.refusals, paths.journals, paths.locks]) {
2886
+ for (const path of [absoluteRoot, join18(absoluteRoot, "projects"), project, paths.baselines, paths.claims, paths.receipts, paths.refusals, paths.journals, paths.locks]) {
2748
2887
  ensureDirectory(path, absoluteRoot);
2749
2888
  }
2750
2889
  return paths;
@@ -2766,7 +2905,7 @@ function fsyncDirectory2(path) {
2766
2905
  function readStateDirectory(path, root) {
2767
2906
  const fd = openPinnedDirectory(path, root, false);
2768
2907
  try {
2769
- return readdirSync6(`/proc/self/fd/${fd}`, { withFileTypes: true });
2908
+ return readdirSync7(`/proc/self/fd/${fd}`, { withFileTypes: true });
2770
2909
  } finally {
2771
2910
  closeSync4(fd);
2772
2911
  }
@@ -2777,7 +2916,7 @@ function readNotebookStateDirectory(path, root) {
2777
2916
  }
2778
2917
  function unlinkStateFile(path, root, allowMissing = false) {
2779
2918
  assertContained(root, path);
2780
- const parentFd = openPinnedDirectory(dirname8(path), root, false);
2919
+ const parentFd = openPinnedDirectory(dirname9(path), root, false);
2781
2920
  const target = pinnedLeaf(parentFd, path);
2782
2921
  let fileFd;
2783
2922
  try {
@@ -2792,7 +2931,7 @@ function unlinkStateFile(path, root, allowMissing = false) {
2792
2931
  throw new NotebookError("CONFLICT", "Refusing to remove a suspect Notebook state entry; run pj notebook audit --json");
2793
2932
  }
2794
2933
  assertOwned(stat);
2795
- unlinkSync5(target);
2934
+ unlinkSync4(target);
2796
2935
  fsyncSync3(parentFd);
2797
2936
  return true;
2798
2937
  } finally {
@@ -2801,8 +2940,8 @@ function unlinkStateFile(path, root, allowMissing = false) {
2801
2940
  }
2802
2941
  }
2803
2942
  function renameStateFile(source, target, root) {
2804
- if (dirname8(source) !== dirname8(target)) throw new NotebookError("INTERNAL_ERROR", "Notebook state rename crossed directories");
2805
- const parentFd = openPinnedDirectory(dirname8(source), root, false);
2943
+ if (dirname9(source) !== dirname9(target)) throw new NotebookError("INTERNAL_ERROR", "Notebook state rename crossed directories");
2944
+ const parentFd = openPinnedDirectory(dirname9(source), root, false);
2806
2945
  const sourcePath = pinnedLeaf(parentFd, source);
2807
2946
  const targetPath = pinnedLeaf(parentFd, target);
2808
2947
  let sourceFd;
@@ -2812,12 +2951,12 @@ function renameStateFile(source, target, root) {
2812
2951
  if (!stat.isFile() || (stat.mode & 511) !== 384) throw new NotebookError("CONFLICT", "Notebook state rename source has an integrity finding");
2813
2952
  assertOwned(stat);
2814
2953
  try {
2815
- lstatSync7(targetPath);
2954
+ lstatSync9(targetPath);
2816
2955
  throw new NotebookError("CONFLICT", "Notebook state rename target already exists");
2817
2956
  } catch (error) {
2818
2957
  if (error.code !== "ENOENT") throw error;
2819
2958
  }
2820
- renameSync4(sourcePath, targetPath);
2959
+ renameSync5(sourcePath, targetPath);
2821
2960
  fsyncSync3(parentFd);
2822
2961
  } finally {
2823
2962
  if (sourceFd !== void 0) closeSync4(sourceFd);
@@ -2826,11 +2965,11 @@ function renameStateFile(source, target, root) {
2826
2965
  }
2827
2966
  function atomicWriteJson(path, value, root, afterParentPinned) {
2828
2967
  assertContained(root, path);
2829
- ensureDirectory(dirname8(path), root);
2968
+ ensureDirectory(dirname9(path), root);
2830
2969
  const text2 = jsonLine(value);
2831
- const parentFd = openPinnedDirectory(dirname8(path), root, false);
2970
+ const parentFd = openPinnedDirectory(dirname9(path), root, false);
2832
2971
  const target = pinnedLeaf(parentFd, path);
2833
- const tempName = `.${basename6(path)}.${process.pid}.${randomUUID3()}.tmp`;
2972
+ const tempName = `.${basename7(path)}.${process.pid}.${randomUUID3()}.tmp`;
2834
2973
  const temp = `/proc/self/fd/${parentFd}/${tempName}`;
2835
2974
  let existingIdentity = null;
2836
2975
  try {
@@ -2857,18 +2996,18 @@ function atomicWriteJson(path, value, root, afterParentPinned) {
2857
2996
  closeSync4(fd);
2858
2997
  }
2859
2998
  try {
2860
- const current = lstatSync7(target);
2999
+ const current = lstatSync9(target);
2861
3000
  if (!existingIdentity || !current.isFile() || current.isSymbolicLink() || current.dev !== existingIdentity.dev || current.ino !== existingIdentity.ino) {
2862
3001
  throw new NotebookError("CONFLICT", "Notebook state target changed during atomic update; preserving both entries for audit");
2863
3002
  }
2864
3003
  } catch (error) {
2865
3004
  if (error.code !== "ENOENT" || existingIdentity) throw error;
2866
3005
  }
2867
- renameSync4(temp, target);
3006
+ renameSync5(temp, target);
2868
3007
  fsyncSync3(parentFd);
2869
3008
  } catch (error) {
2870
3009
  try {
2871
- unlinkSync5(temp);
3010
+ unlinkSync4(temp);
2872
3011
  } catch {
2873
3012
  }
2874
3013
  throw error;
@@ -2879,7 +3018,7 @@ function atomicWriteJson(path, value, root, afterParentPinned) {
2879
3018
  }
2880
3019
  function exclusiveWrite(path, text2, root) {
2881
3020
  assertContained(root, path);
2882
- const parentFd = openPinnedDirectory(dirname8(path), root, false);
3021
+ const parentFd = openPinnedDirectory(dirname9(path), root, false);
2883
3022
  const target = pinnedLeaf(parentFd, path);
2884
3023
  let fd;
2885
3024
  try {
@@ -3036,7 +3175,7 @@ function parseRefusal(value) {
3036
3175
  function safeReadJson(path, maxBytes) {
3037
3176
  let parentFd;
3038
3177
  try {
3039
- parentFd = openPinnedDirectory(dirname8(path), dirname8(path), false);
3178
+ parentFd = openPinnedDirectory(dirname9(path), dirname9(path), false);
3040
3179
  } catch {
3041
3180
  return { reason: "non-regular", bytes: 0 };
3042
3181
  }
@@ -3082,7 +3221,7 @@ function safeEntryId(kind, name) {
3082
3221
  return `${kind}/${/^[a-zA-Z0-9._-]{1,160}$/u.test(name) ? name : sha256Hex(name).slice(0, 24)}`;
3083
3222
  }
3084
3223
  function readBaseline(path, maxBytes) {
3085
- if (!existsSync14(path)) return null;
3224
+ if (!existsSync15(path)) return null;
3086
3225
  const read = safeReadJson(path, maxBytes);
3087
3226
  return read.value === void 0 ? null : parseBaseline(read.value);
3088
3227
  }
@@ -3092,10 +3231,10 @@ function baselineReceiptByteCeiling(limits) {
3092
3231
  function readSessionBaseline(root, projectSlug, sessionKey, limits) {
3093
3232
  assertDigest(sessionKey, "session key");
3094
3233
  const paths = ensureNotebookState(root, projectSlug);
3095
- return readBaseline(join15(paths.baselines, `${sessionKey}.json`), baselineReceiptByteCeiling(limits));
3234
+ return readBaseline(join18(paths.baselines, `${sessionKey}.json`), baselineReceiptByteCeiling(limits));
3096
3235
  }
3097
3236
  function acquireLock(paths, maxWaitMs) {
3098
- const lock = join15(paths.locks, "admission.lock");
3237
+ const lock = join18(paths.locks, "admission.lock");
3099
3238
  const deadline = Date.now() + Math.max(1, maxWaitMs);
3100
3239
  const token = randomUUID3();
3101
3240
  const record = () => ({
@@ -3125,7 +3264,7 @@ function acquireLock(paths, maxWaitMs) {
3125
3264
  throw new NotebookError("CONFLICT", "Notebook state lock has an integrity finding; preserve it and run pj notebook audit --json");
3126
3265
  }
3127
3266
  if (Date.now() >= expiresAt) {
3128
- const recovery = join15(paths.locks, `recovery-${heldToken}.lock`);
3267
+ const recovery = join18(paths.locks, `recovery-${heldToken}.lock`);
3129
3268
  const recoveryRecord = jsonLine({ schema_version: NOTEBOOK_SCHEMA_VERSION, stale_token: heldToken, recovery_token: token, expires_at: new Date(Date.now() + 5e3).toISOString() });
3130
3269
  if (exclusiveWrite(recovery, recoveryRecord, paths.root)) {
3131
3270
  try {
@@ -3133,7 +3272,7 @@ function acquireLock(paths, maxWaitMs) {
3133
3272
  const currentToken = verify.value && typeof verify.value === "object" && !Array.isArray(verify.value) ? verify.value.token : null;
3134
3273
  const currentExpiry = verify.value && typeof verify.value === "object" && !Array.isArray(verify.value) ? Date.parse(String(verify.value.expires_at ?? "")) : Number.NaN;
3135
3274
  if (currentToken === heldToken && Number.isFinite(currentExpiry) && Date.now() >= currentExpiry) {
3136
- const recovered = join15(paths.locks, `.recovered-${heldToken}-${randomUUID3()}.json`);
3275
+ const recovered = join18(paths.locks, `.recovered-${heldToken}-${randomUUID3()}.json`);
3137
3276
  renameStateFile(lock, recovered, paths.root);
3138
3277
  unlinkStateFile(recovered, paths.root);
3139
3278
  }
@@ -3199,7 +3338,7 @@ function scanAuxiliaryState(paths, limits) {
3199
3338
  { kind: "refusals", dir: paths.refusals, suffix: ".json", maxBytes: limits.receipt_max_bytes, parse: parseRefusal, key: (value) => value.session_key }
3200
3339
  ];
3201
3340
  for (const specification of specifications) {
3202
- if (!existsSync14(specification.dir)) continue;
3341
+ if (!existsSync15(specification.dir)) continue;
3203
3342
  let entries;
3204
3343
  try {
3205
3344
  entries = readStateDirectory(specification.dir, paths.root);
@@ -3213,7 +3352,7 @@ function scanAuxiliaryState(paths, limits) {
3213
3352
  addBoundedIntegrity(scan, limits, { entry_id: entryId, reason: "non-regular" });
3214
3353
  continue;
3215
3354
  }
3216
- const read = safeReadJson(join15(specification.dir, entry.name), specification.maxBytes);
3355
+ const read = safeReadJson(join18(specification.dir, entry.name), specification.maxBytes);
3217
3356
  if (read.reason || read.value === void 0) {
3218
3357
  addBoundedIntegrity(scan, limits, { entry_id: entryId, reason: read.reason ?? "invalid-json" }, read.bytes);
3219
3358
  continue;
@@ -3224,7 +3363,7 @@ function scanAuxiliaryState(paths, limits) {
3224
3363
  if (!parsed || expectedName !== entry.name) addBoundedIntegrity(scan, limits, { entry_id: entryId, reason: "invalid-schema" }, read.bytes);
3225
3364
  }
3226
3365
  }
3227
- if (existsSync14(paths.journals)) {
3366
+ if (existsSync15(paths.journals)) {
3228
3367
  let entries;
3229
3368
  try {
3230
3369
  entries = readStateDirectory(paths.journals, paths.root);
@@ -3238,7 +3377,7 @@ function scanAuxiliaryState(paths, limits) {
3238
3377
  addBoundedIntegrity(scan, limits, { entry_id: entryId, reason: "non-regular" });
3239
3378
  continue;
3240
3379
  }
3241
- const read = safeReadJson(join15(paths.journals, entry.name), limits.receipt_max_bytes);
3380
+ const read = safeReadJson(join18(paths.journals, entry.name), limits.receipt_max_bytes);
3242
3381
  if (read.reason || read.value === void 0) {
3243
3382
  addBoundedIntegrity(scan, limits, { entry_id: entryId, reason: read.reason ?? "invalid-json" }, read.bytes);
3244
3383
  continue;
@@ -3253,7 +3392,7 @@ function scanAuxiliaryState(paths, limits) {
3253
3392
  if (reference.summary) scan.unresolvedJournals.push(reference.summary);
3254
3393
  }
3255
3394
  }
3256
- if (existsSync14(paths.locks)) {
3395
+ if (existsSync15(paths.locks)) {
3257
3396
  let entries;
3258
3397
  try {
3259
3398
  entries = readStateDirectory(paths.locks, paths.root);
@@ -3267,7 +3406,7 @@ function scanAuxiliaryState(paths, limits) {
3267
3406
  addBoundedIntegrity(scan, limits, { entry_id: entryId, reason: "non-regular" });
3268
3407
  continue;
3269
3408
  }
3270
- const read = safeReadJson(join15(paths.locks, entry.name), 8192);
3409
+ const read = safeReadJson(join18(paths.locks, entry.name), 8192);
3271
3410
  if (read.reason || read.value === void 0) {
3272
3411
  addBoundedIntegrity(scan, limits, { entry_id: entryId, reason: read.reason ?? "invalid-json" }, read.bytes);
3273
3412
  continue;
@@ -3296,7 +3435,7 @@ function scanReceipts(paths, limits) {
3296
3435
  unresolvedBytes += Math.max(0, knownBytes);
3297
3436
  if (integrity.length < limits.integrity_max_entries) integrity.push(entry);
3298
3437
  };
3299
- if (!existsSync14(paths.receipts)) return { receipts, unresolvedCount, unresolvedBytes, referencedSessions, integrity, integrityCount };
3438
+ if (!existsSync15(paths.receipts)) return { receipts, unresolvedCount, unresolvedBytes, referencedSessions, integrity, integrityCount };
3300
3439
  let entries;
3301
3440
  try {
3302
3441
  entries = readStateDirectory(paths.receipts, paths.root);
@@ -3306,7 +3445,7 @@ function scanReceipts(paths, limits) {
3306
3445
  }
3307
3446
  for (const entry of entries) {
3308
3447
  const entryId = safeEntryId("receipts", entry.name);
3309
- const path = join15(paths.receipts, entry.name);
3448
+ const path = join18(paths.receipts, entry.name);
3310
3449
  if (!entry.isFile() || entry.isSymbolicLink() || !/^[a-f0-9]{64}\.json$/u.test(entry.name)) {
3311
3450
  addIntegrity({ entry_id: entryId, reason: "non-regular" });
3312
3451
  continue;
@@ -3333,10 +3472,10 @@ function scanReceipts(paths, limits) {
3333
3472
  function scanBaselines(paths, nowMs, limits, referenced) {
3334
3473
  let current = 0;
3335
3474
  let stale = 0;
3336
- if (!existsSync14(paths.baselines)) return { current, stale };
3475
+ if (!existsSync15(paths.baselines)) return { current, stale };
3337
3476
  for (const entry of readStateDirectory(paths.baselines, paths.root)) {
3338
3477
  if (!entry.isFile() || !entry.name.endsWith(".json")) continue;
3339
- const baseline = readBaseline(join15(paths.baselines, entry.name), baselineReceiptByteCeiling(limits));
3478
+ const baseline = readBaseline(join18(paths.baselines, entry.name), baselineReceiptByteCeiling(limits));
3340
3479
  if (!baseline || referenced.has(baseline.session_key)) continue;
3341
3480
  const expires = Date.parse(baseline.created_at) + limits.receiptless_session_retention_seconds * 1e3;
3342
3481
  if (nowMs >= expires) stale += 1;
@@ -3346,11 +3485,11 @@ function scanBaselines(paths, nowMs, limits, referenced) {
3346
3485
  }
3347
3486
  function scanRefusals(paths, nowMs, limits, scan) {
3348
3487
  const result2 = [];
3349
- if (!existsSync14(paths.refusals)) return result2;
3488
+ if (!existsSync15(paths.refusals)) return result2;
3350
3489
  const entries = readStateDirectory(paths.refusals, paths.root).sort((a, b) => a.name.localeCompare(b.name, "en")).slice(0, limits.refusal_max_entries);
3351
3490
  for (const entry of entries) {
3352
3491
  if (!entry.isFile() || !entry.name.endsWith(".json")) continue;
3353
- const read = safeReadJson(join15(paths.refusals, entry.name), limits.receipt_max_bytes);
3492
+ const read = safeReadJson(join18(paths.refusals, entry.name), limits.receipt_max_bytes);
3354
3493
  const marker = read.value === void 0 ? null : parseRefusal(read.value);
3355
3494
  if (!marker || `${marker.session_key}.json` !== entry.name) continue;
3356
3495
  if (nowMs >= Date.parse(marker.baseline_created_at) + limits.receiptless_session_retention_seconds * 1e3) continue;
@@ -3433,8 +3572,8 @@ function listCaptureReceipts(root, projectSlug, limits, state) {
3433
3572
  function readCaptureReceipt(root, projectSlug, receiptId, limits) {
3434
3573
  if (!RECEIPT_ID_RE.test(receiptId)) throw new NotebookError("INVALID_INPUT", "Invalid receipt ID");
3435
3574
  const paths = notebookStatePaths(root, projectSlug);
3436
- const path = join15(paths.receipts, `${receiptId}.json`);
3437
- if (!existsSync14(path)) throw new NotebookError("NOT_FOUND", `Capture receipt not found: ${receiptId}`);
3575
+ const path = join18(paths.receipts, `${receiptId}.json`);
3576
+ if (!existsSync15(path)) throw new NotebookError("NOT_FOUND", `Capture receipt not found: ${receiptId}`);
3438
3577
  const read = safeReadJson(path, limits.receipt_max_bytes);
3439
3578
  const receipt = read.value === void 0 ? null : parseReceipt(read.value);
3440
3579
  if (!receipt || receipt.serialized_bytes !== read.bytes) throw new NotebookError("CONFLICT", `Capture receipt has an integrity finding: ${receiptId}`);
@@ -3446,7 +3585,7 @@ function writeReceipt(paths, receipt, limit) {
3446
3585
  const bytes = Buffer.byteLength(text2, "utf8");
3447
3586
  if (bytes > limit) throw new NotebookError("CONFLICT", "Receipt transition exceeds its per-receipt ceiling");
3448
3587
  if (receipt.serialized_bytes === bytes) {
3449
- atomicWriteJson(join15(paths.receipts, `${receipt.receipt_id}.json`), receipt, paths.root);
3588
+ atomicWriteJson(join18(paths.receipts, `${receipt.receipt_id}.json`), receipt, paths.root);
3450
3589
  return receipt;
3451
3590
  }
3452
3591
  receipt.serialized_bytes = bytes;
@@ -3585,25 +3724,25 @@ function pruneNotebookState(root, projectSlug, limits, now = /* @__PURE__ */ new
3585
3724
  const successCutoff = now.getTime() - limits.receipt_succeeded_retention_days * 864e5;
3586
3725
  for (const receipt of scan.receipts) {
3587
3726
  if (receipt.state !== "succeeded" || Date.parse(receipt.updated_at) > successCutoff) continue;
3588
- const path = join15(paths.receipts, `${receipt.receipt_id}.json`);
3727
+ const path = join18(paths.receipts, `${receipt.receipt_id}.json`);
3589
3728
  unlinkStateFile(path, paths.root);
3590
- removed.push(safeEntryId("receipts", basename6(path)));
3729
+ removed.push(safeEntryId("receipts", basename7(path)));
3591
3730
  }
3592
3731
  const remaining = scanReceipts(paths, limits);
3593
3732
  const remainingAuxiliary = scanAuxiliaryState(paths, limits);
3594
3733
  const referenced = /* @__PURE__ */ new Set([...remaining.referencedSessions, ...remainingAuxiliary.referencedSessions]);
3595
3734
  for (const entry of readStateDirectory(paths.baselines, paths.root)) {
3596
3735
  if (!entry.isFile() || !entry.name.endsWith(".json")) continue;
3597
- const path = join15(paths.baselines, entry.name);
3736
+ const path = join18(paths.baselines, entry.name);
3598
3737
  const baseline = readBaseline(path, baselineReceiptByteCeiling(limits));
3599
3738
  if (!baseline || referenced.has(baseline.session_key)) continue;
3600
3739
  if (now.getTime() < Date.parse(baseline.created_at) + limits.receiptless_session_retention_seconds * 1e3) continue;
3601
3740
  unlinkStateFile(path, paths.root);
3602
3741
  removed.push(safeEntryId("baselines", entry.name));
3603
- const claim = join15(paths.claims, `${baseline.session_key}.overview`);
3604
- if (unlinkStateFile(claim, paths.root, true)) removed.push(safeEntryId("claims", basename6(claim)));
3605
- const refusal = join15(paths.refusals, `${baseline.session_key}.json`);
3606
- if (unlinkStateFile(refusal, paths.root, true)) removed.push(safeEntryId("refusals", basename6(refusal)));
3742
+ const claim = join18(paths.claims, `${baseline.session_key}.overview`);
3743
+ if (unlinkStateFile(claim, paths.root, true)) removed.push(safeEntryId("claims", basename7(claim)));
3744
+ const refusal = join18(paths.refusals, `${baseline.session_key}.json`);
3745
+ if (unlinkStateFile(refusal, paths.root, true)) removed.push(safeEntryId("refusals", basename7(refusal)));
3607
3746
  }
3608
3747
  fsyncDirectory2(paths.project);
3609
3748
  return removed;
@@ -3703,9 +3842,9 @@ function parseScopedNoteListItem(value, noteMaxBytes) {
3703
3842
  return parseNoteRecord(value, noteMaxBytes, true);
3704
3843
  }
3705
3844
  function parseNote(value, noteMaxBytes) {
3706
- const note2 = parseNoteRecord(value, noteMaxBytes, false);
3707
- if (note2.content === null) throw new NotebookError("REMOTE_PROTOCOL_ERROR", "Open Notebook returned invalid note content");
3708
- return note2;
3845
+ const note = parseNoteRecord(value, noteMaxBytes, false);
3846
+ if (note.content === null) throw new NotebookError("REMOTE_PROTOCOL_ERROR", "Open Notebook returned invalid note content");
3847
+ return note;
3709
3848
  }
3710
3849
  function errorForStatus(status, message) {
3711
3850
  if (status === 400 || status === 422) return new NotebookError("INVALID_INPUT", message, false, { http_status: status, definitive_rejection: true });
@@ -3831,9 +3970,9 @@ var init_open_notebook_client = __esm({
3831
3970
  }
3832
3971
  async getOwnedNote(notebookId, noteId) {
3833
3972
  const notes = await this.listNotes(notebookId);
3834
- const note2 = notes.find((item) => item.id === noteId);
3835
- if (!note2) throw new NotebookError("NOT_FOUND", `Note is not a proven member of the bound notebook: ${noteId}`);
3836
- return note2;
3973
+ const note = notes.find((item) => item.id === noteId);
3974
+ if (!note) throw new NotebookError("NOT_FOUND", `Note is not a proven member of the bound notebook: ${noteId}`);
3975
+ return note;
3837
3976
  }
3838
3977
  async updateOwnedNote(notebookId, noteId, input) {
3839
3978
  if (input.content !== void 0 && Buffer.byteLength(input.content, "utf8") > this.config.limits.note_max_bytes) throw new NotebookError("INVALID_INPUT", "Note content exceeds the configured ceiling");
@@ -3917,8 +4056,8 @@ var init_open_notebook_client = __esm({
3917
4056
 
3918
4057
  // src/notebook/git-evidence.ts
3919
4058
  import { spawnSync as spawnSync9 } from "node:child_process";
3920
- import { closeSync as closeSync5, constants as constants4, fstatSync as fstatSync3, lstatSync as lstatSync8, openSync as openSync5, readSync as readSync2, realpathSync as realpathSync5 } from "node:fs";
3921
- import { extname, join as join17, relative as relative8, resolve as resolve9, sep as sep4 } from "node:path";
4059
+ import { closeSync as closeSync5, constants as constants4, fstatSync as fstatSync3, lstatSync as lstatSync10, openSync as openSync5, readSync as readSync2, realpathSync as realpathSync6 } from "node:fs";
4060
+ import { extname, join as join20, relative as relative9, resolve as resolve9, sep as sep4 } from "node:path";
3922
4061
  function git(repo, args, maxBuffer = 4 * 1024 * 1024, timeout = 5e3) {
3923
4062
  const result2 = spawnSync9("git", args, { cwd: repo, encoding: "utf8", maxBuffer, timeout, shell: false });
3924
4063
  return { ok: result2.status === 0, stdout: result2.stdout ?? "" };
@@ -3986,19 +4125,19 @@ function validateCommittedGitRef(repoPath, gitRef) {
3986
4125
  }
3987
4126
  function safeRelative(repoPath, relativePath2) {
3988
4127
  if (!relativePath2 || relativePath2.includes("\0") || relativePath2.startsWith("/") || relativePath2.split(/[\\/]/u).includes("..")) return null;
3989
- const root = realpathSync5(repoPath);
4128
+ const root = realpathSync6(repoPath);
3990
4129
  const candidate = resolve9(root, relativePath2);
3991
- const rel = relative8(root, candidate);
4130
+ const rel = relative9(root, candidate);
3992
4131
  if (rel === ".." || rel.startsWith(`..${sep4}`) || rel.startsWith(sep4)) return null;
3993
4132
  return { root, candidate };
3994
4133
  }
3995
4134
  function hasSymlinkComponent(root, candidate) {
3996
- const rel = relative8(root, candidate);
4135
+ const rel = relative9(root, candidate);
3997
4136
  let cursor = root;
3998
4137
  for (const part of rel.split(sep4).filter(Boolean)) {
3999
- cursor = join17(cursor, part);
4138
+ cursor = join20(cursor, part);
4000
4139
  try {
4001
- if (lstatSync8(cursor).isSymbolicLink()) return true;
4140
+ if (lstatSync10(cursor).isSymbolicLink()) return true;
4002
4141
  } catch {
4003
4142
  return true;
4004
4143
  }
@@ -4020,11 +4159,11 @@ function readSafeEvidenceText(repoPath, relativePath2, maxBytes) {
4020
4159
  if (before.size > maxBytes) return { status: "excluded", reason: "oversize" };
4021
4160
  let physical;
4022
4161
  try {
4023
- physical = realpathSync5(`/proc/self/fd/${fd}`);
4162
+ physical = realpathSync6(`/proc/self/fd/${fd}`);
4024
4163
  } catch {
4025
4164
  return { status: "excluded", reason: "unsafe-path" };
4026
4165
  }
4027
- const physicalRel = relative8(safe.root, physical);
4166
+ const physicalRel = relative9(safe.root, physical);
4028
4167
  if (physicalRel === ".." || physicalRel.startsWith(`..${sep4}`) || physicalRel.startsWith(sep4)) return { status: "excluded", reason: "unsafe-path" };
4029
4168
  const chunks = [];
4030
4169
  let total = 0;
@@ -4139,10 +4278,10 @@ var init_git_evidence = __esm({
4139
4278
 
4140
4279
  // src/notebook/remote-mutation-journal.ts
4141
4280
  import { randomUUID as randomUUID4 } from "node:crypto";
4142
- import { join as join18 } from "node:path";
4281
+ import { join as join21 } from "node:path";
4143
4282
  function journalPath(root, projectSlug, operationId) {
4144
4283
  if (!/^[a-f0-9-]{16,64}$/iu.test(operationId)) throw new NotebookError("INVALID_INPUT", "Invalid remote mutation operation ID");
4145
- return join18(ensureNotebookState(root, projectSlug).journals, `${operationId}.json`);
4284
+ return join21(ensureNotebookState(root, projectSlug).journals, `${operationId}.json`);
4146
4285
  }
4147
4286
  function remoteMutationJournalPath(root, projectSlug, operationId) {
4148
4287
  return journalPath(root, projectSlug, operationId);
@@ -4162,7 +4301,7 @@ function listRemoteMutationJournals(root, projectSlug) {
4162
4301
  if (!entry.isFile() || entry.isSymbolicLink() || !/^[a-f0-9-]{16,64}\.json$/iu.test(entry.name)) {
4163
4302
  throw new NotebookError("CONFLICT", `Remote mutation journal state-integrity finding: journals/${entry.name}`);
4164
4303
  }
4165
- const read = readNotebookStateJson(join18(paths.journals, entry.name), paths.root, 65536);
4304
+ const read = readNotebookStateJson(join21(paths.journals, entry.name), paths.root, 65536);
4166
4305
  const journal = read.value === void 0 ? null : parseRemoteMutationJournal(read.value);
4167
4306
  if (!journal || `${journal.operation_id}.json` !== entry.name) {
4168
4307
  throw new NotebookError("CONFLICT", `Remote mutation journal state-integrity finding: journals/${entry.name}`);
@@ -4452,7 +4591,7 @@ async function reconcileManagedNote(input) {
4452
4591
  const reconcile = async () => {
4453
4592
  input.beforeRemote?.();
4454
4593
  const notes = await input.client.listNotes(input.notebookId);
4455
- const candidates2 = notes.filter((note2) => parseNoteEnvelope(note2.content)?.envelope.logical_id === input.logicalId);
4594
+ const candidates2 = notes.filter((note) => parseNoteEnvelope(note.content)?.envelope.logical_id === input.logicalId);
4456
4595
  journal = recordReconciliation({
4457
4596
  stateRoot: input.stateRoot,
4458
4597
  journal,
@@ -4798,10 +4937,10 @@ async function processClaimed(module, receipt, leaseUpdated) {
4798
4937
  content: documentContent,
4799
4938
  beforeRemote: renew
4800
4939
  });
4801
- const note2 = upserted.note;
4940
+ const note = upserted.note;
4802
4941
  if (upserted.journal) journals.push(upserted.journal);
4803
4942
  logicalIds.push(logicalId);
4804
- remoteIds.push(note2.id);
4943
+ remoteIds.push(note.id);
4805
4944
  }
4806
4945
  const summary = summarizeCapture(linked.config, {
4807
4946
  documents: evidence.documents,
@@ -4928,8 +5067,8 @@ var init_capture = __esm({
4928
5067
  });
4929
5068
 
4930
5069
  // src/mcp-server.ts
4931
- import { existsSync as existsSync22, statSync as statSync5 } from "node:fs";
4932
- import { basename as basename8, dirname as dirname14, join as join28, resolve as resolve16 } from "node:path";
5070
+ import { existsSync as existsSync23, statSync as statSync5 } from "node:fs";
5071
+ import { basename as basename9, dirname as dirname14, join as join30, resolve as resolve16 } from "node:path";
4933
5072
  import { fileURLToPath as fileURLToPath8 } from "node:url";
4934
5073
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
4935
5074
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
@@ -5324,21 +5463,21 @@ function selectPackVersion(packDir) {
5324
5463
  }
5325
5464
  function stripTomlComment(line) {
5326
5465
  let out = "";
5327
- let quote = null;
5466
+ let quote2 = null;
5328
5467
  for (let index = 0; index < line.length; index += 1) {
5329
5468
  const ch = line[index];
5330
- if (quote) {
5331
- if (ch === "\\" && quote === '"') {
5469
+ if (quote2) {
5470
+ if (ch === "\\" && quote2 === '"') {
5332
5471
  out += ch + (line[index + 1] ?? "");
5333
5472
  index += 1;
5334
5473
  continue;
5335
5474
  }
5336
- if (ch === quote) quote = null;
5475
+ if (ch === quote2) quote2 = null;
5337
5476
  out += ch;
5338
5477
  continue;
5339
5478
  }
5340
5479
  if (ch === '"' || ch === "'") {
5341
- quote = ch;
5480
+ quote2 = ch;
5342
5481
  out += ch;
5343
5482
  continue;
5344
5483
  }
@@ -5349,15 +5488,15 @@ function stripTomlComment(line) {
5349
5488
  }
5350
5489
  function bracketDepth(text2) {
5351
5490
  let depth = 0;
5352
- let quote = null;
5491
+ let quote2 = null;
5353
5492
  for (let index = 0; index < text2.length; index += 1) {
5354
5493
  const ch = text2[index];
5355
- if (quote) {
5356
- if (ch === "\\" && quote === '"') index += 1;
5357
- else if (ch === quote) quote = null;
5494
+ if (quote2) {
5495
+ if (ch === "\\" && quote2 === '"') index += 1;
5496
+ else if (ch === quote2) quote2 = null;
5358
5497
  continue;
5359
5498
  }
5360
- if (ch === '"' || ch === "'") quote = ch;
5499
+ if (ch === '"' || ch === "'") quote2 = ch;
5361
5500
  else if (ch === "[") depth += 1;
5362
5501
  else if (ch === "]") depth -= 1;
5363
5502
  }
@@ -6033,6 +6172,8 @@ function discoverRoles(repoRoot) {
6033
6172
  ticketProviderIdentifier: yamlGet(text2, "plane.identifier"),
6034
6173
  bloodbankEnabled: yamlGet(text2, "bloodbank.enabled"),
6035
6174
  deploymentSystemd: yamlGet(text2, "deployment.systemd"),
6175
+ serviceStateGateway: yamlGet(text2, "service_state.gateway"),
6176
+ serviceStateHeartbeat: yamlGet(text2, "service_state.heartbeat"),
6036
6177
  legacyReconcileEnabled: yamlGet(text2, "reconcile.enabled"),
6037
6178
  legacyReconcileGraceHours: yamlGet(text2, "reconcile.grace_hours"),
6038
6179
  legacyReconcileAutoReview: yamlGet(text2, "reconcile.auto_review"),
@@ -6920,13 +7061,8 @@ ${block}
6920
7061
  `;
6921
7062
  }
6922
7063
  var BASE_MISE_PATH_ENTRIES = [".mise/scripts", "agents/hermes/pm"];
6923
- var CONDITIONAL_HERMES_PATHS = ["agents/hermes/pm/hermes", "agent/hermes/pm/hermes"];
6924
- function requiredMisePathEntries(ctx) {
6925
- const required = [...BASE_MISE_PATH_ENTRIES];
6926
- for (const candidate of CONDITIONAL_HERMES_PATHS) {
6927
- if (existsSync2(join3(ctx.repoRoot, candidate)) && !required.includes(candidate)) required.push(candidate);
6928
- }
6929
- return required;
7064
+ function requiredMisePathEntries(_ctx) {
7065
+ return [...BASE_MISE_PATH_ENTRIES];
6930
7066
  }
6931
7067
  function upsertMisePath(text2, required = BASE_MISE_PATH_ENTRIES) {
6932
7068
  const render = (values) => `_.path = [${values.map((value) => JSON.stringify(value)).join(", ")}]`;
@@ -7785,6 +7921,94 @@ function checkUnit(unit) {
7785
7921
  const active = systemctlUser(["is-active", unit]).ok;
7786
7922
  return { enabled: enabled2, active };
7787
7923
  }
7924
+ function persistRoleServiceState(role, updates) {
7925
+ try {
7926
+ const stat = lstatSync2(role.roleYamlPath);
7927
+ if (!stat.isFile() || stat.isSymbolicLink()) {
7928
+ return { changed: false, error: `refusing unsafe role manifest ${role.roleYamlPath}` };
7929
+ }
7930
+ const current = readFileSync2(role.roleYamlPath, "utf8");
7931
+ const document = YAML.parseDocument(current);
7932
+ if (document.errors.length) throw document.errors[0];
7933
+ const serviceState = document.get("service_state", true);
7934
+ if (serviceState !== void 0 && serviceState !== null && !YAML.isMap(serviceState)) {
7935
+ return { changed: false, error: `${role.roleYamlPath} service_state must be a YAML mapping` };
7936
+ }
7937
+ for (const [leaf, value] of Object.entries(updates)) {
7938
+ document.setIn(["service_state", leaf], value);
7939
+ }
7940
+ const next = String(document);
7941
+ if (next === current) return { changed: false };
7942
+ const transaction = mkdtempSync(join3(dirname2(role.roleYamlPath), ".pjangler-role-state-"));
7943
+ try {
7944
+ atomicWriteBuffer(
7945
+ role.roleYamlPath,
7946
+ Buffer.from(next),
7947
+ Number(stat.mode) & 511,
7948
+ join3(transaction, "role.yaml")
7949
+ );
7950
+ } finally {
7951
+ rmSync(transaction, { recursive: true, force: true });
7952
+ }
7953
+ return { changed: true };
7954
+ } catch (error) {
7955
+ return { changed: false, error: error instanceof Error ? error.message : String(error) };
7956
+ }
7957
+ }
7958
+ function reconcileHermesRoleUnits(ctx, role, changedFiles, details) {
7959
+ const gatewayUnit = `hermes-${role.agentId}-gateway.service`;
7960
+ const heartbeatUnit = `hermes-${role.agentId}-heartbeat.timer`;
7961
+ const gatewayDeferred = role.serviceStateGateway === "deferred";
7962
+ const stateUpdates = {};
7963
+ if (role.serviceStateHeartbeat !== "active") stateUpdates.heartbeat = "active";
7964
+ if (!gatewayDeferred && role.serviceStateGateway !== "active") stateUpdates.gateway = "active";
7965
+ if (ctx.dryRun) {
7966
+ details.push("would run: systemctl --user daemon-reload");
7967
+ details.push(`would run: systemctl --user enable --now ${heartbeatUnit}`);
7968
+ details.push(`would run: systemctl --user ${gatewayDeferred ? "disable" : "enable"} --now ${gatewayUnit}`);
7969
+ if (Object.keys(stateUpdates).length) {
7970
+ if (!changedFiles.includes(role.roleYamlPath)) changedFiles.push(role.roleYamlPath);
7971
+ details.push(`would atomically record verified service_state in ${relative2(ctx.repoRoot, role.roleYamlPath)}`);
7972
+ }
7973
+ return true;
7974
+ }
7975
+ const reload = systemctlUser(["daemon-reload"]);
7976
+ if (!reload.ok) {
7977
+ details.push(`script failed: systemctl --user daemon-reload: ${reload.stderr || reload.stdout || "unknown error"}`);
7978
+ return false;
7979
+ }
7980
+ const heartbeat = systemctlUser(["enable", "--now", heartbeatUnit]);
7981
+ const gateway = systemctlUser([gatewayDeferred ? "disable" : "enable", "--now", gatewayUnit]);
7982
+ if (!heartbeat.ok) {
7983
+ details.push(`script failed: could not enable ${heartbeatUnit}: ${heartbeat.stderr || heartbeat.stdout || "unknown error"}`);
7984
+ }
7985
+ if (!gateway.ok) {
7986
+ details.push(`script failed: could not ${gatewayDeferred ? "disable" : "enable"} ${gatewayUnit}: ${gateway.stderr || gateway.stdout || "unknown error"}`);
7987
+ }
7988
+ if (!heartbeat.ok || !gateway.ok) return false;
7989
+ const heartbeatState = checkUnit(heartbeatUnit);
7990
+ const gatewayState = checkUnit(gatewayUnit);
7991
+ const heartbeatHealthy = heartbeatState.enabled && heartbeatState.active;
7992
+ const gatewayHealthy = gatewayDeferred ? !gatewayState.enabled && !gatewayState.active : gatewayState.enabled && gatewayState.active;
7993
+ if (!heartbeatHealthy) {
7994
+ details.push(`script failed: ${heartbeatUnit} did not become enabled+active after systemctl reported success`);
7995
+ }
7996
+ if (!gatewayHealthy) {
7997
+ details.push(`script failed: ${gatewayUnit} did not become ${gatewayDeferred ? "disabled+inactive" : "enabled+active"} after systemctl reported success`);
7998
+ }
7999
+ if (!heartbeatHealthy || !gatewayHealthy) return false;
8000
+ const persisted = persistRoleServiceState(role, stateUpdates);
8001
+ if (persisted.error) {
8002
+ details.push(`script failed: could not update ${relative2(ctx.repoRoot, role.roleYamlPath)}: ${persisted.error}`);
8003
+ return false;
8004
+ }
8005
+ if (persisted.changed) {
8006
+ if (!changedFiles.includes(role.roleYamlPath)) changedFiles.push(role.roleYamlPath);
8007
+ details.push(`atomically recorded verified service_state in ${relative2(ctx.repoRoot, role.roleYamlPath)}`);
8008
+ }
8009
+ details.push(`verified ${heartbeatUnit} enabled+active and ${gatewayUnit} ${gatewayDeferred ? "disabled+inactive" : "enabled+active"}`);
8010
+ return true;
8011
+ }
7788
8012
  var BMAD_NPM_PACKAGE = "bmad-method";
7789
8013
  var BMAD_INSTALLER_VERSION = "6.11.1-next.1";
7790
8014
  var BMAD_TARGET_CHANNEL = "next";
@@ -9962,20 +10186,52 @@ function createHermesChecks() {
9962
10186
  }
9963
10187
  const probe = systemctlUser(["is-system-running"]);
9964
10188
  if (!probe.ok && !/running|degraded|starting|maintenance/.test(`${probe.stdout} ${probe.stderr}`)) {
9965
- return { id: "systemd.sentinel", title: "Hermes systemd/sentinel units enabled + active", status: "warn", summary: "systemd --user unavailable; unit state not auditable here", details: [], fixable: false };
10189
+ const sysDir2 = join3(ctx.homeDir, ".config", "systemd", "user");
10190
+ const details2 = [];
10191
+ for (const role of requiredRoles) {
10192
+ const gateway = role.serviceStateGateway || "active";
10193
+ const heartbeat = role.serviceStateHeartbeat || "active";
10194
+ for (const unit of [`hermes-${role.agentId}-gateway.service`, `hermes-${role.agentId}-heartbeat.timer`]) {
10195
+ if (!existsSync2(join3(sysDir2, unit))) details2.push(`${unit} should be installed`);
10196
+ }
10197
+ if (heartbeat !== "installed") details2.push(`${role.agentId} heartbeat should record installed while systemd --user is unavailable (got ${heartbeat})`);
10198
+ if (gateway !== "installed" && gateway !== "deferred") details2.push(`${role.agentId} gateway should record installed or deferred while systemd --user is unavailable (got ${gateway})`);
10199
+ }
10200
+ return {
10201
+ id: "systemd.sentinel",
10202
+ title: "Hermes systemd/sentinel units enabled + active",
10203
+ status: details2.length ? "warn" : "pass",
10204
+ summary: details2.length ? "systemd --user unavailable and installed-state metadata is incomplete" : "Hermes units are installed; activation is deferred because systemd --user is unavailable",
10205
+ details: details2,
10206
+ fixable: false
10207
+ };
9966
10208
  }
9967
10209
  const details = [];
10210
+ const sysDir = join3(ctx.homeDir, ".config", "systemd", "user");
9968
10211
  for (const role of requiredRoles) {
9969
- for (const unit of [`hermes-${role.agentId}-gateway.service`, `hermes-${role.agentId}-heartbeat.timer`]) {
9970
- const state = checkUnit(unit);
9971
- if (!state.enabled || !state.active) details.push(`${unit} should be enabled+active`);
10212
+ const gatewayUnit = `hermes-${role.agentId}-gateway.service`;
10213
+ const heartbeatUnit = `hermes-${role.agentId}-heartbeat.timer`;
10214
+ const gatewayState = role.serviceStateGateway || "active";
10215
+ const heartbeatState = role.serviceStateHeartbeat || "active";
10216
+ for (const unit of [gatewayUnit, heartbeatUnit]) {
10217
+ if (!existsSync2(join3(sysDir, unit))) details.push(`${unit} should be installed`);
10218
+ }
10219
+ const heartbeat = checkUnit(heartbeatUnit);
10220
+ if (heartbeatState !== "active" || !heartbeat.enabled || !heartbeat.active) {
10221
+ details.push(`${heartbeatUnit} should be enabled+active (manifest: ${heartbeatState || "missing"})`);
10222
+ }
10223
+ const gateway = checkUnit(gatewayUnit);
10224
+ if (gatewayState === "deferred") {
10225
+ if (gateway.enabled || gateway.active) details.push(`${gatewayUnit} is deferred and should be disabled+inactive`);
10226
+ } else if (gatewayState !== "active" || !gateway.enabled || !gateway.active) {
10227
+ details.push(`${gatewayUnit} should be enabled+active (manifest: ${gatewayState || "missing"})`);
9972
10228
  }
9973
10229
  }
9974
10230
  return {
9975
10231
  id: "systemd.sentinel",
9976
10232
  title: "Hermes systemd/sentinel units enabled + active",
9977
10233
  status: details.length === 0 ? "pass" : "fail",
9978
- summary: details.length === 0 ? "Hermes user units are enabled and active" : `${details.length} systemd parity issue(s) detected`,
10234
+ summary: details.length === 0 ? "Hermes user units match each role's declared service state" : `${details.length} systemd parity issue(s) detected`,
9979
10235
  details,
9980
10236
  fixable: true
9981
10237
  };
@@ -10000,17 +10256,12 @@ function createHermesChecks() {
10000
10256
  if (text2 === null) return true;
10001
10257
  return text2.includes("/agents/hermes/") && !text2.includes(role.roleDir);
10002
10258
  });
10003
- if (allUnitsPresent && !unitsStale) {
10004
- if (ctx.dryRun) {
10005
- details.push(`would run: systemctl --user enable --now ${units.join(" ")}`);
10006
- } else {
10007
- systemctlUser(["daemon-reload"]);
10008
- for (const unit of units) {
10009
- systemctlUser(["enable", "--now", unit]);
10010
- }
10011
- }
10259
+ const manifestNeedsReconcile = [role.serviceStateGateway, role.serviceStateHeartbeat].some((state) => state === "pending" || state === "error");
10260
+ if (allUnitsPresent && !unitsStale && !manifestNeedsReconcile) {
10261
+ reconcileHermesRoleUnits(ctx, role, changedFiles, details);
10012
10262
  continue;
10013
10263
  }
10264
+ let regenerated = false;
10014
10265
  for (const script of [join3(role.roleDir, ".scripts", "70-systemd.sh")]) {
10015
10266
  if (!existsSync2(script)) {
10016
10267
  details.push(`script failed: missing ${script}`);
@@ -10027,16 +10278,26 @@ function createHermesChecks() {
10027
10278
  if (result2.status !== 0) {
10028
10279
  details.push(`script failed: ${script}: ${result2.stderr.trim() || result2.stdout.trim()}`);
10029
10280
  } else {
10281
+ regenerated = true;
10030
10282
  details.push(`regenerated systemd units for ${role.agentId} from ${role.roleDir}`);
10031
10283
  }
10032
10284
  }
10033
10285
  }
10286
+ if (ctx.dryRun) continue;
10287
+ if (regenerated) {
10288
+ const refreshed = discoverRoles(ctx.repoRoot).find((candidate) => candidate.agentId === role.agentId);
10289
+ if (!refreshed) {
10290
+ details.push(`script failed: regenerated role ${role.agentId} could not be rediscovered`);
10291
+ } else {
10292
+ reconcileHermesRoleUnits(ctx, refreshed, changedFiles, details);
10293
+ }
10294
+ }
10034
10295
  }
10035
10296
  return {
10036
10297
  id: finding2.id,
10037
10298
  title: finding2.title,
10038
10299
  status: details.some((detail) => detail.includes("failed:")) ? "blocked" : details.length ? ctx.dryRun ? "skipped" : "applied" : "noop",
10039
- summary: details.length ? ctx.dryRun ? "Planned systemd remediation commands" : "Attempted systemd remediation" : "No changes required",
10300
+ summary: details.length ? ctx.dryRun ? "Planned systemd remediation commands" : "Reconciled and verified systemd service state" : "No changes required",
10040
10301
  changedFiles,
10041
10302
  details
10042
10303
  };
@@ -10863,8 +11124,21 @@ var DockerRecipe = class extends Recipe {
10863
11124
 
10864
11125
  // src/commands/hermes/EnsureTemplateConfig.ts
10865
11126
  import { homedir as homedir3, platform } from "node:os";
10866
- import { existsSync as existsSync3, mkdirSync as mkdirSync3, writeFileSync as writeFileSync3 } from "node:fs";
10867
- import { join as join4, dirname as dirname3 } from "node:path";
11127
+ import {
11128
+ existsSync as existsSync3,
11129
+ lstatSync as lstatSync3,
11130
+ mkdirSync as mkdirSync3,
11131
+ readFileSync as readFileSync3,
11132
+ realpathSync as realpathSync2,
11133
+ renameSync as renameSync2,
11134
+ rmSync as rmSync2,
11135
+ writeFileSync as writeFileSync3
11136
+ } from "node:fs";
11137
+ import { spawnSync as spawnSync2 } from "node:child_process";
11138
+ import { basename as basename3, join as join4, dirname as dirname3 } from "node:path";
11139
+ var HERMES_GIT_URL = "https://github.com/delorenj/hermes-agent.git";
11140
+ var HERMES_GIT_REF = "main";
11141
+ var HERMES_GIT_SHA = "0408fec7a153e6c32c064acd2b8053917f1525f1";
10868
11142
  function resolveTemplateConfigPath() {
10869
11143
  const fromEnv = process.env.HERMES_TEMPLATE_CONFIG;
10870
11144
  if (fromEnv && fromEnv.trim()) return fromEnv.trim();
@@ -10872,69 +11146,395 @@ function resolveTemplateConfigPath() {
10872
11146
  const base = xdg && xdg.length ? xdg : join4(homedir3(), ".config");
10873
11147
  return join4(base, "hermes-agent-template", "config.toml");
10874
11148
  }
10875
- function detectHermesBin(home) {
11149
+ function quote(value) {
11150
+ return JSON.stringify(value);
11151
+ }
11152
+ function detectHermesInstall(home) {
11153
+ const releaseRoot = join4(home, ".local", "share", "hermes-agent", "releases", HERMES_GIT_SHA);
11154
+ const devRoot = join4(home, "code", "hermes-agent");
10876
11155
  const candidates = [
10877
- join4(home, "code", "hermes-agent", "venv", "bin", "hermes"),
10878
- join4(home, "code", "hermes-agent", ".venv", "bin", "hermes"),
10879
- join4(home, ".local", "bin", "hermes")
11156
+ join4(releaseRoot, ".venv", "bin", "hermes"),
11157
+ join4(home, ".local", "bin", "hermes"),
11158
+ join4(devRoot, "venv", "bin", "hermes"),
11159
+ join4(devRoot, ".venv", "bin", "hermes")
10880
11160
  ];
10881
- for (const c of candidates) {
10882
- if (existsSync3(c)) return c;
11161
+ const bin = candidates.find((candidate) => existsSync3(candidate)) ?? candidates[0];
11162
+ try {
11163
+ const resolved = realpathSync2(bin);
11164
+ const environment = dirname3(dirname3(resolved));
11165
+ if (["venv", ".venv"].includes(environment.split("/").at(-1) ?? "")) {
11166
+ return { bin, repo: dirname3(environment) };
11167
+ }
11168
+ } catch {
10883
11169
  }
10884
- return candidates[0];
11170
+ return { bin, repo: bin.startsWith(devRoot) ? devRoot : releaseRoot };
10885
11171
  }
10886
- function renderHostConfig() {
11172
+ function hostSchema() {
10887
11173
  const home = homedir3();
10888
- const hermesBin = detectHermesBin(home);
10889
- const hermesRepo = join4(home, "code", "hermes-agent");
10890
- const scaffoldDir = join4(home, "code", "hermes-agent-template", "runtime-scaffold");
10891
- const skillsDir = join4(home, ".agents", "skills");
10892
- const pmExternalSkillGlobalDir = join4(home, "code", "skillex", "skill-sets", "global", ".system");
11174
+ const hermes = detectHermesInstall(home);
11175
+ return [
11176
+ {
11177
+ section: "fleet",
11178
+ values: [
11179
+ ["hermes_bin", quote(hermes.bin)],
11180
+ ["hermes_repo", quote(hermes.repo)],
11181
+ ["pjangler_bin", quote("pj")],
11182
+ ["hermes_git_url", quote(HERMES_GIT_URL)],
11183
+ ["hermes_git_ref", quote(HERMES_GIT_REF)],
11184
+ ["hermes_git_sha", quote(HERMES_GIT_SHA)],
11185
+ ["runtime_scaffold_dir", quote(join4(home, "code", "hermes-agent-template", "runtime-scaffold"))],
11186
+ ["fleet_env", quote("~/.hermes/fleet.env")],
11187
+ ["registry_file", quote("~/.hermes/agents-registry.yaml")],
11188
+ ["oauth_file", quote("~/.hermes/auth.json")],
11189
+ ["codex_home", quote("~/.codex")],
11190
+ ["canonical_skills_dir", quote(join4(home, ".agents", "skills"))],
11191
+ ["vox_plugin_name", quote("vox")],
11192
+ ["vox_plugin_dir", quote(join4(home, "code", "voxxy", "plugins", "tts", "vox"))],
11193
+ ["vox_voice", quote("carlin")],
11194
+ ["vox_url", quote("https://vox.delo.sh")],
11195
+ ["onepassword_vault", quote("DeLoSecrets")],
11196
+ ["onepassword_item_prefix", quote("hermes-agent")],
11197
+ [
11198
+ "symlinked_runtime_skills",
11199
+ `[${[
11200
+ "delonet-conventions",
11201
+ "delonet-dotenv",
11202
+ "hermes-pm-template-maintenance",
11203
+ "hindsight",
11204
+ "33god-projects",
11205
+ "subagent-driven-development"
11206
+ ].map(quote).join(", ")}]`
11207
+ ]
11208
+ ]
11209
+ },
11210
+ { section: "github", values: [["runtime_repo_owner", quote("")]] },
11211
+ {
11212
+ section: "plane",
11213
+ values: [
11214
+ ["base", quote("https://plane.delo.sh")],
11215
+ ["workspace", quote("33god")]
11216
+ ]
11217
+ }
11218
+ ];
11219
+ }
11220
+ function renderHostConfig() {
11221
+ const sections = hostSchema().map(({ section: section2, values }) => `[${section2}]
11222
+ ${values.map(([key, value]) => `${key} = ${value}`).join("\n")}`).join("\n\n");
10893
11223
  return `# hermes-agent-template \u2014 host configuration
10894
- # Bootstrapped by \`pjangler config bootstrap\` for $HOME=${home} (platform=${platform()}).
10895
- #
10896
- # [fleet] paths below were derived from THIS machine. The identity values in
10897
- # [github]/[plane]/[bloodbank] are intentionally left to be confirmed before a
10898
- # CLOUD provision (\`pjangler hermes\` without --local); they are unused by the
10899
- # default local-only provision.
10900
- #
10901
- # Resolution precedence per value: env var > ~/.hermes/fleet.env > this file > fallback.
11224
+ # Bootstrapped by \`pj config bootstrap\` for $HOME=${homedir3()} (platform=${platform()}).
11225
+ # Existing values and additional keys are preserved by \`--force\`; it only adds
11226
+ # fields missing from the schema pinned in this pjangler release.
11227
+ # Resolution precedence: env var > ~/.hermes/fleet.env > this file > fallback.
10902
11228
 
10903
- [fleet]
10904
- home = "~/.hermes"
10905
- hermes_bin = "${hermesBin}"
10906
- hermes_repo = "${hermesRepo}"
10907
- runtime_scaffold_dir = "${scaffoldDir}"
10908
- # Shared fleet source-of-truth env file + fleet registry. ~ is expanded.
10909
- fleet_env = "~/.hermes/fleet.env"
10910
- registry_file = "~/.hermes/agents-registry.yaml"
10911
- canonical_skills_dir = "${skillsDir}"
10912
- # BMAD is NOT listed here. "bmad-method install" writes bmad-* skills into each
10913
- # project's own .agents/skills, so an agent sees the version its repo pins
10914
- # rather than one frozen fleet-wide copy. This list is for genuinely shared,
10915
- # out-of-project skill sources only.
10916
- pm_external_skill_dirs = [
10917
- "${pmExternalSkillGlobalDir}",
10918
- ]
10919
- symlinked_runtime_skills = []
10920
-
10921
- [github]
10922
- # Owner of the per-agent runtime repos (creates <owner>/agent-hm-<repo>-<role>).
10923
- # REQUIRED before a cloud provision. Leave empty for local-only runs.
10924
- runtime_repo_owner = ""
11229
+ ${sections}
11230
+ `;
11231
+ }
11232
+ function isEscaped(value, index) {
11233
+ let slashes = 0;
11234
+ for (let cursor = index - 1; cursor >= 0 && value[cursor] === "\\"; cursor -= 1) slashes += 1;
11235
+ return slashes % 2 === 1;
11236
+ }
11237
+ function scanMultilineState(line, initial) {
11238
+ let state = initial;
11239
+ let string;
11240
+ for (let index = 0; index < line.length; ) {
11241
+ if (state) {
11242
+ const marker = state === "basic" ? '"""' : "'''";
11243
+ const close = line.indexOf(marker, index);
11244
+ if (close === -1) return state;
11245
+ if (state === "basic" && isEscaped(line, close)) {
11246
+ index = close + marker.length;
11247
+ continue;
11248
+ }
11249
+ state = void 0;
11250
+ index = close + marker.length;
11251
+ continue;
11252
+ }
11253
+ if (string === "basic") {
11254
+ if (line[index] === '"' && !isEscaped(line, index)) string = void 0;
11255
+ index += 1;
11256
+ continue;
11257
+ }
11258
+ if (string === "literal") {
11259
+ if (line[index] === "'") string = void 0;
11260
+ index += 1;
11261
+ continue;
11262
+ }
11263
+ if (line[index] === "#") break;
11264
+ if (line.startsWith('"""', index)) {
11265
+ state = "basic";
11266
+ index += 3;
11267
+ continue;
11268
+ }
11269
+ if (line.startsWith("'''", index)) {
11270
+ state = "literal";
11271
+ index += 3;
11272
+ continue;
11273
+ }
11274
+ if (line[index] === '"') string = "basic";
11275
+ else if (line[index] === "'") string = "literal";
11276
+ index += 1;
11277
+ }
11278
+ return state;
11279
+ }
11280
+ function parseBasicKey(raw) {
11281
+ try {
11282
+ const parsed = JSON.parse(raw);
11283
+ return typeof parsed === "string" ? parsed : void 0;
11284
+ } catch {
11285
+ return void 0;
11286
+ }
11287
+ }
11288
+ function parseDottedKey(raw) {
11289
+ const path = [];
11290
+ let cursor = 0;
11291
+ const whitespace = () => {
11292
+ while (cursor < raw.length && /[ \t]/.test(raw[cursor])) cursor += 1;
11293
+ };
11294
+ whitespace();
11295
+ while (cursor < raw.length) {
11296
+ let key;
11297
+ if (raw[cursor] === '"') {
11298
+ const start = cursor;
11299
+ cursor += 1;
11300
+ while (cursor < raw.length) {
11301
+ if (raw[cursor] === '"' && !isEscaped(raw, cursor)) {
11302
+ cursor += 1;
11303
+ key = parseBasicKey(raw.slice(start, cursor));
11304
+ break;
11305
+ }
11306
+ cursor += 1;
11307
+ }
11308
+ } else if (raw[cursor] === "'") {
11309
+ const end = raw.indexOf("'", cursor + 1);
11310
+ if (end !== -1) {
11311
+ key = raw.slice(cursor + 1, end);
11312
+ cursor = end + 1;
11313
+ }
11314
+ } else {
11315
+ const match = raw.slice(cursor).match(/^[A-Za-z0-9_-]+/);
11316
+ if (match) {
11317
+ key = match[0];
11318
+ cursor += match[0].length;
11319
+ }
11320
+ }
11321
+ if (key === void 0) return void 0;
11322
+ path.push(key);
11323
+ whitespace();
11324
+ if (cursor === raw.length) return path;
11325
+ if (raw[cursor] !== ".") return void 0;
11326
+ cursor += 1;
11327
+ whitespace();
11328
+ if (cursor === raw.length) return void 0;
11329
+ }
11330
+ return path.length ? path : void 0;
11331
+ }
11332
+ function parseHeaderLine(line) {
11333
+ const text2 = line.trimStart();
11334
+ if (!text2.startsWith("[")) return void 0;
11335
+ const kind = text2.startsWith("[[") ? "array-table" : "table";
11336
+ const openLength = kind === "array-table" ? 2 : 1;
11337
+ const close = kind === "array-table" ? "]]" : "]";
11338
+ let string;
11339
+ let closeAt = -1;
11340
+ for (let cursor = openLength; cursor < text2.length; cursor += 1) {
11341
+ if (string === "basic") {
11342
+ if (text2[cursor] === '"' && !isEscaped(text2, cursor)) string = void 0;
11343
+ continue;
11344
+ }
11345
+ if (string === "literal") {
11346
+ if (text2[cursor] === "'") string = void 0;
11347
+ continue;
11348
+ }
11349
+ if (text2[cursor] === '"') {
11350
+ string = "basic";
11351
+ continue;
11352
+ }
11353
+ if (text2[cursor] === "'") {
11354
+ string = "literal";
11355
+ continue;
11356
+ }
11357
+ if (text2.startsWith(close, cursor)) {
11358
+ closeAt = cursor;
11359
+ break;
11360
+ }
11361
+ }
11362
+ if (closeAt === -1) return void 0;
11363
+ const tail = text2.slice(closeAt + close.length);
11364
+ if (!/^[ \t]*(?:#.*)?$/.test(tail)) return void 0;
11365
+ const path = parseDottedKey(text2.slice(openLength, closeAt));
11366
+ return path ? { kind, path } : void 0;
11367
+ }
11368
+ function parseTomlTableHeaders(source) {
11369
+ const parsed = [];
11370
+ let offset = 0;
11371
+ let multiline;
11372
+ for (const match of source.matchAll(/[^\r\n]*(?:\r\n|\n|\r|$)/g)) {
11373
+ const segment = match[0];
11374
+ if (!segment) break;
11375
+ const line = segment.replace(/(?:\r\n|\n|\r)$/, "");
11376
+ if (!multiline) {
11377
+ const header = parseHeaderLine(line);
11378
+ if (header) {
11379
+ parsed.push({
11380
+ ...header,
11381
+ headerStart: offset,
11382
+ bodyStart: offset + segment.length
11383
+ });
11384
+ }
11385
+ }
11386
+ multiline = scanMultilineState(line, multiline);
11387
+ offset += segment.length;
11388
+ }
11389
+ return parsed.map((header, index) => ({
11390
+ ...header,
11391
+ bodyEnd: parsed[index + 1]?.headerStart ?? source.length
11392
+ }));
11393
+ }
11394
+ function ownedBareKeys(source, table) {
11395
+ const keys = /* @__PURE__ */ new Set();
11396
+ let multiline;
11397
+ const body = source.slice(table.bodyStart, table.bodyEnd);
11398
+ for (const match of body.matchAll(/[^\r\n]*(?:\r\n|\n|\r|$)/g)) {
11399
+ const segment = match[0];
11400
+ if (!segment) break;
11401
+ const line = segment.replace(/(?:\r\n|\n|\r)$/, "");
11402
+ if (!multiline) {
11403
+ const assignment = line.match(/^\s*((?:"(?:\\.|[^"\\])*"|'[^']*'|[A-Za-z0-9_-]+))\s*=/);
11404
+ if (assignment) {
11405
+ const path = parseDottedKey(assignment[1]);
11406
+ if (path?.length === 1) keys.add(path[0]);
11407
+ }
11408
+ }
11409
+ multiline = scanMultilineState(line, multiline);
11410
+ }
11411
+ return keys;
11412
+ }
11413
+ var TOMLLIB_VALIDATE = String.raw`
11414
+ import sys
10925
11415
 
10926
- [plane]
10927
- # Plane instance + workspace (one project per agent). Confirm before cloud provision.
10928
- base = "https://plane.delo.sh"
10929
- workspace = "33god"
11416
+ try:
11417
+ import tomllib
11418
+ except Exception as exc:
11419
+ sys.stderr.write("TOMLLIB_UNAVAILABLE:" + repr(exc))
11420
+ raise SystemExit(2)
10930
11421
 
10931
- [bloodbank]
10932
- # NATS endpoint the consumer connects to. For a remote fleet node, point this at
10933
- # the bloodbank host over Tailscale rather than localhost.
10934
- nats_host = "127.0.0.1"
10935
- nats_port = 4222
10936
- compose_dir = "~/code/33GOD/bloodbank"
11422
+ try:
11423
+ source = sys.stdin.buffer.read().decode("utf-8", errors="strict")
11424
+ tomllib.loads(source)
11425
+ except (UnicodeDecodeError, tomllib.TOMLDecodeError) as exc:
11426
+ sys.stderr.write("TOML_INVALID:" + exc.__class__.__name__ + ": " + str(exc))
11427
+ raise SystemExit(1)
11428
+ `;
11429
+ var TOMLLIB_VALIDATION_TIMEOUT_MS = 5e3;
11430
+ function isolatedPythonEnvironment() {
11431
+ return Object.fromEntries(
11432
+ Object.entries(process.env).filter(([name]) => !name.toUpperCase().startsWith("PYTHON"))
11433
+ );
11434
+ }
11435
+ function validateTomlBytes(source, label) {
11436
+ const validation = spawnSync2("python3", ["-I", "-S", "-c", TOMLLIB_VALIDATE], {
11437
+ input: source,
11438
+ encoding: "utf8",
11439
+ env: isolatedPythonEnvironment(),
11440
+ maxBuffer: 1024 * 1024,
11441
+ timeout: TOMLLIB_VALIDATION_TIMEOUT_MS,
11442
+ killSignal: "SIGKILL"
11443
+ });
11444
+ if (validation.error) {
11445
+ const code = validation.error.code;
11446
+ if (code === "ETIMEDOUT") {
11447
+ throw new Error(`${label} validation timed out after ${TOMLLIB_VALIDATION_TIMEOUT_MS}ms`);
11448
+ }
11449
+ throw new Error(
11450
+ code === "ENOENT" ? `${label} cannot be validated: python3 with tomllib is required but was not found` : `${label} validation failed to start: ${validation.error.message}`
11451
+ );
11452
+ }
11453
+ if (validation.status === 2 || validation.stderr.startsWith("TOMLLIB_UNAVAILABLE:")) {
11454
+ throw new Error(
11455
+ `${label} cannot be validated: python3 with tomllib is required (${validation.stderr.replace(/^TOMLLIB_UNAVAILABLE:/, "")})`
11456
+ );
11457
+ }
11458
+ if (validation.status !== 0) {
11459
+ const detail = validation.stderr.replace(/^TOML_INVALID:/, "").trim() || `python3 exited ${validation.status ?? "without a status"}`;
11460
+ throw new Error(`${label} is not valid TOML 1.0 for Python tomllib: ${detail}`);
11461
+ }
11462
+ }
11463
+ function assertValidToml(source, label) {
11464
+ validateTomlBytes(Buffer.from(source, "utf8"), label);
11465
+ }
11466
+ function mergeHostConfig(existingBytes) {
11467
+ validateTomlBytes(existingBytes, "Existing Hermes template config");
11468
+ const existing = existingBytes.toString("utf8");
11469
+ if (!Buffer.from(existing, "utf8").equals(existingBytes)) {
11470
+ throw new Error("Existing Hermes template config is not valid UTF-8");
11471
+ }
11472
+ let merged = existing;
11473
+ for (const { section: section2, values } of hostSchema()) {
11474
+ const tables = parseTomlTableHeaders(merged);
11475
+ const matching = tables.filter((table2) => table2.kind === "table" && table2.path.length === 1 && table2.path[0] === section2);
11476
+ if (matching.length > 1) {
11477
+ throw new Error(`Cannot merge [${section2}]: config contains duplicate table headers`);
11478
+ }
11479
+ const table = matching[0];
11480
+ if (!table) {
11481
+ const incompatible = tables.find((candidate) => candidate.path[0] === section2);
11482
+ if (incompatible) {
11483
+ throw new Error(`Cannot merge [${section2}]: config defines ${incompatible.kind === "array-table" ? "an array table" : "a child table"} at [${incompatible.path.join(".")}] without an owning [${section2}] table`);
11484
+ }
11485
+ const prefix = merged.length === 0 ? "" : merged.endsWith("\n") ? "\n" : "\n\n";
11486
+ merged += `${prefix}[${section2}]
11487
+ ${values.map(([key, value]) => `${key} = ${value}`).join("\n")}
10937
11488
  `;
11489
+ continue;
11490
+ }
11491
+ const existingKeys = ownedBareKeys(merged, table);
11492
+ const missing = values.filter(([key]) => !existingKeys.has(key));
11493
+ if (!missing.length) continue;
11494
+ const body = merged.slice(table.bodyStart, table.bodyEnd);
11495
+ const addition = `${body.length === 0 || body.endsWith("\n") ? "" : "\n"}${missing.map(([key, value]) => `${key} = ${value}`).join("\n")}
11496
+ `;
11497
+ merged = `${merged.slice(0, table.bodyEnd)}${addition}${merged.slice(table.bodyEnd)}`;
11498
+ }
11499
+ assertValidToml(merged, "Merged Hermes template config");
11500
+ return merged;
11501
+ }
11502
+ function describePathType(stats) {
11503
+ if (stats.isSymbolicLink()) return "symbolic link";
11504
+ if (stats.isDirectory()) return "directory";
11505
+ if (stats.isFIFO()) return "FIFO";
11506
+ if (stats.isSocket()) return "socket";
11507
+ if (stats.isCharacterDevice()) return "character device";
11508
+ if (stats.isBlockDevice()) return "block device";
11509
+ return "non-regular file";
11510
+ }
11511
+ function inspectConfigPath(path) {
11512
+ let stats;
11513
+ try {
11514
+ stats = lstatSync3(path);
11515
+ } catch (error) {
11516
+ if (error.code === "ENOENT") return void 0;
11517
+ throw error;
11518
+ }
11519
+ if (!stats.isFile() || stats.isSymbolicLink()) {
11520
+ throw new Error(`Unsafe Hermes template config path ${path}: expected a regular file, found ${describePathType(stats)}`);
11521
+ }
11522
+ return stats;
11523
+ }
11524
+ function installConfig(path, next, previous) {
11525
+ const dir = dirname3(path);
11526
+ mkdirSync3(dir, { recursive: true });
11527
+ const tmp = join4(dir, `.${basename3(path)}.pjangler-${process.pid}.tmp`);
11528
+ try {
11529
+ writeFileSync3(tmp, next, { mode: previous ? previous.mode & 511 : 384 });
11530
+ renameSync2(tmp, path);
11531
+ } catch (error) {
11532
+ try {
11533
+ rmSync2(tmp, { force: true });
11534
+ } catch {
11535
+ }
11536
+ throw error;
11537
+ }
10938
11538
  }
10939
11539
  var EnsureTemplateConfig = class extends Command {
10940
11540
  async invoke() {
@@ -10948,33 +11548,75 @@ var EnsureTemplateConfig = class extends Command {
10948
11548
  }
10949
11549
  const force = ctx.forceConfig === true || process.env.PJANGLER_FORCE_CONFIG === "1";
10950
11550
  const path = resolveTemplateConfigPath();
10951
- const exists = existsSync3(path);
11551
+ let stats;
11552
+ try {
11553
+ stats = inspectConfigPath(path);
11554
+ } catch (error) {
11555
+ return {
11556
+ success: false,
11557
+ outcome: "failed",
11558
+ message: `Failed to inspect ${path}: ${error instanceof Error ? error.message : String(error)}`
11559
+ };
11560
+ }
11561
+ const exists = stats !== void 0;
10952
11562
  if (exists && !force) {
10953
11563
  if (!ctx.quiet) console.log(`\u2713 Config present: ${path}`);
10954
- return { success: true, outcome: "unchanged", message: "" };
11564
+ return { success: true, outcome: "unchanged", message: `Config present: ${path}` };
11565
+ }
11566
+ let next = renderHostConfig();
11567
+ let current = "";
11568
+ try {
11569
+ if (exists) {
11570
+ const bytes = readFileSync3(path);
11571
+ next = mergeHostConfig(bytes);
11572
+ current = bytes.toString("utf8");
11573
+ } else {
11574
+ assertValidToml(next, "Rendered Hermes template config");
11575
+ }
11576
+ } catch (error) {
11577
+ return {
11578
+ success: false,
11579
+ outcome: "failed",
11580
+ message: `Failed to prepare ${path}; no changes were applied: ${error instanceof Error ? error.message : String(error)}`
11581
+ };
11582
+ }
11583
+ if (exists && next === current) {
11584
+ return { success: true, outcome: "unchanged", message: `Config schema already current: ${path}` };
10955
11585
  }
10956
11586
  if (ctx.dryRun) {
10957
- if (!ctx.quiet) console.log(`[DRY RUN] Would ${exists ? "overwrite" : "create"} config: ${path}`);
10958
- return { success: true, outcome: "planned", filePath: path, message: "" };
11587
+ if (!ctx.quiet) console.log(`[DRY RUN] Would ${exists ? "merge missing schema fields into" : "create"} config: ${path}`);
11588
+ return {
11589
+ success: true,
11590
+ outcome: "planned",
11591
+ filePath: path,
11592
+ message: `[DRY RUN] Would ${exists ? "merge missing schema fields into" : "create"} config: ${path}`
11593
+ };
10959
11594
  }
10960
11595
  try {
10961
- mkdirSync3(dirname3(path), { recursive: true });
10962
- writeFileSync3(path, renderHostConfig());
10963
- } catch (err) {
10964
- const msg = err instanceof Error ? err.message : String(err);
10965
- return { success: false, outcome: "failed", message: `Failed to write ${path}: ${msg}` };
11596
+ installConfig(path, next, stats);
11597
+ } catch (error) {
11598
+ return {
11599
+ success: false,
11600
+ outcome: "failed",
11601
+ message: `Failed to write ${path}: ${error instanceof Error ? error.message : String(error)}`
11602
+ };
10966
11603
  }
10967
11604
  if (!ctx.quiet) {
10968
- console.log(`\u2713 Bootstrapped config: ${path}`);
10969
- console.log(" Review [github].runtime_repo_owner + [plane] + [bloodbank] before a cloud provision.");
11605
+ console.log(`\u2713 ${exists ? "Updated" : "Bootstrapped"} config: ${path}`);
11606
+ if (!exists) console.log(" Review [github].runtime_repo_owner + [plane] + [bloodbank] before a cloud provision.");
10970
11607
  }
10971
- return { success: true, outcome: "changed", filePath: path, message: "" };
11608
+ return {
11609
+ success: true,
11610
+ outcome: "changed",
11611
+ filePath: path,
11612
+ message: `${exists ? "Updated" : "Bootstrapped"} config without replacing existing values: ${path}`
11613
+ };
10972
11614
  }
10973
11615
  };
10974
11616
 
10975
11617
  // src/commands/hermes/PromptForAgentConfig.ts
10976
- import { basename as basename3, join as join5 } from "node:path";
10977
- import { readFileSync as readFileSync3 } from "node:fs";
11618
+ import { basename as basename4, join as join5 } from "node:path";
11619
+ import { readFileSync as readFileSync4 } from "node:fs";
10978
11620
  import * as p from "@clack/prompts";
10979
11621
 
10980
11622
  // src/commands/hermes/types.ts
@@ -10989,7 +11631,7 @@ function deriveProfileName(repo, role) {
10989
11631
  // src/commands/hermes/PromptForAgentConfig.ts
10990
11632
  function detectTicketProvider(targetDir) {
10991
11633
  try {
10992
- const t = JSON.parse(readFileSync3(join5(targetDir, ".project.json"), "utf8"))?.ticket_provider?.type;
11634
+ const t = JSON.parse(readFileSync4(join5(targetDir, ".project.json"), "utf8"))?.ticket_provider?.type;
10993
11635
  return t === "plane" || t === "trello" ? t : void 0;
10994
11636
  } catch {
10995
11637
  return void 0;
@@ -10998,7 +11640,7 @@ function detectTicketProvider(targetDir) {
10998
11640
  var PromptForAgentConfig = class extends Command {
10999
11641
  async invoke() {
11000
11642
  const ctx = this.context;
11001
- const defaultRepo = basename3(ctx.targetDir).toLowerCase();
11643
+ const defaultRepo = basename4(ctx.targetDir).toLowerCase();
11002
11644
  ctx.targetRepo = (ctx.targetRepo ?? defaultRepo).toLowerCase();
11003
11645
  ctx.role ??= "pm";
11004
11646
  ctx.agentPurpose ??= `${ctx.role} agent for ${ctx.targetRepo}`;
@@ -11054,12 +11696,73 @@ var PromptForAgentConfig = class extends Command {
11054
11696
  }
11055
11697
  };
11056
11698
 
11699
+ // src/commands/hermes/ValidateHermesOptions.ts
11700
+ import { lstatSync as lstatSync7, readdirSync as readdirSync5 } from "node:fs";
11701
+ import { join as join10, relative as relative6 } from "node:path";
11702
+ init_project();
11703
+ var EMAIL_UNSUPPORTED_MESSAGE = "Email provisioning is unavailable: the pinned Hermes template has no supported email provisioner. Omit --email; no files or external state were changed.";
11704
+ var HARMLESS_ROLE_PLACEHOLDERS = /* @__PURE__ */ new Set([".gitkeep", ".DS_Store", "Thumbs.db"]);
11705
+ function existingRoleDirectoryBlockers(roleDir) {
11706
+ let root;
11707
+ try {
11708
+ root = lstatSync7(roleDir);
11709
+ } catch (error) {
11710
+ if (error.code === "ENOENT") return [];
11711
+ throw error;
11712
+ }
11713
+ if (!root.isDirectory() || root.isSymbolicLink()) return ["<target is not a real directory>"];
11714
+ const blockers = [];
11715
+ const visit = (directory) => {
11716
+ for (const entry of readdirSync5(directory, { withFileTypes: true })) {
11717
+ const path = join10(directory, entry.name);
11718
+ const stats = lstatSync7(path);
11719
+ if (stats.isDirectory() && !stats.isSymbolicLink()) {
11720
+ visit(path);
11721
+ } else if (!HARMLESS_ROLE_PLACEHOLDERS.has(entry.name) || !stats.isFile() || stats.isSymbolicLink() || stats.nlink !== 1) {
11722
+ blockers.push(relative6(roleDir, path));
11723
+ }
11724
+ }
11725
+ };
11726
+ visit(roleDir);
11727
+ return blockers.sort();
11728
+ }
11729
+ function existingRoleRefusal(roleDir) {
11730
+ const blockers = existingRoleDirectoryBlockers(roleDir);
11731
+ if (!blockers.length) return void 0;
11732
+ const sample = blockers.slice(0, 3).join(", ");
11733
+ const remainder = blockers.length > 3 ? ` and ${blockers.length - 3} more` : "";
11734
+ return `Hermes target directory is not empty at ${roleDir} (found ${sample}${remainder}); non-interactive mode will not render into it. Re-run with --force to re-render explicitly.`;
11735
+ }
11736
+ var ValidateHermesOptions = class extends Command {
11737
+ async invoke() {
11738
+ const ctx = this.context;
11739
+ if (ctx.skipEmail === false) {
11740
+ return { success: false, outcome: "failed", message: EMAIL_UNSUPPORTED_MESSAGE };
11741
+ }
11742
+ const role = normalizeAgentRole(ctx.role ?? "pm");
11743
+ const roleDir = resolveContainedPath(
11744
+ ctx.targetDir,
11745
+ join10(ctx.targetDir, "agents", "hermes", role),
11746
+ "Hermes role directory"
11747
+ );
11748
+ const refusal = (ctx.yes || ctx.quiet) && !ctx.force ? existingRoleRefusal(roleDir) : void 0;
11749
+ if (refusal) {
11750
+ return {
11751
+ success: false,
11752
+ outcome: "failed",
11753
+ message: refusal
11754
+ };
11755
+ }
11756
+ return { success: true, outcome: "unchanged", message: "" };
11757
+ }
11758
+ };
11759
+
11057
11760
  // src/commands/hermes/RunCopierTemplate.ts
11058
- import { spawnSync as spawnSync3 } from "node:child_process";
11761
+ import { spawnSync as spawnSync4 } from "node:child_process";
11059
11762
  import { homedir as homedir5 } from "node:os";
11060
- import { join as join9, dirname as dirname6, relative as relative6 } from "node:path";
11061
- import { existsSync as existsSync7, mkdirSync as mkdirSync5, readFileSync as readFileSync7, writeFileSync as writeFileSync5 } from "node:fs";
11062
- import { fileURLToPath as fileURLToPath3 } from "node:url";
11763
+ import { join as join11, dirname as dirname7, relative as relative7 } from "node:path";
11764
+ import { existsSync as existsSync7, mkdirSync as mkdirSync5, readFileSync as readFileSync9, writeFileSync as writeFileSync5 } from "node:fs";
11765
+ import { fileURLToPath as fileURLToPath4 } from "node:url";
11063
11766
  import * as p2 from "@clack/prompts";
11064
11767
  import YAML4 from "yaml";
11065
11768
  init_project();
@@ -11089,9 +11792,9 @@ function scrubInteractiveChannelCredentials(env2) {
11089
11792
  delete env2.WIRE_SLACK;
11090
11793
  }
11091
11794
  function registerRenderedAgent(ctx, roleDir, role) {
11092
- const manifestPath = join9(ctx.targetDir, ".project.json");
11795
+ const manifestPath = join11(ctx.targetDir, ".project.json");
11093
11796
  if (!existsSync7(manifestPath) || !ctx.targetRepo) return;
11094
- const current = readFileSync7(manifestPath, "utf8");
11797
+ const current = readFileSync9(manifestPath, "utf8");
11095
11798
  const parsed = JSON.parse(current);
11096
11799
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
11097
11800
  throw new Error(`${manifestPath} must contain a JSON object`);
@@ -11106,7 +11809,7 @@ function registerRenderedAgent(ctx, roleDir, role) {
11106
11809
  Object.defineProperty(agents, agentId, {
11107
11810
  value: {
11108
11811
  role,
11109
- role_dir: relative6(ctx.targetDir, roleDir),
11812
+ role_dir: relative7(ctx.targetDir, roleDir),
11110
11813
  provisioning_state: "provisioned"
11111
11814
  },
11112
11815
  configurable: true,
@@ -11121,14 +11824,14 @@ function registerRenderedAgent(ctx, roleDir, role) {
11121
11824
  function resolveVendoredTemplate(name) {
11122
11825
  let dir;
11123
11826
  try {
11124
- dir = dirname6(fileURLToPath3(import.meta.url));
11827
+ dir = dirname7(fileURLToPath4(import.meta.url));
11125
11828
  } catch {
11126
11829
  return void 0;
11127
11830
  }
11128
11831
  for (let i = 0; i < 8; i++) {
11129
- const candidate = join9(dir, "templates", name);
11130
- if (existsSync7(join9(candidate, "copier.yml"))) return candidate;
11131
- const parent = dirname6(dir);
11832
+ const candidate = join11(dir, "templates", name);
11833
+ if (existsSync7(join11(candidate, "copier.yml"))) return candidate;
11834
+ const parent = dirname7(dir);
11132
11835
  if (parent === dir) break;
11133
11836
  dir = parent;
11134
11837
  }
@@ -11160,11 +11863,14 @@ var RunCopierTemplate = class extends Command {
11160
11863
  ctx.role = safeRole;
11161
11864
  const roleDir = resolveContainedPath(
11162
11865
  ctx.targetDir,
11163
- join9(ctx.targetDir, "agents", "hermes", safeRole),
11866
+ join11(ctx.targetDir, "agents", "hermes", safeRole),
11164
11867
  "Hermes role directory"
11165
11868
  );
11166
11869
  ctx.roleDir = roleDir;
11167
- ctx.runtimeRepo = `delorenj/agent-hm-${targetRepo}-${safeRole}`;
11870
+ const nonInteractiveRefusal = (ctx.yes || ctx.quiet) && !ctx.force ? existingRoleRefusal(roleDir) : void 0;
11871
+ if (nonInteractiveRefusal) {
11872
+ return { success: false, outcome: "failed", message: nonInteractiveRefusal };
11873
+ }
11168
11874
  const trustedCopierRequired = Boolean(ctx.deferredExternalEffects && !ctx.dryRun);
11169
11875
  if (trustedCopierRequired && !ctx.trustedCopier) {
11170
11876
  return {
@@ -11174,7 +11880,7 @@ var RunCopierTemplate = class extends Command {
11174
11880
  };
11175
11881
  }
11176
11882
  if (!ctx.trustedCopier) {
11177
- const which = spawnSync3("which", ["copier"], { encoding: "utf8" });
11883
+ const which = spawnSync4("which", ["copier"], { encoding: "utf8" });
11178
11884
  if (which.status !== 0) {
11179
11885
  return {
11180
11886
  success: false,
@@ -11183,10 +11889,8 @@ var RunCopierTemplate = class extends Command {
11183
11889
  };
11184
11890
  }
11185
11891
  }
11186
- if (existsSync7(join9(roleDir, "role.yaml")) && !ctx.force) {
11187
- if (ctx.yes) {
11188
- ctx.force = true;
11189
- } else {
11892
+ if (existsSync7(join11(roleDir, "role.yaml")) && !ctx.force) {
11893
+ if (!ctx.yes && !ctx.quiet) {
11190
11894
  const proceed = await p2.confirm({
11191
11895
  message: `${safeRole}/role.yaml already exists \u2014 re-render with --overwrite?`,
11192
11896
  initialValue: false
@@ -11214,9 +11918,10 @@ var RunCopierTemplate = class extends Command {
11214
11918
  // renders repo-local files first and executes those scripts only after a
11215
11919
  // structural lifecycle gate has accepted the render.
11216
11920
  SKIP_HOST_STATE: ctx.deferredExternalEffects ? "1" : "0",
11217
- // Bloodbank is a fleet-shared Hermes gateway. Never provision the legacy
11218
- // per-profile file consumer, even when an older template still exposes it.
11219
- SKIP_RUNTIME_REPO: ctx.deferredExternalEffects ? "1" : ctx.skipRuntimeRepo ? "1" : "0",
11921
+ // The legacy script/env name now owns only ignored role-local runtime
11922
+ // setup. Structured transactions defer it until rendered eligibility;
11923
+ // ordinary CLI deploys always run it and no flag can create a remote repo.
11924
+ SKIP_RUNTIME_REPO: ctx.deferredExternalEffects ? "1" : "0",
11220
11925
  SKIP_PLANE: ctx.deferredExternalEffects ? "1" : ctx.skipPlane ? "1" : "0",
11221
11926
  SKIP_BLOODBANK: "1",
11222
11927
  SKIP_SYSTEMD: ctx.deferredExternalEffects ? "1" : ctx.skipSystemd ? "1" : "0"
@@ -11229,9 +11934,9 @@ var RunCopierTemplate = class extends Command {
11229
11934
  env2.PYTHONNOUSERSITE = "1";
11230
11935
  env2.PYTHONSAFEPATH = "1";
11231
11936
  }
11232
- const LOCAL_TEMPLATE = join9(homedir5(), "code", "hermes-agent-template");
11937
+ const LOCAL_TEMPLATE = join11(homedir5(), "code", "hermes-agent-template");
11233
11938
  const vendored = resolveVendoredTemplate("hermes-agent");
11234
- const templateSrc = process.env.PJANGLER_HERMES_TEMPLATE || vendored || (existsSync7(join9(LOCAL_TEMPLATE, "copier.yml")) ? LOCAL_TEMPLATE : HERMES_AGENT_TEMPLATE);
11939
+ const templateSrc = process.env.PJANGLER_HERMES_TEMPLATE || vendored || (existsSync7(join11(LOCAL_TEMPLATE, "copier.yml")) ? LOCAL_TEMPLATE : HERMES_AGENT_TEMPLATE);
11235
11940
  const args = [
11236
11941
  "copy",
11237
11942
  templateSrc,
@@ -11261,6 +11966,7 @@ var RunCopierTemplate = class extends Command {
11261
11966
  "--trust",
11262
11967
  "--vcs-ref=HEAD"
11263
11968
  ];
11969
+ if (ctx.yes || ctx.quiet) args.push("--defaults");
11264
11970
  if (ctx.force) args.push("--overwrite");
11265
11971
  if (ctx.dryRun) {
11266
11972
  return {
@@ -11280,12 +11986,12 @@ var RunCopierTemplate = class extends Command {
11280
11986
  };
11281
11987
  }
11282
11988
  }
11283
- mkdirSync5(join9(ctx.targetDir, "agents", "hermes"), { recursive: true });
11284
- const spinner4 = ctx.quiet ? void 0 : p2.spinner();
11285
- spinner4?.start(`Running copier copy (target: agents/hermes/${safeRole})`);
11989
+ mkdirSync5(join11(ctx.targetDir, "agents", "hermes"), { recursive: true });
11990
+ const spinner3 = ctx.quiet ? void 0 : p2.spinner();
11991
+ spinner3?.start(`Running copier copy (target: agents/hermes/${safeRole})`);
11286
11992
  const copierExecutable = ctx.trustedCopier?.executable ?? "copier";
11287
- const result2 = spawnSync3(copierExecutable, args, ctx.quiet ? { encoding: "utf8", env: env2, cwd: ctx.targetDir } : { stdio: "inherit", env: env2, cwd: ctx.targetDir });
11288
- spinner4?.stop(result2.status === 0 ? "\u2713 copier run complete" : "\u2717 copier failed");
11993
+ const result2 = spawnSync4(copierExecutable, args, ctx.quiet ? { encoding: "utf8", env: env2, cwd: ctx.targetDir } : { stdio: "inherit", env: env2, cwd: ctx.targetDir });
11994
+ spinner3?.stop(result2.status === 0 ? "\u2713 copier run complete" : "\u2717 copier failed");
11289
11995
  if (result2.status !== 0) {
11290
11996
  return {
11291
11997
  success: false,
@@ -11293,9 +11999,9 @@ var RunCopierTemplate = class extends Command {
11293
11999
  message: `copier exited with status ${result2.status}.${ctx.quiet && String(result2.stderr ?? "").trim() ? ` ${String(result2.stderr).trim()}` : " Check the output above; re-run with the same flags after fixing."}`
11294
12000
  };
11295
12001
  }
11296
- const roleManifest = join9(roleDir, "role.yaml");
12002
+ const roleManifest = join11(roleDir, "role.yaml");
11297
12003
  try {
11298
- const current = readFileSync7(roleManifest, "utf8");
12004
+ const current = readFileSync9(roleManifest, "utf8");
11299
12005
  const document = YAML4.parseDocument(current);
11300
12006
  if (document.errors.length) throw document.errors[0];
11301
12007
  document.setIn(["deployment", "local_only"], ctx.deferredExternalEffects ? true : Boolean(ctx.local));
@@ -11312,15 +12018,16 @@ var RunCopierTemplate = class extends Command {
11312
12018
  }
11313
12019
  return {
11314
12020
  success: true,
11315
- message: `\u2713 Provisioned ${roleDir} (runtime: gh:${ctx.runtimeRepo})`
12021
+ outcome: "changed",
12022
+ message: `Rendered Hermes role at ${roleDir}; lifecycle postconditions are pending`
11316
12023
  };
11317
12024
  }
11318
12025
  };
11319
12026
 
11320
12027
  // src/commands/hermes/UntrackHermesRuntimes.ts
11321
- import { existsSync as existsSync8, readFileSync as readFileSync8, writeFileSync as writeFileSync6, readdirSync as readdirSync5 } from "fs";
11322
- import { join as join10 } from "path";
11323
- import { spawnSync as spawnSync4 } from "node:child_process";
12028
+ import { existsSync as existsSync8, readFileSync as readFileSync10, writeFileSync as writeFileSync6, readdirSync as readdirSync6 } from "fs";
12029
+ import { join as join12 } from "path";
12030
+ import { spawnSync as spawnSync5 } from "node:child_process";
11324
12031
  function sectionHasPath(section2, targetPath) {
11325
12032
  return section2.split(/\r?\n/).some((line) => /^\s*path\s*=/.test(line) && line.replace(/^\s*path\s*=\s*/, "").trim() === targetPath);
11326
12033
  }
@@ -11330,14 +12037,14 @@ function removeSubmodulePath(content, targetPath) {
11330
12037
  var UntrackHermesRuntimes = class extends Command {
11331
12038
  async invoke() {
11332
12039
  const targetDir = this.context.targetDir;
11333
- const rolesDir = join10(targetDir, "agents", "hermes");
12040
+ const rolesDir = join12(targetDir, "agents", "hermes");
11334
12041
  if (!existsSync8(rolesDir)) {
11335
12042
  return {
11336
12043
  success: true,
11337
12044
  message: "No Hermes agents found (no agents/hermes directory)."
11338
12045
  };
11339
12046
  }
11340
- const roles = readdirSync5(rolesDir, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name);
12047
+ const roles = readdirSync6(rolesDir, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name);
11341
12048
  if (roles.length === 0) {
11342
12049
  return {
11343
12050
  success: true,
@@ -11347,12 +12054,12 @@ var UntrackHermesRuntimes = class extends Command {
11347
12054
  let modifiedAny = false;
11348
12055
  const details = [];
11349
12056
  for (const role of roles) {
11350
- const roleDir = join10("agents", "hermes", role);
11351
- const runtimePath = join10(roleDir, "runtime");
11352
- const gitignorePath = join10(roleDir, ".gitignore");
11353
- const gitmodulesPath = join10(targetDir, ".gitmodules");
12057
+ const roleDir = join12("agents", "hermes", role);
12058
+ const runtimePath = join12(roleDir, "runtime");
12059
+ const gitignorePath = join12(roleDir, ".gitignore");
12060
+ const gitmodulesPath = join12(targetDir, ".gitmodules");
11354
12061
  let isTracked = false;
11355
- const lsResult = spawnSync4("git", ["ls-files", "--stage", "--", runtimePath], {
12062
+ const lsResult = spawnSync5("git", ["ls-files", "--stage", "--", runtimePath], {
11356
12063
  cwd: targetDir,
11357
12064
  encoding: "utf8"
11358
12065
  });
@@ -11368,14 +12075,14 @@ var UntrackHermesRuntimes = class extends Command {
11368
12075
  let hasStaleMapping = false;
11369
12076
  let gitmodulesContent = "";
11370
12077
  if (existsSync8(gitmodulesPath)) {
11371
- gitmodulesContent = readFileSync8(gitmodulesPath, "utf8");
12078
+ gitmodulesContent = readFileSync10(gitmodulesPath, "utf8");
11372
12079
  const sections = gitmodulesContent.match(/^\[submodule "[^"\n]+"\][\s\S]*?(?=^\[submodule "|(?![\s\S]))/gm) ?? [];
11373
12080
  hasStaleMapping = sections.some((section2) => sectionHasPath(section2, runtimePath));
11374
12081
  }
11375
12082
  let isIgnored = false;
11376
- const fullGitignorePath = join10(targetDir, gitignorePath);
12083
+ const fullGitignorePath = join12(targetDir, gitignorePath);
11377
12084
  if (existsSync8(fullGitignorePath)) {
11378
- const content = readFileSync8(fullGitignorePath, "utf8");
12085
+ const content = readFileSync10(fullGitignorePath, "utf8");
11379
12086
  const lines = content.split(/\r?\n/).map((line) => line.trim());
11380
12087
  isIgnored = lines.includes("runtime/") || lines.includes("runtime");
11381
12088
  }
@@ -11384,7 +12091,7 @@ var UntrackHermesRuntimes = class extends Command {
11384
12091
  if (isTracked) {
11385
12092
  details.push(`untrack agents/hermes/${role}/runtime`);
11386
12093
  if (!this.context.dryRun) {
11387
- const rmResult = spawnSync4("git", ["rm", "--cached", "-r", "-f", "--", runtimePath], {
12094
+ const rmResult = spawnSync5("git", ["rm", "--cached", "-r", "-f", "--", runtimePath], {
11388
12095
  cwd: targetDir,
11389
12096
  encoding: "utf8"
11390
12097
  });
@@ -11394,7 +12101,7 @@ var UntrackHermesRuntimes = class extends Command {
11394
12101
  message: `\u2717 Failed to untrack ${runtimePath}: ${rmResult.stderr.trim() || `exit ${rmResult.status}`}`
11395
12102
  };
11396
12103
  }
11397
- const verifyResult = spawnSync4("git", ["ls-files", "--stage", "--", runtimePath], {
12104
+ const verifyResult = spawnSync5("git", ["ls-files", "--stage", "--", runtimePath], {
11398
12105
  cwd: targetDir,
11399
12106
  encoding: "utf8"
11400
12107
  });
@@ -11419,7 +12126,7 @@ var UntrackHermesRuntimes = class extends Command {
11419
12126
  if (!this.context.dryRun) {
11420
12127
  let content = "";
11421
12128
  if (existsSync8(fullGitignorePath)) {
11422
- content = readFileSync8(fullGitignorePath, "utf8");
12129
+ content = readFileSync10(fullGitignorePath, "utf8");
11423
12130
  }
11424
12131
  if (content && !content.endsWith("\n")) {
11425
12132
  content += "\n";
@@ -11446,8 +12153,8 @@ ${details.map((d) => ` - ${d}`).join("\n")}`
11446
12153
  };
11447
12154
 
11448
12155
  // src/commands/hermes/WireTelegram.ts
11449
- import { spawnSync as spawnSync5 } from "node:child_process";
11450
- import { join as join11 } from "node:path";
12156
+ import { spawnSync as spawnSync6 } from "node:child_process";
12157
+ import { join as join13 } from "node:path";
11451
12158
  import { existsSync as existsSync9, unlinkSync as unlinkSync3 } from "node:fs";
11452
12159
  import * as p3 from "@clack/prompts";
11453
12160
  var WireTelegram = class extends Command {
@@ -11477,7 +12184,7 @@ var WireTelegram = class extends Command {
11477
12184
  let token = process.env.TELEGRAM_BOT_TOKEN;
11478
12185
  let source = token ? "env" : null;
11479
12186
  if (!token) {
11480
- const tryOp = spawnSync5("op", ["read", vaultRef], { encoding: "utf8" });
12187
+ const tryOp = spawnSync6("op", ["read", vaultRef], { encoding: "utf8" });
11481
12188
  if (tryOp.status === 0) {
11482
12189
  token = tryOp.stdout.trim();
11483
12190
  source = "op";
@@ -11516,7 +12223,7 @@ var WireTelegram = class extends Command {
11516
12223
  initialValue: true
11517
12224
  });
11518
12225
  if (!p3.isCancel(persist) && persist) {
11519
- const create = spawnSync5(
12226
+ const create = spawnSync6(
11520
12227
  "op",
11521
12228
  [
11522
12229
  "item",
@@ -11543,28 +12250,32 @@ var WireTelegram = class extends Command {
11543
12250
  if (p3.isCancel(allowedAnswer)) {
11544
12251
  return { success: false, message: "\u2717 Aborted; Telegram step deferred." };
11545
12252
  }
11546
- const script = join11(roleDir, ".scripts", "30-telegram.sh");
12253
+ const script = join13(roleDir, ".scripts", "30-telegram.sh");
11547
12254
  if (!existsSync9(script)) {
11548
12255
  return {
11549
12256
  success: false,
11550
- message: `\u2717 ${script} not found. Did copier finish? Re-run with --skip-runtime-repo=0 if you skipped it.`
12257
+ message: `\u2717 ${script} not found. Did Copier finish rendering the Hermes role?`
11551
12258
  };
11552
12259
  }
11553
- const marker = join11(roleDir, ".scripts", ".done-30-telegram");
12260
+ const marker = join13(roleDir, ".scripts", ".done-30-telegram");
11554
12261
  if (existsSync9(marker)) unlinkSync3(marker);
11555
- const spinner4 = p3.spinner();
11556
- spinner4.start("Verifying token + wiring profile");
11557
- const result2 = spawnSync5("bash", [script], {
12262
+ const spinner3 = p3.spinner();
12263
+ spinner3.start("Verifying token + wiring profile");
12264
+ const result2 = spawnSync6("bash", [script], {
11558
12265
  stdio: "inherit",
11559
12266
  env: {
11560
12267
  ...process.env,
12268
+ // Interactive wiring is an explicit host-state operation: it writes the
12269
+ // profile. Set the gate rather than inheriting whatever a deferred MCP
12270
+ // render left in the environment, which would silently no-op this run.
12271
+ SKIP_HOST_STATE: "0",
11561
12272
  SKIP_TELEGRAM: "0",
11562
12273
  TELEGRAM_BOT_TOKEN: token,
11563
12274
  TELEGRAM_ALLOWED_USERS: String(allowedAnswer).trim()
11564
12275
  },
11565
12276
  cwd: roleDir
11566
12277
  });
11567
- spinner4.stop(result2.status === 0 ? "\u2713 Telegram wired" : "\u2717 Telegram step failed");
12278
+ spinner3.stop(result2.status === 0 ? "\u2713 Telegram wired" : "\u2717 Telegram step failed");
11568
12279
  if (result2.status !== 0) {
11569
12280
  return { success: false, message: "Telegram wire-up failed. See output above." };
11570
12281
  }
@@ -11577,164 +12288,67 @@ function cap(s) {
11577
12288
  }
11578
12289
 
11579
12290
  // src/commands/hermes/WireEmail.ts
11580
- import { spawnSync as spawnSync6 } from "node:child_process";
11581
- import { join as join12 } from "node:path";
11582
- import { existsSync as existsSync10, unlinkSync as unlinkSync4 } from "node:fs";
11583
- import * as p4 from "@clack/prompts";
11584
12291
  var WireEmail = class extends Command {
11585
12292
  async invoke() {
11586
12293
  const ctx = this.context;
11587
- if (ctx.skipEmail) {
11588
- return { success: true, message: "" };
11589
- }
11590
- if (ctx.quiet) {
11591
- return {
11592
- success: false,
11593
- outcome: "failed",
11594
- message: "Email wiring is interactive and unavailable during quiet/non-interactive Hermes execution"
11595
- };
11596
- }
11597
- if (ctx.dryRun) {
11598
- return { success: true, message: this.formatMessage("Would create CF Email Routing rule") };
11599
- }
11600
- const { targetRepo, role, roleDir } = ctx;
11601
- if (!targetRepo || !role || !roleDir) {
11602
- return { success: false, message: "Cannot wire email: missing target_repo/role/roleDir" };
11603
- }
11604
- const script = join12(roleDir, ".scripts", "50-email.sh");
11605
- if (!existsSync10(script)) {
11606
- return { success: false, message: `\u2717 ${script} not found` };
11607
- }
11608
- let token = process.env.CF_EMAIL_ROUTING_TOKEN;
11609
- if (!token) {
11610
- const tryOp = spawnSync6(
11611
- "op",
11612
- ["read", "op://DeLoSecrets/Cloudflare-EmailRouting/token"],
11613
- { encoding: "utf8" }
11614
- );
11615
- if (tryOp.status === 0) {
11616
- token = tryOp.stdout.trim();
11617
- }
11618
- }
11619
- if (!token) {
11620
- p4.log.warn("CF Email Routing token not found. Required scopes:");
11621
- p4.log.info(
11622
- [
11623
- " Zone (delo.sh) \u2192 Email Routing Rules : Edit",
11624
- " Zone (delo.sh) \u2192 Email Routing Settings : Read",
11625
- " Account \u2192 Email Routing Addresses : Read",
11626
- "Create at: https://dash.cloudflare.com/profile/api-tokens"
11627
- ].join("\n")
11628
- );
11629
- const provideNow = await p4.confirm({
11630
- message: "Paste a token now? (skipping leaves email unwired until you re-run.)",
11631
- initialValue: false
11632
- });
11633
- if (p4.isCancel(provideNow) || !provideNow) {
11634
- return { success: true, message: "\u2192 Email skipped (no token). Re-run later." };
11635
- }
11636
- const tokenAnswer = await p4.password({
11637
- message: "CF token (will be passed via env, not stored)",
11638
- mask: "\u2022",
11639
- validate: (v) => String(v ?? "").trim() ? void 0 : "required"
11640
- });
11641
- if (p4.isCancel(tokenAnswer)) {
11642
- return { success: true, message: "\u2192 Email skipped (cancelled)" };
11643
- }
11644
- token = String(tokenAnswer).trim();
11645
- const persist = await p4.confirm({
11646
- message: "Save to op://DeLoSecrets/Cloudflare-EmailRouting/token for next time?",
11647
- initialValue: true
11648
- });
11649
- if (!p4.isCancel(persist) && persist) {
11650
- const create = spawnSync6(
11651
- "op",
11652
- [
11653
- "item",
11654
- "create",
11655
- "--category=API Credential",
11656
- "--vault=DeLoSecrets",
11657
- "--title=Cloudflare-EmailRouting",
11658
- `token=${token}`
11659
- ],
11660
- { stdio: "inherit" }
11661
- );
11662
- if (create.status !== 0) {
11663
- p4.log.warn("Could not store in 1Password \u2014 token is still set for this run.");
11664
- }
11665
- }
12294
+ if (ctx.skipEmail !== false) {
12295
+ return { success: true, outcome: "skipped", message: "" };
11666
12296
  }
11667
- const marker = join12(roleDir, ".scripts", ".done-50-email");
11668
- if (existsSync10(marker)) unlinkSync4(marker);
11669
- const spinner4 = p4.spinner();
11670
- spinner4.start("Creating Cloudflare Email Routing rule");
11671
- const result2 = spawnSync6("bash", [script], {
11672
- stdio: "inherit",
11673
- env: { ...process.env, SKIP_EMAIL: "0", CF_EMAIL_ROUTING_TOKEN: token },
11674
- cwd: roleDir
11675
- });
11676
- spinner4.stop(result2.status === 0 ? "\u2713 Email rule created" : "\u2717 Email step failed");
11677
- if (result2.status !== 0) {
11678
- return { success: false, message: "Email rule creation failed. See output above." };
11679
- }
11680
- return {
11681
- success: true,
11682
- message: `\u2713 Email: ${targetRepo}-${role}@delo.sh \u2192 jaradd@gmail.com`
11683
- };
12297
+ return { success: false, outcome: "failed", message: EMAIL_UNSUPPORTED_MESSAGE };
11684
12298
  }
11685
12299
  };
11686
12300
 
11687
12301
  // src/commands/hermes/PrintHermesSummary.ts
11688
- import * as p5 from "@clack/prompts";
12302
+ import { existsSync as existsSync10, readFileSync as readFileSync11 } from "node:fs";
12303
+ import { join as join14 } from "node:path";
12304
+ import YAML5 from "yaml";
12305
+ function readServiceState(roleDir) {
12306
+ if (!roleDir || !existsSync10(join14(roleDir, "role.yaml"))) return { gateway: "planned", heartbeat: "planned" };
12307
+ try {
12308
+ const role = YAML5.parse(readFileSync11(join14(roleDir, "role.yaml"), "utf8"));
12309
+ return {
12310
+ gateway: typeof role?.service_state?.gateway === "string" ? role.service_state.gateway : "unknown",
12311
+ heartbeat: typeof role?.service_state?.heartbeat === "string" ? role.service_state.heartbeat : "unknown"
12312
+ };
12313
+ } catch {
12314
+ return { gateway: "unknown", heartbeat: "unknown" };
12315
+ }
12316
+ }
12317
+ function renderHermesSummary(ctx) {
12318
+ const outcome = ctx.deploymentOutcome ?? (ctx.dryRun ? "planned" : "failed");
12319
+ const service = readServiceState(ctx.roleDir);
12320
+ const deferrals = [...new Set(ctx.deploymentDeferrals ?? [])];
12321
+ const title = outcome === "planned" ? "Hermes deployment plan (no changes applied)" : outcome === "verified-deferred" ? "Hermes deployment healthy with deferred capabilities" : outcome === "verified" ? "Hermes deployment verified" : "Hermes deployment not verified";
12322
+ const lines = [
12323
+ title,
12324
+ `agent_id: ${ctx.agentId ?? "planned"}`,
12325
+ `role_dir: ${ctx.roleDir ?? join14(ctx.targetDir, "agents", "hermes", ctx.role ?? "pm")}`,
12326
+ `runtime: local role runtime (${join14(ctx.roleDir ?? join14(ctx.targetDir, "agents", "hermes", ctx.role ?? "pm"), "runtime")})`,
12327
+ `heartbeat: ${service.heartbeat}`,
12328
+ `gateway: ${service.gateway}`
12329
+ ];
12330
+ if (deferrals.length) lines.push(`deferred: ${deferrals.join(", ")}`);
12331
+ for (const assertion of ctx.deploymentPostconditions ?? []) lines.push(`verified: ${assertion}`);
12332
+ if (outcome === "planned") lines.push("Apply by rerunning without --dry-run.");
12333
+ return lines.join("\n");
12334
+ }
11689
12335
  var PrintHermesSummary = class extends Command {
11690
12336
  async invoke() {
11691
12337
  const ctx = this.context;
11692
- const { targetRepo, role, agentId, runtimeRepo, skipTelegram, skipEmail } = ctx;
11693
- const botHandle = `${targetRepo?.toLowerCase().replace(/-/g, "_")}_${role?.toLowerCase()}_bot`;
11694
- const email = `${targetRepo}-${role}@delo.sh`;
11695
- const gw = `hermes-${agentId}-gateway.service`;
11696
- const hb = `hermes-${agentId}-heartbeat.timer`;
11697
- const lines = [];
11698
- lines.push(`agent_id ${agentId}`);
11699
- lines.push(`role dir ${ctx.roleDir}`);
11700
- lines.push(`runtime gh:${runtimeRepo}`);
11701
- lines.push(`telegram @${botHandle}${skipTelegram ? " (NOT yet wired)" : ""}`);
11702
- if (!skipEmail) lines.push(`email ${email}`);
11703
- lines.push("");
11704
- lines.push("Start daemons:");
11705
- lines.push(` systemctl --user start ${hb}`);
11706
- if (!skipTelegram) {
11707
- lines.push(` systemctl --user start ${gw}`);
11708
- } else {
11709
- lines.push(` # gateway needs Telegram wired first (re-run with --skip-telegram=0)`);
11710
- }
11711
- lines.push(" # Bloodbank commands arrive through the fleet-shared Hermes gateway");
11712
- lines.push("");
11713
- lines.push("Talk locally:");
11714
- lines.push(` ${ctx.roleDir}/hermes chat "status"`);
11715
- if (skipTelegram) {
11716
- lines.push("");
11717
- lines.push("Wire Telegram later:");
11718
- lines.push(" pjangler hermes-agent # re-run and answer yes when asked");
11719
- }
11720
- if (!ctx.quiet) {
11721
- p5.note(lines.join("\n"), `Provisioned ${agentId}`);
11722
- p5.outro("Done.");
11723
- }
11724
- return { success: true, outcome: "unchanged", message: "" };
12338
+ return { success: ctx.deploymentOutcome !== "failed", outcome: "unchanged", message: renderHermesSummary(ctx) };
11725
12339
  }
11726
12340
  };
11727
12341
 
11728
12342
  // src/commands/hermes/ApplyDeferredExternalEffects.ts
11729
12343
  import { spawnSync as spawnSync7 } from "node:child_process";
11730
- import { existsSync as existsSync11, readFileSync as readFileSync9, writeFileSync as writeFileSync7 } from "node:fs";
11731
- import { join as join13 } from "node:path";
11732
- import YAML5 from "yaml";
12344
+ import { existsSync as existsSync11, readFileSync as readFileSync12, writeFileSync as writeFileSync7 } from "node:fs";
12345
+ import { join as join15 } from "node:path";
12346
+ import YAML6 from "yaml";
11733
12347
  var ApplyDeferredExternalEffects = class extends Command {
11734
12348
  async invoke() {
11735
12349
  const ctx = this.context;
11736
12350
  const selected = ctx.deferredExternalEffects;
11737
- if (!selected || !selected.runtimeRepo && !selected.ticketBoard && !selected.systemd) {
12351
+ if (!selected || !selected.ticketBoard && !selected.systemd) {
11738
12352
  return { success: true, outcome: "unchanged", message: "Hermes external effects not selected" };
11739
12353
  }
11740
12354
  if (!ctx.roleDir) {
@@ -11748,24 +12362,25 @@ var ApplyDeferredExternalEffects = class extends Command {
11748
12362
  SKIP_EMAIL: "1",
11749
12363
  SKIP_SLACK: "1",
11750
12364
  SKIP_BLOODBANK: "1",
11751
- SKIP_RUNTIME_REPO: selected.runtimeRepo ? "0" : "1",
12365
+ // Role-local runtime is already converged by ApplyDeferredHostEffects.
12366
+ // External consent can never dispatch the retired GitHub runtime model.
12367
+ SKIP_RUNTIME_REPO: "1",
11752
12368
  SKIP_PLANE: selected.ticketBoard ? "0" : "1",
11753
12369
  SKIP_SYSTEMD: selected.systemd ? "0" : "1"
11754
12370
  };
11755
12371
  scrubInteractiveChannelCredentials(env2);
11756
12372
  if (!selected.ticketBoard) scrubTicketProviderCredentials(env2);
11757
- const roleManifest = join13(ctx.roleDir, "role.yaml");
12373
+ const roleManifest = join15(ctx.roleDir, "role.yaml");
11758
12374
  const scripts = [
11759
- ...selected.runtimeRepo ? ["20-runtime-repo.sh"] : [],
11760
12375
  ...selected.ticketBoard ? ["42-ticket-provider.sh"] : [],
11761
12376
  ...selected.systemd ? ["70-systemd.sh"] : [],
11762
- // Refresh fleet metadata after a board binding or runtime/systemd state
12377
+ // Refresh fleet metadata after a board binding or systemd state
11763
12378
  // changes. 80-registry.sh is idempotent and performs no provider call.
11764
12379
  "80-registry.sh"
11765
12380
  ];
11766
12381
  const logs = [];
11767
12382
  for (const script of scripts) {
11768
- const path = join13(ctx.roleDir, ".scripts", script);
12383
+ const path = join15(ctx.roleDir, ".scripts", script);
11769
12384
  if (!existsSync11(path)) {
11770
12385
  return { success: false, outcome: "failed", message: `Deferred Hermes script is missing: ${path}` };
11771
12386
  }
@@ -11778,8 +12393,8 @@ var ApplyDeferredExternalEffects = class extends Command {
11778
12393
  }
11779
12394
  }
11780
12395
  try {
11781
- const current = readFileSync9(roleManifest, "utf8");
11782
- const document = YAML5.parseDocument(current);
12396
+ const current = readFileSync12(roleManifest, "utf8");
12397
+ const document = YAML6.parseDocument(current);
11783
12398
  if (document.errors.length) throw document.errors[0];
11784
12399
  document.setIn(["deployment", "local_only"], Boolean(ctx.local));
11785
12400
  document.setIn(["deployment", "systemd"], selected.systemd ? "required" : "deferred");
@@ -11804,7 +12419,7 @@ ${logs.join("\n")}` : ""}`
11804
12419
  // src/commands/hermes/ApplyDeferredHostEffects.ts
11805
12420
  import { spawnSync as spawnSync8 } from "node:child_process";
11806
12421
  import { existsSync as existsSync12 } from "node:fs";
11807
- import { join as join14 } from "node:path";
12422
+ import { join as join16 } from "node:path";
11808
12423
  var ApplyDeferredHostEffects = class extends Command {
11809
12424
  async invoke() {
11810
12425
  const ctx = this.context;
@@ -11830,16 +12445,18 @@ var ApplyDeferredHostEffects = class extends Command {
11830
12445
  SKIP_EMAIL: "1",
11831
12446
  SKIP_SLACK: "1",
11832
12447
  SKIP_BLOODBANK: "1",
11833
- SKIP_RUNTIME_REPO: "1",
12448
+ // The legacy name controls role-local runtime/profile convergence only.
12449
+ // It is a required host/local phase, never a GitHub repository effect.
12450
+ SKIP_RUNTIME_REPO: "0",
11834
12451
  SKIP_PLANE: "1",
11835
12452
  SKIP_SYSTEMD: "1"
11836
12453
  };
11837
12454
  scrubTicketProviderCredentials(env2);
11838
12455
  scrubInteractiveChannelCredentials(env2);
11839
- const scripts = ["01-config.sh", "05-fleet-env.sh", "10-hermes-profile.sh"];
12456
+ const scripts = ["01-config.sh", "05-fleet-env.sh", "10-hermes-profile.sh", "20-runtime-repo.sh"];
11840
12457
  const logs = [];
11841
12458
  for (const script of scripts) {
11842
- const path = join14(ctx.roleDir, ".scripts", script);
12459
+ const path = join16(ctx.roleDir, ".scripts", script);
11843
12460
  if (!existsSync12(path)) {
11844
12461
  return { success: false, outcome: "failed", message: `Deferred Hermes host script is missing: ${path}` };
11845
12462
  }
@@ -11863,7 +12480,56 @@ ${logs.join("\n")}` : ""}`
11863
12480
  // src/recipes/HermesAgentRecipe.ts
11864
12481
  init_tree_diff();
11865
12482
  init_preflight();
11866
- import { resolve as resolve6 } from "node:path";
12483
+ import { existsSync as existsSync13, readFileSync as readFileSync13 } from "node:fs";
12484
+ import { join as join17, resolve as resolve6 } from "node:path";
12485
+ import YAML7 from "yaml";
12486
+ function deploymentDeferrals(ctx) {
12487
+ const deferred = [];
12488
+ if (ctx.local || ctx.skipPlane) deferred.push("ticket-board provisioning");
12489
+ if (ctx.local || ctx.skipSystemd) deferred.push("systemd service activation");
12490
+ const projectManifest = join17(ctx.targetDir, ".project.json");
12491
+ if (!ctx.skipPlane && existsSync13(projectManifest)) {
12492
+ try {
12493
+ const project = JSON.parse(readFileSync13(projectManifest, "utf8"));
12494
+ const state = project.ticket_provider?.state;
12495
+ if (typeof state === "string" && state !== "linked") deferred.push(`ticket board (${state})`);
12496
+ } catch {
12497
+ deferred.push("ticket board state unreadable");
12498
+ }
12499
+ }
12500
+ const rolePath = ctx.roleDir ? join17(ctx.roleDir, "role.yaml") : "";
12501
+ if (rolePath && existsSync13(rolePath)) {
12502
+ try {
12503
+ const role = YAML7.parse(readFileSync13(rolePath, "utf8"));
12504
+ const gateway = role?.service_state?.gateway;
12505
+ const heartbeat = role?.service_state?.heartbeat;
12506
+ if (typeof gateway === "string" && gateway !== "active") deferred.push(`gateway (${gateway})`);
12507
+ if (typeof heartbeat === "string" && heartbeat !== "active") deferred.push(`heartbeat (${heartbeat})`);
12508
+ } catch {
12509
+ deferred.push("service state unreadable");
12510
+ }
12511
+ } else if (!ctx.dryRun) {
12512
+ deferred.push("role service state unavailable");
12513
+ }
12514
+ return [...new Set(deferred)];
12515
+ }
12516
+ async function summaryResult(ctx) {
12517
+ const summary = await new PrintHermesSummary(ctx).invoke();
12518
+ return {
12519
+ recipeId: "hermes-agent",
12520
+ ok: summary.success,
12521
+ dryRun: Boolean(ctx.dryRun),
12522
+ changedFiles: [],
12523
+ logs: summary.message ? [summary.message] : [],
12524
+ errors: summary.success ? [] : [summary.message || "Hermes summary could not be rendered"],
12525
+ phases: [{
12526
+ id: "hermes.summary",
12527
+ status: summary.success ? "unchanged" : "failed",
12528
+ changedFiles: [],
12529
+ message: summary.message || void 0
12530
+ }]
12531
+ };
12532
+ }
11867
12533
  var HermesAgentRecipe = class extends Recipe {
11868
12534
  checks = createHermesChecks();
11869
12535
  metadata = {
@@ -11872,13 +12538,14 @@ var HermesAgentRecipe = class extends Recipe {
11872
12538
  description: "Add and reconcile a Hermes agent role",
11873
12539
  dependencies: [],
11874
12540
  commands: [
11875
- "EnsureTemplateConfig",
11876
12541
  "PromptForAgentConfig",
12542
+ "ValidateHermesOptions",
12543
+ "EnsureTemplateConfig",
11877
12544
  "RunCopierTemplate",
11878
12545
  "UntrackHermesRuntimes",
11879
12546
  "WireTelegram",
11880
12547
  "WireEmail",
11881
- "PrintHermesSummary"
12548
+ "PrintHermesSummary (postconditions only)"
11882
12549
  ],
11883
12550
  publicRuleIds: this.checks.map((check) => check.id)
11884
12551
  };
@@ -11889,13 +12556,13 @@ var HermesAgentRecipe = class extends Recipe {
11889
12556
  const errors = [];
11890
12557
  const changedFiles = [];
11891
12558
  const ingredients = [
11892
- EnsureTemplateConfig,
11893
12559
  PromptForAgentConfig,
12560
+ ValidateHermesOptions,
12561
+ EnsureTemplateConfig,
11894
12562
  RunCopierTemplate,
11895
12563
  UntrackHermesRuntimes,
11896
12564
  WireTelegram,
11897
- WireEmail,
11898
- PrintHermesSummary
12565
+ WireEmail
11899
12566
  ];
11900
12567
  for (const [ingredientIndex, CommandClass] of ingredients.entries()) {
11901
12568
  if (typeof CommandClass !== "function") {
@@ -11964,30 +12631,46 @@ var HermesAgentRecipe = class extends Recipe {
11964
12631
  if (!commandResult.ok) return commandResult;
11965
12632
  const lifecycle = await this.initializeOwnedChecks(ctx);
11966
12633
  const localResult = mergeInitResults(this.metadata.id, Boolean(ctx.dryRun), [commandResult, lifecycle]);
11967
- if (!localResult.ok || ctx.dryRun) return localResult;
12634
+ if (!localResult.ok) return localResult;
11968
12635
  const hermesContext = ctx;
11969
- if (hermesContext.deferredExternalEffects?.owner !== "hermes") return localResult;
12636
+ if (ctx.dryRun) {
12637
+ hermesContext.deploymentOutcome = "planned";
12638
+ hermesContext.deploymentDeferrals = deploymentDeferrals(hermesContext);
12639
+ hermesContext.deploymentPostconditions = ["dry-run made no repository or host changes"];
12640
+ return mergeInitResults(this.metadata.id, true, [localResult, await summaryResult(ctx)]);
12641
+ }
11970
12642
  const selected = hermesContext.deferredExternalEffects;
11971
- if (!selected.runtimeRepo && !selected.ticketBoard && !selected.systemd) return localResult;
11972
- const beforeExternal = snapshotTree(ctx.targetDir);
11973
- const external = await new ApplyDeferredExternalEffects(ctx).invoke();
11974
- const externalChanges = changedTreePaths(ctx.targetDir, beforeExternal, snapshotTree(ctx.targetDir));
11975
- const externalResult = {
11976
- recipeId: this.metadata.id,
11977
- ok: external.success,
11978
- dryRun: false,
11979
- changedFiles: externalChanges,
11980
- logs: external.message ? [external.message] : [],
11981
- errors: external.success ? [] : [external.message || "Deferred Hermes external effects failed"],
11982
- phases: [{
11983
- id: "hermes.external-effects",
11984
- status: external.success ? "changed" : "failed",
11985
- changedFiles: external.success ? externalChanges : [],
11986
- message: external.message || void 0
11987
- }]
11988
- };
11989
- if (!externalResult.ok) return mergeInitResults(this.metadata.id, false, [localResult, externalResult]);
11990
- const findings = this.audit(ctx).filter((finding2) => finding2.status !== "pass" && finding2.status !== "skip");
12643
+ if (selected?.owner === "project" || ctx.quiet && !selected) return localResult;
12644
+ let appliedResult = localResult;
12645
+ if (selected?.owner === "hermes" && (selected.ticketBoard || selected.systemd)) {
12646
+ const beforeExternal = snapshotTree(ctx.targetDir);
12647
+ const external = await new ApplyDeferredExternalEffects(ctx).invoke();
12648
+ const externalChanges = changedTreePaths(ctx.targetDir, beforeExternal, snapshotTree(ctx.targetDir));
12649
+ const externalResult = {
12650
+ recipeId: this.metadata.id,
12651
+ ok: external.success,
12652
+ dryRun: false,
12653
+ changedFiles: externalChanges,
12654
+ logs: external.message ? [external.message] : [],
12655
+ errors: external.success ? [] : [external.message || "Deferred Hermes external effects failed"],
12656
+ phases: [{
12657
+ id: "hermes.external-effects",
12658
+ status: external.success ? "changed" : "failed",
12659
+ changedFiles: external.success ? externalChanges : [],
12660
+ message: external.message || void 0
12661
+ }]
12662
+ };
12663
+ appliedResult = mergeInitResults(this.metadata.id, false, [localResult, externalResult]);
12664
+ if (!externalResult.ok) return appliedResult;
12665
+ }
12666
+ const crossChecks = [
12667
+ ...createMiseChecks().filter((check) => check.id === "mise.config-root"),
12668
+ ...createProjectChecks().filter((check) => check.id === "sot.project-json")
12669
+ ];
12670
+ const findings = [
12671
+ ...this.audit(ctx),
12672
+ ...crossChecks.map((check) => check.audit(ctx))
12673
+ ].filter((finding2) => finding2.status !== "pass" && finding2.status !== "skip");
11991
12674
  const verification = {
11992
12675
  recipeId: this.metadata.id,
11993
12676
  ok: findings.length === 0,
@@ -12002,7 +12685,18 @@ var HermesAgentRecipe = class extends Recipe {
12002
12685
  message: findings.length ? "Hermes postcondition audit failed" : "Hermes postcondition audit passed"
12003
12686
  }]
12004
12687
  };
12005
- return mergeInitResults(this.metadata.id, false, [localResult, externalResult, verification]);
12688
+ const verifiedResult = mergeInitResults(this.metadata.id, false, [appliedResult, verification]);
12689
+ if (!verification.ok) {
12690
+ hermesContext.deploymentOutcome = "failed";
12691
+ return verifiedResult;
12692
+ }
12693
+ hermesContext.deploymentDeferrals = deploymentDeferrals(hermesContext);
12694
+ hermesContext.deploymentOutcome = hermesContext.deploymentDeferrals.length ? "verified-deferred" : "verified";
12695
+ hermesContext.deploymentPostconditions = [
12696
+ "Hermes lifecycle audit passed",
12697
+ "mise PATH and canonical project manifest passed"
12698
+ ];
12699
+ return mergeInitResults(this.metadata.id, false, [verifiedResult, await summaryResult(ctx)]);
12006
12700
  }
12007
12701
  printNextSteps() {
12008
12702
  }
@@ -12181,22 +12875,22 @@ var NodeRecipe = class extends Recipe {
12181
12875
  // src/recipes/ProjectRecipe.ts
12182
12876
  init_project();
12183
12877
  import { spawnSync as spawnSync13 } from "node:child_process";
12184
- import { existsSync as existsSync18, lstatSync as lstatSync10, readFileSync as readFileSync16, rmSync as rmSync4 } from "node:fs";
12185
- import { dirname as dirname11, isAbsolute as isAbsolute5, join as join22, relative as relativePath, resolve as resolve13 } from "node:path";
12878
+ import { existsSync as existsSync19, lstatSync as lstatSync12, readFileSync as readFileSync19, rmSync as rmSync5 } from "node:fs";
12879
+ import { dirname as dirname11, isAbsolute as isAbsolute5, join as join24, relative as relativePath, resolve as resolve13 } from "node:path";
12186
12880
  init_tree_diff();
12187
12881
 
12188
12882
  // src/recipes/NotebookRecipe.ts
12189
12883
  init_project();
12190
- import { existsSync as existsSync17, readFileSync as readFileSync15 } from "node:fs";
12191
- import { join as join21 } from "node:path";
12884
+ import { existsSync as existsSync18, readFileSync as readFileSync18 } from "node:fs";
12885
+ import { join as join23 } from "node:path";
12192
12886
 
12193
12887
  // src/notebook/checks.ts
12194
12888
  init_config();
12195
12889
  init_notes();
12196
12890
  init_output();
12197
12891
  init_state();
12198
- import { existsSync as existsSync15, readFileSync as readFileSync12 } from "node:fs";
12199
- import { join as join16 } from "node:path";
12892
+ import { existsSync as existsSync16, readFileSync as readFileSync16 } from "node:fs";
12893
+ import { join as join19 } from "node:path";
12200
12894
  var NOTEBOOK_RULE_IDS = [
12201
12895
  "notebook.configuration",
12202
12896
  "notebook.binding",
@@ -12213,10 +12907,10 @@ function result(check, status, summary, changedFiles = [], details = []) {
12213
12907
  return { id: check.id, title: check.title, status, summary, changedFiles, details };
12214
12908
  }
12215
12909
  function manifestNotebook(repo) {
12216
- const path = join16(repo, ".project.json");
12217
- if (!existsSync15(path)) return null;
12910
+ const path = join19(repo, ".project.json");
12911
+ if (!existsSync16(path)) return null;
12218
12912
  try {
12219
- const parsed = JSON.parse(readFileSync12(path, "utf8"));
12913
+ const parsed = JSON.parse(readFileSync16(path, "utf8"));
12220
12914
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return null;
12221
12915
  const notebook = parsed.notebook;
12222
12916
  return notebook && typeof notebook === "object" && !Array.isArray(notebook) ? notebook : null;
@@ -12484,8 +13178,8 @@ init_notes();
12484
13178
  init_git_evidence();
12485
13179
  init_types();
12486
13180
  import { spawnSync as spawnSync10 } from "node:child_process";
12487
- import { realpathSync as realpathSync6 } from "node:fs";
12488
- import { relative as relative9, resolve as resolve10, sep as sep5 } from "node:path";
13181
+ import { realpathSync as realpathSync7 } from "node:fs";
13182
+ import { relative as relative10, resolve as resolve10, sep as sep5 } from "node:path";
12489
13183
  var DEFAULT_OVERVIEW_REFERENCES = [".project.json", "README.md", "AGENTS.md", "CLAUDE.md", "docs/architecture.md"];
12490
13184
  function git2(repo, args, timeout) {
12491
13185
  const result2 = spawnSync10("git", args, { cwd: repo, encoding: "utf8", maxBuffer: 1024 * 1024, timeout, shell: false });
@@ -12493,9 +13187,9 @@ function git2(repo, args, timeout) {
12493
13187
  }
12494
13188
  function normalizedReference(repo, value) {
12495
13189
  if (!value || value.includes("\0") || value.startsWith("/") || value.split(/[\\/]/u).includes("..")) throw new NotebookError("INVALID_INPUT", `Overview reference is not a contained relative path: ${value}`);
12496
- const root = realpathSync6(repo);
13190
+ const root = realpathSync7(repo);
12497
13191
  const candidate = resolve10(root, value);
12498
- const rel = relative9(root, candidate).split(sep5).join("/");
13192
+ const rel = relative10(root, candidate).split(sep5).join("/");
12499
13193
  if (!rel || rel === ".." || rel.startsWith("../")) throw new NotebookError("INVALID_INPUT", `Overview reference escapes the repository: ${value}`);
12500
13194
  return rel.normalize("NFC");
12501
13195
  }
@@ -12621,44 +13315,21 @@ init_reconcile();
12621
13315
  init_config();
12622
13316
 
12623
13317
  // src/notebook/hooks.ts
12624
- import { spawn, spawnSync as spawnSync11 } from "node:child_process";
12625
- import { createHash as createHash7, randomUUID as randomUUID5 } from "node:crypto";
12626
- import { chmodSync as chmodSync4, closeSync as closeSync6, constants as constants5, copyFileSync as copyFileSync3, existsSync as existsSync16, fstatSync as fstatSync4, fsyncSync as fsyncSync4, lstatSync as lstatSync9, mkdirSync as mkdirSync7, openSync as openSync6, readFileSync as readFileSync14, readSync as readSync3, readdirSync as readdirSync7, realpathSync as realpathSync7, renameSync as renameSync5, rmSync as rmSync3, symlinkSync as symlinkSync2, unlinkSync as unlinkSync6, writeFileSync as writeFileSync10 } from "node:fs";
12627
- import { basename as basename7, dirname as dirname10, isAbsolute as isAbsolute3, join as join20, parse as parse3, relative as relative10, resolve as resolve11, sep as sep6 } from "node:path";
12628
- import { fileURLToPath as fileURLToPath5 } from "node:url";
12629
-
12630
- // src/utils/version.ts
12631
- import { readFileSync as readFileSync13 } from "node:fs";
12632
- import { dirname as dirname9, join as join19 } from "node:path";
12633
- import { fileURLToPath as fileURLToPath4 } from "node:url";
12634
- var PJANGLER_VERSION = (() => {
12635
- try {
12636
- let dir = dirname9(fileURLToPath4(import.meta.url));
12637
- for (let i = 0; i < 4; i++) {
12638
- try {
12639
- const raw = readFileSync13(join19(dir, "package.json"), "utf8");
12640
- return JSON.parse(raw).version ?? "0.0.0";
12641
- } catch {
12642
- const parent = dirname9(dir);
12643
- if (parent === dir) break;
12644
- dir = parent;
12645
- }
12646
- }
12647
- } catch {
12648
- }
12649
- return "0.0.0";
12650
- })();
12651
-
12652
- // src/notebook/hooks.ts
13318
+ init_version();
12653
13319
  init_git_evidence();
12654
13320
  init_notes();
12655
13321
  init_state();
12656
13322
  init_types();
13323
+ import { spawn, spawnSync as spawnSync11 } from "node:child_process";
13324
+ import { createHash as createHash7, randomUUID as randomUUID5 } from "node:crypto";
13325
+ import { chmodSync as chmodSync4, closeSync as closeSync6, constants as constants5, copyFileSync as copyFileSync3, existsSync as existsSync17, fstatSync as fstatSync4, fsyncSync as fsyncSync4, lstatSync as lstatSync11, mkdirSync as mkdirSync7, openSync as openSync6, readFileSync as readFileSync17, readSync as readSync3, readdirSync as readdirSync8, realpathSync as realpathSync8, renameSync as renameSync6, rmSync as rmSync4, symlinkSync as symlinkSync2, unlinkSync as unlinkSync5, writeFileSync as writeFileSync10 } from "node:fs";
13326
+ import { basename as basename8, dirname as dirname10, isAbsolute as isAbsolute3, join as join22, parse as parse3, relative as relative11, resolve as resolve11, sep as sep6 } from "node:path";
13327
+ import { fileURLToPath as fileURLToPath5 } from "node:url";
12657
13328
  function bundledSkillCandidates() {
12658
13329
  const candidates = [];
12659
13330
  let cursor = dirname10(fileURLToPath5(import.meta.url));
12660
13331
  for (let depth = 0; depth < 8; depth++) {
12661
- candidates.push(join20(cursor, "dist", "assets", "project-notebook-skill"), join20(cursor, "assets", "project-notebook-skill"));
13332
+ candidates.push(join22(cursor, "dist", "assets", "project-notebook-skill"), join22(cursor, "assets", "project-notebook-skill"));
12662
13333
  const parent = dirname10(cursor);
12663
13334
  if (parent === cursor) break;
12664
13335
  cursor = parent;
@@ -12676,13 +13347,13 @@ function assertOwnedSkillTree(source) {
12676
13347
  assertNoSymlinkComponents2(source);
12677
13348
  const uid = typeof process.getuid === "function" ? process.getuid() : void 0;
12678
13349
  const walk = (directory) => {
12679
- const directoryStat = lstatSync9(directory);
13350
+ const directoryStat = lstatSync11(directory);
12680
13351
  if (!directoryStat.isDirectory() || directoryStat.isSymbolicLink()) throw new NotebookError("CONFLICT", "Project Notebook skill source contains a non-directory component");
12681
13352
  if (uid !== void 0 && directoryStat.uid !== uid) throw new NotebookError("CONFLICT", "Project Notebook skill source is not owned by the current user");
12682
13353
  if (directoryStat.mode & 3586) throw new NotebookError("CONFLICT", "Project Notebook skill source has unsafe directory mode bits");
12683
- for (const entry of readdirSync7(directory, { withFileTypes: true })) {
12684
- const path = join20(directory, entry.name);
12685
- const stat = lstatSync9(path);
13354
+ for (const entry of readdirSync8(directory, { withFileTypes: true })) {
13355
+ const path = join22(directory, entry.name);
13356
+ const stat = lstatSync11(path);
12686
13357
  if (stat.isSymbolicLink()) throw new NotebookError("CONFLICT", "Project Notebook skill source contains a symlink");
12687
13358
  if (entry.isDirectory()) walk(path);
12688
13359
  else if (!entry.isFile()) throw new NotebookError("CONFLICT", "Project Notebook skill source contains a non-regular entry");
@@ -12697,17 +13368,17 @@ function assertOwnedSkillTree(source) {
12697
13368
  function enumerateSkillPayload(source) {
12698
13369
  const result2 = [];
12699
13370
  const walk = (directory) => {
12700
- for (const entry of readdirSync7(directory, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name, "en"))) {
12701
- const path = join20(directory, entry.name);
12702
- const rel = relative10(source, path).split(sep6).join("/");
13371
+ for (const entry of readdirSync8(directory, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name, "en"))) {
13372
+ const path = join22(directory, entry.name);
13373
+ const rel = relative11(source, path).split(sep6).join("/");
12703
13374
  if (!rel.includes("/") && (rel === "export-manifest.json" || rel === "SHA256SUMS" || rel === ".source.yaml")) continue;
12704
13375
  if (!safeSkillRelativePath(rel)) throw new NotebookError("CONFLICT", `Project Notebook skill export path is unsafe: ${rel}`);
12705
- const stat = lstatSync9(path);
13376
+ const stat = lstatSync11(path);
12706
13377
  if (stat.isSymbolicLink()) throw new NotebookError("CONFLICT", "Project Notebook skill export contains a symlink");
12707
13378
  if (entry.isDirectory()) walk(path);
12708
13379
  else if (entry.isFile()) {
12709
13380
  const expectedMode = rel.endsWith(".sh") || rel.startsWith("scripts/") ? "0755" : "0644";
12710
- result2.push({ path: rel, sha256: createHash7("sha256").update(readFileSync14(path)).digest("hex"), mode: expectedMode });
13381
+ result2.push({ path: rel, sha256: createHash7("sha256").update(readFileSync17(path)).digest("hex"), mode: expectedMode });
12711
13382
  } else throw new NotebookError("CONFLICT", "Project Notebook skill export contains a non-regular entry");
12712
13383
  }
12713
13384
  };
@@ -12715,9 +13386,9 @@ function enumerateSkillPayload(source) {
12715
13386
  return result2.sort((a, b) => a.path.localeCompare(b.path, "en"));
12716
13387
  }
12717
13388
  function parsePackedManifest(source) {
12718
- const manifestPath = join20(source, "export-manifest.json");
12719
- if (!existsSync16(manifestPath)) return null;
12720
- const value = JSON.parse(readFileSync14(manifestPath, "utf8"));
13389
+ const manifestPath = join22(source, "export-manifest.json");
13390
+ if (!existsSync17(manifestPath)) return null;
13391
+ const value = JSON.parse(readFileSync17(manifestPath, "utf8"));
12721
13392
  if (!value || typeof value !== "object" || Array.isArray(value)) throw new NotebookError("CONFLICT", "Project Notebook skill export manifest is invalid");
12722
13393
  const manifest = value;
12723
13394
  if (manifest.schema_version !== 1 || manifest.skill !== "project-notebook" || !Array.isArray(manifest.files)) throw new NotebookError("CONFLICT", "Project Notebook skill export manifest is incompatible");
@@ -12726,10 +13397,10 @@ function parsePackedManifest(source) {
12726
13397
  if (!entry || typeof entry.path !== "string" || !/^[a-f0-9]{64}$/u.test(entry.sha256) || !/^(?:0644|0755)$/u.test(entry.mode)) throw new NotebookError("CONFLICT", "Project Notebook skill export entry is invalid");
12727
13398
  if (!safeSkillRelativePath(entry.path) || paths.has(entry.path)) throw new NotebookError("CONFLICT", "Project Notebook skill export path is unsafe or duplicated");
12728
13399
  paths.add(entry.path);
12729
- const path = join20(source, ...entry.path.split("/"));
12730
- const stat = lstatSync9(path);
13400
+ const path = join22(source, ...entry.path.split("/"));
13401
+ const stat = lstatSync11(path);
12731
13402
  if (!stat.isFile() || stat.isSymbolicLink()) throw new NotebookError("CONFLICT", "Project Notebook skill export contains a non-regular entry");
12732
- const actual = createHash7("sha256").update(readFileSync14(path)).digest("hex");
13403
+ const actual = createHash7("sha256").update(readFileSync17(path)).digest("hex");
12733
13404
  if (actual !== entry.sha256) throw new NotebookError("CONFLICT", `Project Notebook skill digest mismatch: ${entry.path}`);
12734
13405
  const actualMode = stat.mode & 511;
12735
13406
  const executable = entry.mode === "0755";
@@ -12741,12 +13412,12 @@ function parsePackedManifest(source) {
12741
13412
  if (JSON.stringify(actualPaths) !== JSON.stringify(manifest.files.map((entry) => entry.path))) throw new NotebookError("CONFLICT", "Project Notebook skill manifest does not exactly enumerate its payload");
12742
13413
  const sums = `${manifest.files.map((entry) => `${entry.sha256} ${entry.path}`).join("\n")}
12743
13414
  `;
12744
- if (!existsSync16(join20(source, "SHA256SUMS")) || readFileSync14(join20(source, "SHA256SUMS"), "utf8") !== sums) throw new NotebookError("CONFLICT", "Project Notebook skill SHA256SUMS is missing or stale");
13415
+ if (!existsSync17(join22(source, "SHA256SUMS")) || readFileSync17(join22(source, "SHA256SUMS"), "utf8") !== sums) throw new NotebookError("CONFLICT", "Project Notebook skill SHA256SUMS is missing or stale");
12745
13416
  return manifest;
12746
13417
  }
12747
13418
  function expectedPackedSkill() {
12748
13419
  for (const candidate of bundledSkillCandidates()) {
12749
- if (!existsSync16(join20(candidate, "export-manifest.json"))) continue;
13420
+ if (!existsSync17(join22(candidate, "export-manifest.json"))) continue;
12750
13421
  assertOwnedSkillTree(candidate);
12751
13422
  const manifest = parsePackedManifest(candidate);
12752
13423
  if (!manifest) continue;
@@ -12813,13 +13484,13 @@ function verifyProjectNotebookSkillExport(source) {
12813
13484
  function resolveProjectNotebookSkillSource(env2 = process.env) {
12814
13485
  if (env2.PJ_PROJECT_NOTEBOOK_SKILL_ROOT) {
12815
13486
  const explicit = resolve11(env2.PJ_PROJECT_NOTEBOOK_SKILL_ROOT);
12816
- if (!existsSync16(join20(explicit, "SKILL.md"))) throw new NotebookError("NOT_CONFIGURED", "Configured Project Notebook skill source is unavailable");
13487
+ if (!existsSync17(join22(explicit, "SKILL.md"))) throw new NotebookError("NOT_CONFIGURED", "Configured Project Notebook skill source is unavailable");
12817
13488
  verifyProjectNotebookSkillExport(explicit);
12818
13489
  return explicit;
12819
13490
  }
12820
13491
  if (env2.PJ_SKILLS_REGISTRY_ROOT) {
12821
13492
  const canonical = resolve11(env2.PJ_SKILLS_REGISTRY_ROOT, "all-skills", "project-notebook");
12822
- if (existsSync16(join20(canonical, "SKILL.md"))) {
13493
+ if (existsSync17(join22(canonical, "SKILL.md"))) {
12823
13494
  verifyProjectNotebookSkillExport(canonical);
12824
13495
  return canonical;
12825
13496
  }
@@ -12834,7 +13505,7 @@ var REPAIR_COMMAND = "pj notebook skill --apply";
12834
13505
  function probeCanonicalSkillexRootProjection(skillsRoot, link) {
12835
13506
  let rootLink;
12836
13507
  try {
12837
- rootLink = lstatSync9(skillsRoot);
13508
+ rootLink = lstatSync11(skillsRoot);
12838
13509
  } catch (error) {
12839
13510
  if (error.code === "ENOENT") return { state: "absent" };
12840
13511
  throw error;
@@ -12845,21 +13516,21 @@ function probeCanonicalSkillexRootProjection(skillsRoot, link) {
12845
13516
  assertNoSymlinkComponents2(dirname10(skillsRoot));
12846
13517
  const uid = typeof process.getuid === "function" ? process.getuid() : void 0;
12847
13518
  if (uid !== void 0 && rootLink.uid !== uid) throw new Error("owner");
12848
- const globalRoot = realpathSync7(skillsRoot);
13519
+ const globalRoot = realpathSync8(skillsRoot);
12849
13520
  assertNoSymlinkComponents2(globalRoot);
12850
- const globalStat = lstatSync9(globalRoot);
13521
+ const globalStat = lstatSync11(globalRoot);
12851
13522
  if (!globalStat.isDirectory() || globalStat.isSymbolicLink()) throw new Error("root-type");
12852
13523
  if (uid !== void 0 && globalStat.uid !== uid) throw new Error("root-owner");
12853
13524
  if (globalStat.mode & 3586) throw new Error("root-mode");
12854
13525
  const skillSetsRoot = dirname10(globalRoot);
12855
- if (basename7(globalRoot) !== "global" || basename7(skillSetsRoot) !== "skill-sets") throw new Error("layout");
13526
+ if (basename8(globalRoot) !== "global" || basename8(skillSetsRoot) !== "skill-sets") throw new Error("layout");
12856
13527
  const checkoutRoot = dirname10(skillSetsRoot);
12857
- const expectedSource = join20(checkoutRoot, "all-skills", "project-notebook");
12858
- const linkStat = lstatSync9(link);
13528
+ const expectedSource = join22(checkoutRoot, "all-skills", "project-notebook");
13529
+ const linkStat = lstatSync11(link);
12859
13530
  if (!linkStat.isSymbolicLink()) throw new Error("projection-type");
12860
13531
  if (uid !== void 0 && linkStat.uid !== uid) throw new Error("projection-owner");
12861
- const projectedSource = realpathSync7(link);
12862
- if (projectedSource !== realpathSync7(expectedSource)) throw new Error("projection-target");
13532
+ const projectedSource = realpathSync8(link);
13533
+ if (projectedSource !== realpathSync8(expectedSource)) throw new Error("projection-target");
12863
13534
  if (!isVerifiedCanonicalSkillexProjection(projectedSource)) {
12864
13535
  return {
12865
13536
  state: "declined",
@@ -12892,27 +13563,27 @@ function installPackagedProjectNotebookSkill(input = {}) {
12892
13563
  const digest = createHash7("sha256").update(JSON.stringify(manifest)).digest("hex");
12893
13564
  const home = env2.HOME;
12894
13565
  if (!home || !resolve11(home).startsWith("/")) throw new NotebookError("NOT_CONFIGURED", "A trusted HOME is required to install the Project Notebook skill");
12895
- const skillsRoot = join20(home, ".agents", "skills");
12896
- const link = join20(skillsRoot, "project-notebook");
13566
+ const skillsRoot = join22(home, ".agents", "skills");
13567
+ const link = join22(skillsRoot, "project-notebook");
12897
13568
  const probe = probeCanonicalSkillexRootProjection(skillsRoot, link);
12898
13569
  if (probe.state === "adopted") return { installed: false, path: link, digest };
12899
13570
  if (probe.state === "declined") return { installed: false, path: link, digest, blocked: probe.block };
12900
- const dataRoot = resolve11(env2.XDG_DATA_HOME || join20(home, ".local", "share"), "pjangler", "skills", "project-notebook");
12901
- const payload = join20(dataRoot, `${PJANGLER_VERSION}-${digest}`);
13571
+ const dataRoot = resolve11(env2.XDG_DATA_HOME || join22(home, ".local", "share"), "pjangler", "skills", "project-notebook");
13572
+ const payload = join22(dataRoot, `${PJANGLER_VERSION}-${digest}`);
12902
13573
  assertNoSymlinkComponents2(dirname10(dataRoot), true);
12903
13574
  mkdirSync7(dataRoot, { recursive: true, mode: 448 });
12904
13575
  assertNoSymlinkComponents2(dataRoot);
12905
- const dataStat = lstatSync9(dataRoot);
13576
+ const dataStat = lstatSync11(dataRoot);
12906
13577
  if (!dataStat.isDirectory() || dataStat.isSymbolicLink() || typeof process.getuid === "function" && dataStat.uid !== process.getuid()) throw new NotebookError("CONFLICT", "Project Notebook skill data root is not a current-user directory");
12907
13578
  chmodSync4(dataRoot, 448);
12908
- if (!existsSync16(payload)) {
12909
- const staging = join20(dataRoot, `.staging-${randomUUID5()}`);
13579
+ if (!existsSync17(payload)) {
13580
+ const staging = join22(dataRoot, `.staging-${randomUUID5()}`);
12910
13581
  mkdirSync7(staging, { recursive: false, mode: 448 });
12911
13582
  try {
12912
13583
  for (const entry of manifest.files) {
12913
- const destination = join20(staging, ...entry.path.split("/"));
13584
+ const destination = join22(staging, ...entry.path.split("/"));
12914
13585
  mkdirSync7(dirname10(destination), { recursive: true, mode: 493 });
12915
- copyFileSync3(join20(source, ...entry.path.split("/")), destination);
13586
+ copyFileSync3(join22(source, ...entry.path.split("/")), destination);
12916
13587
  const mode = entry.mode === "0755" ? 493 : 420;
12917
13588
  chmodSync4(destination, mode);
12918
13589
  const fd = openSync6(destination, constants5.O_RDONLY | (constants5.O_NOFOLLOW ?? 0));
@@ -12924,27 +13595,27 @@ function installPackagedProjectNotebookSkill(input = {}) {
12924
13595
  closeSync6(fd);
12925
13596
  }
12926
13597
  }
12927
- writeFileSync10(join20(staging, "export-manifest.json"), `${JSON.stringify(manifest, null, 2)}
13598
+ writeFileSync10(join22(staging, "export-manifest.json"), `${JSON.stringify(manifest, null, 2)}
12928
13599
  `, { mode: 420, flag: "wx" });
12929
- writeFileSync10(join20(staging, "SHA256SUMS"), `${manifest.files.map((entry) => `${entry.sha256} ${entry.path}`).join("\n")}
13600
+ writeFileSync10(join22(staging, "SHA256SUMS"), `${manifest.files.map((entry) => `${entry.sha256} ${entry.path}`).join("\n")}
12930
13601
  `, { mode: 420, flag: "wx" });
12931
13602
  verifyProjectNotebookSkillExport(staging);
12932
- renameSync5(staging, payload);
13603
+ renameSync6(staging, payload);
12933
13604
  } finally {
12934
- if (existsSync16(staging)) rmSync3(staging, { recursive: true, force: true });
13605
+ if (existsSync17(staging)) rmSync4(staging, { recursive: true, force: true });
12935
13606
  }
12936
13607
  } else {
12937
- const stat = lstatSync9(payload);
13608
+ const stat = lstatSync11(payload);
12938
13609
  if (!stat.isDirectory() || stat.isSymbolicLink()) throw new NotebookError("CONFLICT", "Installed Project Notebook payload is not a real directory");
12939
13610
  verifyProjectNotebookSkillExport(payload);
12940
13611
  }
12941
13612
  assertNoSymlinkComponents2(skillsRoot, true);
12942
13613
  mkdirSync7(skillsRoot, { recursive: true, mode: 448 });
12943
13614
  assertNoSymlinkComponents2(skillsRoot);
12944
- let linkExists = existsSync16(link);
13615
+ let linkExists = existsSync17(link);
12945
13616
  if (!linkExists) {
12946
13617
  try {
12947
- lstatSync9(link);
13618
+ lstatSync11(link);
12948
13619
  linkExists = true;
12949
13620
  } catch {
12950
13621
  }
@@ -12961,10 +13632,10 @@ function installPackagedProjectNotebookSkill(input = {}) {
12961
13632
  repair: REPAIR_COMMAND
12962
13633
  }
12963
13634
  });
12964
- const stat = lstatSync9(link);
13635
+ const stat = lstatSync11(link);
12965
13636
  if (!stat.isSymbolicLink()) return foreign(["the path is a real file or directory, not a PJ\xE1ngler-owned link"]);
12966
- const target = realpathSync7(link);
12967
- if (target !== realpathSync7(payload) && !isVerifiedCanonicalSkillexProjection(target)) {
13637
+ const target = realpathSync8(link);
13638
+ if (target !== realpathSync8(payload) && !isVerifiedCanonicalSkillexProjection(target)) {
12968
13639
  return foreign(isCanonicalSkillexProjectionPath(target) ? describeProjectNotebookSkillDrift(target) : [`the link targets ${target}, which is neither the pinned payload nor a canonical Skillex projection`]);
12969
13640
  }
12970
13641
  try {
@@ -12982,25 +13653,25 @@ function collectSupersededPayloads(dataRoot, keep) {
12982
13653
  const kept = /* @__PURE__ */ new Set();
12983
13654
  for (const path of keep) {
12984
13655
  try {
12985
- kept.add(realpathSync7(path));
13656
+ kept.add(realpathSync8(path));
12986
13657
  } catch {
12987
13658
  }
12988
13659
  }
12989
13660
  const removed = [];
12990
13661
  let entries;
12991
13662
  try {
12992
- entries = readdirSync7(dataRoot);
13663
+ entries = readdirSync8(dataRoot);
12993
13664
  } catch {
12994
13665
  return removed;
12995
13666
  }
12996
13667
  for (const name of entries) {
12997
13668
  if (!/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?-[0-9a-f]{64}$/u.test(name)) continue;
12998
- const candidate = join20(dataRoot, name);
13669
+ const candidate = join22(dataRoot, name);
12999
13670
  try {
13000
- const stat = lstatSync9(candidate);
13671
+ const stat = lstatSync11(candidate);
13001
13672
  if (!stat.isDirectory() || stat.isSymbolicLink()) continue;
13002
- if (kept.has(realpathSync7(candidate))) continue;
13003
- rmSync3(candidate, { recursive: true, force: true });
13673
+ if (kept.has(realpathSync8(candidate))) continue;
13674
+ rmSync4(candidate, { recursive: true, force: true });
13004
13675
  removed.push(candidate);
13005
13676
  } catch {
13006
13677
  }
@@ -13011,8 +13682,8 @@ function repairProjectNotebookSkillProjection(input = {}) {
13011
13682
  const env2 = input.env ?? process.env;
13012
13683
  const home = env2.HOME;
13013
13684
  if (!home || !resolve11(home).startsWith("/")) throw new NotebookError("NOT_CONFIGURED", "A trusted HOME is required to repair the Project Notebook skill projection");
13014
- const skillsRoot = join20(home, ".agents", "skills");
13015
- const link = join20(skillsRoot, "project-notebook");
13685
+ const skillsRoot = join22(home, ".agents", "skills");
13686
+ const link = join22(skillsRoot, "project-notebook");
13016
13687
  const probe = probeCanonicalSkillexRootProjection(skillsRoot, link);
13017
13688
  if (probe.state === "adopted") return { status: "clean", summary: "Canonical Skillex projection already matches the version-pinned export", source: probe.source, drift: [], changed_files: [] };
13018
13689
  if (probe.state !== "declined") {
@@ -13025,9 +13696,9 @@ function repairProjectNotebookSkillProjection(input = {}) {
13025
13696
  assertOwnedSkillTree(source);
13026
13697
  const present = [];
13027
13698
  const walk = (directory) => {
13028
- for (const entry of readdirSync7(directory, { withFileTypes: true })) {
13029
- const path = join20(directory, entry.name);
13030
- const rel = relative10(source, path).split(sep6).join("/");
13699
+ for (const entry of readdirSync8(directory, { withFileTypes: true })) {
13700
+ const path = join22(directory, entry.name);
13701
+ const rel = relative11(source, path).split(sep6).join("/");
13031
13702
  if (entry.isDirectory()) {
13032
13703
  walk(path);
13033
13704
  continue;
@@ -13040,11 +13711,11 @@ function repairProjectNotebookSkillProjection(input = {}) {
13040
13711
  walk(source);
13041
13712
  const stale = present.filter((rel) => !wanted.has(rel)).sort();
13042
13713
  const changed = expected.manifest.files.filter((entry) => {
13043
- const path = join20(source, ...entry.path.split("/"));
13044
- if (!existsSync16(path)) return true;
13045
- const stat = lstatSync9(path);
13714
+ const path = join22(source, ...entry.path.split("/"));
13715
+ if (!existsSync17(path)) return true;
13716
+ const stat = lstatSync11(path);
13046
13717
  if (!stat.isFile()) return true;
13047
- if (createHash7("sha256").update(readFileSync14(path)).digest("hex") !== entry.sha256) return true;
13718
+ if (createHash7("sha256").update(readFileSync17(path)).digest("hex") !== entry.sha256) return true;
13048
13719
  const mode = stat.mode & 511;
13049
13720
  const executable = entry.mode === "0755";
13050
13721
  return executable && (mode & 64) === 0 || !executable && (mode & 73) !== 0;
@@ -13059,15 +13730,15 @@ function repairProjectNotebookSkillProjection(input = {}) {
13059
13730
  summary: `Would restore ${affected.length} file(s) in ${source} from the version-pinned export`,
13060
13731
  source,
13061
13732
  drift: probe.block.details,
13062
- changed_files: affected.map((rel) => join20(source, ...rel.split("/")))
13733
+ changed_files: affected.map((rel) => join22(source, ...rel.split("/")))
13063
13734
  };
13064
13735
  }
13065
- for (const rel of stale) rmSync3(join20(source, ...rel.split("/")), { force: true });
13736
+ for (const rel of stale) rmSync4(join22(source, ...rel.split("/")), { force: true });
13066
13737
  for (const rel of changed) {
13067
- const destination = join20(source, ...rel.split("/"));
13738
+ const destination = join22(source, ...rel.split("/"));
13068
13739
  const entry = expected.manifest.files.find((item) => item.path === rel);
13069
13740
  mkdirSync7(dirname10(destination), { recursive: true, mode: 493 });
13070
- copyFileSync3(join20(expected.source, ...rel.split("/")), destination);
13741
+ copyFileSync3(join22(expected.source, ...rel.split("/")), destination);
13071
13742
  chmodSync4(destination, entry.mode === "0755" ? 493 : 420);
13072
13743
  }
13073
13744
  verifyProjectNotebookSkillExport(source);
@@ -13076,21 +13747,21 @@ function repairProjectNotebookSkillProjection(input = {}) {
13076
13747
  summary: `Restored ${affected.length} file(s) in ${source} from the version-pinned export; commit the change in the Skillex checkout`,
13077
13748
  source,
13078
13749
  drift: probe.block.details,
13079
- changed_files: affected.map((rel) => join20(source, ...rel.split("/")))
13750
+ changed_files: affected.map((rel) => join22(source, ...rel.split("/")))
13080
13751
  };
13081
13752
  }
13082
13753
  function inspectProjectNotebookIntegration(env2 = process.env) {
13083
13754
  const home = env2.HOME;
13084
13755
  if (!home) return { skill_installed: false, hooks_projected: false, details: ["HOME is unavailable"] };
13085
- const skillsRoot = join20(home, ".agents", "skills");
13086
- const link = join20(skillsRoot, "project-notebook");
13756
+ const skillsRoot = join22(home, ".agents", "skills");
13757
+ const link = join22(skillsRoot, "project-notebook");
13087
13758
  try {
13088
13759
  const expected = expectedPackedSkill();
13089
- const dataRoot = resolve11(env2.XDG_DATA_HOME || join20(home, ".local", "share"), "pjangler", "skills", "project-notebook");
13090
- const expectedPayload = join20(dataRoot, `${PJANGLER_VERSION}-${expected.digest}`);
13760
+ const dataRoot = resolve11(env2.XDG_DATA_HOME || join22(home, ".local", "share"), "pjangler", "skills", "project-notebook");
13761
+ const expectedPayload = join22(dataRoot, `${PJANGLER_VERSION}-${expected.digest}`);
13091
13762
  const probe = probeCanonicalSkillexRootProjection(skillsRoot, link);
13092
13763
  if (probe.state === "declined") return { skill_installed: false, hooks_projected: false, details: probe.block.details, blocked: probe.block };
13093
- const stat = lstatSync9(link);
13764
+ const stat = lstatSync11(link);
13094
13765
  if (!stat.isSymbolicLink()) {
13095
13766
  return {
13096
13767
  skill_installed: false,
@@ -13099,8 +13770,8 @@ function inspectProjectNotebookIntegration(env2 = process.env) {
13099
13770
  blocked: { code: "projection-foreign", summary: `${link} is not a PJ\xE1ngler-owned link`, details: ["the path is a real file or directory"], repair: REPAIR_COMMAND }
13100
13771
  };
13101
13772
  }
13102
- const source = realpathSync7(link);
13103
- const packedPayloadMatches = existsSync16(expectedPayload) && source === realpathSync7(expectedPayload);
13773
+ const source = realpathSync8(link);
13774
+ const packedPayloadMatches = existsSync17(expectedPayload) && source === realpathSync8(expectedPayload);
13104
13775
  if (!packedPayloadMatches && !isVerifiedCanonicalSkillexProjection(source)) {
13105
13776
  const details = isCanonicalSkillexProjectionPath(source) ? describeProjectNotebookSkillDrift(source) : [`the link targets ${source}, which is neither the version-pinned payload nor a canonical Skillex projection`];
13106
13777
  return {
@@ -13127,8 +13798,8 @@ function inspectProjectNotebookIntegration(env2 = process.env) {
13127
13798
  }
13128
13799
  }
13129
13800
  function projectorArguments(source, command, input) {
13130
- const script = join20(source, "scripts", "project-hooks.py");
13131
- const args = [script, command, "--master", join20(source, "hooks", "hooks.master.json"), "--fragment", join20(source, "hooks", "claude.settings.json"), "--target", input.target];
13801
+ const script = join22(source, "scripts", "project-hooks.py");
13802
+ const args = [script, command, "--master", join22(source, "hooks", "hooks.master.json"), "--fragment", join22(source, "hooks", "claude.settings.json"), "--target", input.target];
13132
13803
  if (command === "check") args.push("--json");
13133
13804
  else if (input.stateHome) args.push("--state-home", input.stateHome);
13134
13805
  return args;
@@ -13150,7 +13821,7 @@ function checkProjectNotebookHooks(input = {}) {
13150
13821
  verifyProjectNotebookSkillExport(source);
13151
13822
  const home = env2.HOME;
13152
13823
  if (!home) throw new NotebookError("NOT_CONFIGURED", "HOME is required to check Project Notebook hooks");
13153
- const target = resolve11(input.target ?? env2.PJ_PROJECT_NOTEBOOK_CLAUDE_SETTINGS ?? join20(home, ".claude", "settings.json"));
13824
+ const target = resolve11(input.target ?? env2.PJ_PROJECT_NOTEBOOK_CLAUDE_SETTINGS ?? join22(home, ".claude", "settings.json"));
13154
13825
  const result2 = spawnSync11("/usr/bin/python3", ["-I", ...projectorArguments(source, "check", { target })], { encoding: "utf8", env: projectorEnvironment(env2), timeout: 5e3, maxBuffer: 1048576 });
13155
13826
  if (result2.status !== 0 && result2.status !== 1) throw new NotebookError("CONFLICT", (result2.stderr || "Project Notebook projector check failed").trim().slice(0, 512));
13156
13827
  try {
@@ -13165,17 +13836,17 @@ function installProjectNotebookIntegration(input = {}) {
13165
13836
  const env2 = input.env ?? process.env;
13166
13837
  const skill = installPackagedProjectNotebookSkill({ source: input.source, env: env2 });
13167
13838
  if (skill.blocked) return { skill, hooksChanged: false, blocked: skill.blocked };
13168
- const source = realpathSync7(skill.path);
13839
+ const source = realpathSync8(skill.path);
13169
13840
  const home = env2.HOME;
13170
13841
  if (!home) throw new NotebookError("NOT_CONFIGURED", "HOME is required to install Project Notebook hooks");
13171
- const target = resolve11(input.target ?? env2.PJ_PROJECT_NOTEBOOK_CLAUDE_SETTINGS ?? join20(home, ".claude", "settings.json"));
13172
- const stateHome = resolve11(env2.XDG_STATE_HOME || join20(home, ".local", "state"));
13842
+ const target = resolve11(input.target ?? env2.PJ_PROJECT_NOTEBOOK_CLAUDE_SETTINGS ?? join22(home, ".claude", "settings.json"));
13843
+ const stateHome = resolve11(env2.XDG_STATE_HOME || join22(home, ".local", "state"));
13173
13844
  const result2 = spawnSync11("/usr/bin/python3", ["-I", ...projectorArguments(source, "install", { target, stateHome })], { encoding: "utf8", env: projectorEnvironment(env2), timeout: 5e3, maxBuffer: 1048576 });
13174
13845
  if (result2.status !== 0) {
13175
13846
  if (skill.installed) {
13176
13847
  try {
13177
- const stat = lstatSync9(skill.path);
13178
- if (stat.isSymbolicLink() && realpathSync7(skill.path) === source) unlinkSync6(skill.path);
13848
+ const stat = lstatSync11(skill.path);
13849
+ if (stat.isSymbolicLink() && realpathSync8(skill.path) === source) unlinkSync5(skill.path);
13179
13850
  } catch {
13180
13851
  }
13181
13852
  }
@@ -13192,10 +13863,10 @@ function assertNoSymlinkComponents2(path, allowMissing = false) {
13192
13863
  const root = parse3(absolute).root;
13193
13864
  let cursor = root;
13194
13865
  for (const component of absolute.slice(root.length).split(sep6).filter(Boolean)) {
13195
- cursor = join20(cursor, component);
13866
+ cursor = join22(cursor, component);
13196
13867
  let stat;
13197
13868
  try {
13198
- stat = lstatSync9(cursor);
13869
+ stat = lstatSync11(cursor);
13199
13870
  } catch (error) {
13200
13871
  if (allowMissing && error.code === "ENOENT") return;
13201
13872
  throw error;
@@ -13281,13 +13952,13 @@ async function prepareNotebookObservationResolved(module, resolved, config, loca
13281
13952
  health: healthy ? "healthy" : "drifted",
13282
13953
  notebook_check: notebookCheck,
13283
13954
  notebook,
13284
- scoped_notes: notes.map((note2) => ({
13285
- id: note2.id,
13286
- title: note2.title,
13287
- note_type: note2.note_type,
13288
- created_at: note2.created_at,
13289
- updated_at: note2.updated_at,
13290
- envelope_logical_id: parseNoteEnvelope(note2.content)?.envelope.logical_id ?? null
13955
+ scoped_notes: notes.map((note) => ({
13956
+ id: note.id,
13957
+ title: note.title,
13958
+ note_type: note.note_type,
13959
+ created_at: note.created_at,
13960
+ updated_at: note.updated_at,
13961
+ envelope_logical_id: parseNoteEnvelope(note.content)?.envelope.logical_id ?? null
13291
13962
  })),
13292
13963
  overview,
13293
13964
  error: null
@@ -13350,9 +14021,9 @@ function ensureFinalNoteContent(value, maxBytes) {
13350
14021
  return value;
13351
14022
  }
13352
14023
  function getScoped(notes, noteId) {
13353
- const note2 = notes.find((item) => item.id === noteId);
13354
- if (!note2) throw new NotebookError("NOT_FOUND", `Note is not a proven member of this project notebook: ${noteId}`);
13355
- return note2;
14024
+ const note = notes.find((item) => item.id === noteId);
14025
+ if (!note) throw new NotebookError("NOT_FOUND", `Note is not a proven member of this project notebook: ${noteId}`);
14026
+ return note;
13356
14027
  }
13357
14028
  var NotebookModule = class {
13358
14029
  registryPath;
@@ -13564,8 +14235,8 @@ var NotebookModule = class {
13564
14235
  async deleteNote(repo, noteId, confirmed) {
13565
14236
  if (!confirmed) throw new NotebookError("INVALID_INPUT", "Note deletion requires confirmation or --yes");
13566
14237
  const { ctx, notes, notebookId } = await this.scoped(repo);
13567
- const note2 = getScoped(notes, noteId);
13568
- const parsed = parseNoteEnvelope(note2.content);
14238
+ const note = getScoped(notes, noteId);
14239
+ const parsed = parseNoteEnvelope(note.content);
13569
14240
  if (parsed && parsed.envelope.project_slug !== ctx.config.project_slug) throw new NotebookError("CROSS_PROJECT", "Managed note envelope belongs to a different project");
13570
14241
  if (noteId === ctx.config.binding.overview_note_id || parsed?.envelope.kind === "overview") throw new NotebookError("CONFLICT", "The stable Project Overview note cannot be deleted");
13571
14242
  await ctx.client.deleteOwnedNote(notebookId, noteId);
@@ -13674,17 +14345,17 @@ init_remote_mutation_journal();
13674
14345
  function integrationEnvironment(module, ctx) {
13675
14346
  const env2 = { ...module.environment, HOME: ctx.homeDir };
13676
14347
  if (module.environment.HOME !== ctx.homeDir) {
13677
- env2.XDG_DATA_HOME = join21(ctx.homeDir, ".local", "share");
13678
- env2.XDG_STATE_HOME = join21(ctx.homeDir, ".local", "state");
13679
- env2.PJ_PROJECT_NOTEBOOK_CLAUDE_SETTINGS = join21(ctx.homeDir, ".claude", "settings.json");
14348
+ env2.XDG_DATA_HOME = join23(ctx.homeDir, ".local", "share");
14349
+ env2.XDG_STATE_HOME = join23(ctx.homeDir, ".local", "state");
14350
+ env2.PJ_PROJECT_NOTEBOOK_CLAUDE_SETTINGS = join23(ctx.homeDir, ".claude", "settings.json");
13680
14351
  }
13681
14352
  return env2;
13682
14353
  }
13683
14354
  function resolvedForPlan(plan) {
13684
- const manifestPath = join21(plan.project.repo_path, ".project.json");
14355
+ const manifestPath = join23(plan.project.repo_path, ".project.json");
13685
14356
  let manifest = plan.manifest;
13686
- if (existsSync17(manifestPath)) {
13687
- const parsed = JSON.parse(readFileSync15(manifestPath, "utf8"));
14357
+ if (existsSync18(manifestPath)) {
14358
+ const parsed = JSON.parse(readFileSync18(manifestPath, "utf8"));
13688
14359
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error(`${manifestPath} must contain a JSON object`);
13689
14360
  manifest = parsed;
13690
14361
  }
@@ -13845,7 +14516,7 @@ function unsafeToRemove(targetDir) {
13845
14516
  while (!seen.has(cursor)) {
13846
14517
  seen.add(cursor);
13847
14518
  try {
13848
- const stat = lstatSync10(cursor);
14519
+ const stat = lstatSync12(cursor);
13849
14520
  if (stat.isSymbolicLink()) {
13850
14521
  return cursor === absolute ? `${absolute} is a symlink; removing it would leave the tree it points at orphaned` : `${cursor} is a symlink on the path to ${absolute}; a recursive remove would traverse it`;
13851
14522
  }
@@ -13873,12 +14544,12 @@ function publicMigration(report) {
13873
14544
  };
13874
14545
  }
13875
14546
  function hasGitRepository(runtime, targetDir) {
13876
- if (!existsSync18(join22(targetDir, ".git"))) return false;
14547
+ if (!existsSync19(join24(targetDir, ".git"))) return false;
13877
14548
  return runtime.runGit(targetDir, ["rev-parse", "--is-inside-work-tree"]).status === 0;
13878
14549
  }
13879
14550
  function refreshPlanFromCanonicalManifest(plan) {
13880
- const manifestPath = join22(plan.project.repo_path, ".project.json");
13881
- const manifest = JSON.parse(readFileSync16(manifestPath, "utf8"));
14551
+ const manifestPath = join24(plan.project.repo_path, ".project.json");
14552
+ const manifest = JSON.parse(readFileSync19(manifestPath, "utf8"));
13882
14553
  if (!manifest || typeof manifest !== "object" || Array.isArray(manifest)) {
13883
14554
  throw new Error(`${manifestPath} must contain a JSON object`);
13884
14555
  }
@@ -13898,12 +14569,25 @@ function refreshPlanFromCanonicalManifest(plan) {
13898
14569
  if (!manifestTicket || typeof manifestTicket !== "object") {
13899
14570
  throw new Error(`${manifestPath} ticket_provider is missing`);
13900
14571
  }
14572
+ const manifestIdentifier = String(manifestTicket.identifier ?? "");
14573
+ const manifestBoardId = String(manifestTicket.board_id ?? "");
14574
+ const manifestType = String(manifestTicket.type ?? "");
14575
+ const recorded = plan.project.ticket_provider;
14576
+ const carriesProvenance = (recorded?.identifier ?? "") === manifestIdentifier && (recorded?.board_id ?? "") === manifestBoardId;
14577
+ const identifierSource = (carriesProvenance ? recorded?.identifier_source : void 0) ?? "proposed";
14578
+ const manifestConfirmedAt = typeof manifestTicket.board_confirmed_at === "string" ? manifestTicket.board_confirmed_at.trim() : "";
14579
+ const boardConfirmedAt = manifestConfirmedAt || ((recorded?.board_id ?? "") === manifestBoardId ? recorded?.board_confirmed_at ?? "" : "");
14580
+ const keyIsProven = !providerAssignsIdentifiers(manifestType) || identifierSource === "provider";
14581
+ const manifestState = typeof manifestTicket.state === "string" ? manifestTicket.state : void 0;
13901
14582
  const ticketProvider = {
13902
- type: String(manifestTicket.type ?? ""),
14583
+ type: manifestType,
13903
14584
  workspace: String(manifestTicket.workspace ?? ""),
13904
- identifier: String(manifestTicket.identifier ?? ""),
13905
- board_id: String(manifestTicket.board_id ?? ""),
13906
- state: manifestTicket.state
14585
+ identifier: manifestIdentifier,
14586
+ identifier_source: identifierSource,
14587
+ ...carriesProvenance && identifierSource === "provider" && recorded?.identifier_fetched_at ? { identifier_fetched_at: recorded.identifier_fetched_at } : {},
14588
+ board_id: manifestBoardId,
14589
+ ...boardConfirmedAt ? { board_confirmed_at: boardConfirmedAt } : {},
14590
+ state: manifestState === "skipped" ? "skipped" : manifestBoardId && boardConfirmedAt && keyIsProven ? "linked" : "planned"
13907
14591
  };
13908
14592
  plan.manifest = manifest;
13909
14593
  plan.project.agents = agents;
@@ -13950,7 +14634,7 @@ var ProjectRecipe = class extends Recipe {
13950
14634
  const logs = [];
13951
14635
  const errors = [];
13952
14636
  const changedFiles = [];
13953
- const targetExistedAtStart = existsSync18(targetDir);
14637
+ const targetExistedAtStart = existsSync19(targetDir);
13954
14638
  const transactionContext = {
13955
14639
  ...ctx,
13956
14640
  targetDir,
@@ -14039,7 +14723,6 @@ var ProjectRecipe = class extends Recipe {
14039
14723
  force: Boolean(ctx.force),
14040
14724
  skipTelegram: true,
14041
14725
  skipEmail: true,
14042
- skipRuntimeRepo: agentAction.context.skipRuntimeRepo,
14043
14726
  skipPlane: agentAction.context.skipPlane,
14044
14727
  skipBloodbank: agentAction.context.skipBloodbank,
14045
14728
  skipSystemd: agentAction.context.skipSystemd,
@@ -14113,7 +14796,7 @@ var ProjectRecipe = class extends Recipe {
14113
14796
  if (hasGitRepository(this.runtime, targetDir)) {
14114
14797
  phases.push({ id: "project.git", status: "unchanged", changedFiles: [], message: "Git repository already initialized" });
14115
14798
  } else {
14116
- const gitPath = join22(targetDir, ".git");
14799
+ const gitPath = join24(targetDir, ".git");
14117
14800
  for (const { args, label, options } of [
14118
14801
  { args: ["init", "--initial-branch=main"], label: "git init" },
14119
14802
  { args: ["add", "-A"], label: "git add" },
@@ -14129,7 +14812,7 @@ var ProjectRecipe = class extends Recipe {
14129
14812
  phases.push({ id: `project.git:${label}`, status: "failed", changedFiles: changedFiles.includes(gitPath) ? [gitPath] : [], message: errors.at(-1) });
14130
14813
  break;
14131
14814
  }
14132
- if (label === "git init" && existsSync18(gitPath)) changedFiles.push(gitPath);
14815
+ if (label === "git init" && existsSync19(gitPath)) changedFiles.push(gitPath);
14133
14816
  logs.push(`${label}: ok`);
14134
14817
  }
14135
14818
  if (errors.length === 0) {
@@ -14137,7 +14820,7 @@ var ProjectRecipe = class extends Recipe {
14137
14820
  const headReady = repositoryReady && this.runtime.runGit(targetDir, ["rev-parse", "--verify", "HEAD"]).status === 0;
14138
14821
  if (!headReady) {
14139
14822
  errors.push("git postcondition failed: repository or initial commit is missing");
14140
- phases.push({ id: "project.git:postcondition", status: "failed", changedFiles: existsSync18(gitPath) ? [gitPath] : [], message: errors.at(-1) });
14823
+ phases.push({ id: "project.git:postcondition", status: "failed", changedFiles: existsSync19(gitPath) ? [gitPath] : [], message: errors.at(-1) });
14141
14824
  } else {
14142
14825
  if (!changedFiles.includes(gitPath)) changedFiles.push(gitPath);
14143
14826
  phases.push({ id: "project.git", status: "changed", changedFiles: [gitPath], message: "Git repository initialized and committed" });
@@ -14207,7 +14890,7 @@ var ProjectRecipe = class extends Recipe {
14207
14890
  }
14208
14891
  }
14209
14892
  const deferred = provisionedAgentContext?.deferredExternalEffects;
14210
- if (errors.length === 0 && deferred?.owner === "project" && (deferred.runtimeRepo || deferred.ticketBoard || deferred.systemd)) {
14893
+ if (errors.length === 0 && deferred?.owner === "project" && (deferred.ticketBoard || deferred.systemd)) {
14211
14894
  externalDispatchStarted = true;
14212
14895
  rollbackEligible = false;
14213
14896
  const beforeExternal = snapshotTree(targetDir);
@@ -14270,7 +14953,7 @@ var ProjectRecipe = class extends Recipe {
14270
14953
  message: errors.at(-1)
14271
14954
  });
14272
14955
  }
14273
- if (errors.length > 0 && mode === "create" && !targetExistedAtStart && rollbackEligible && existsSync18(targetDir)) {
14956
+ if (errors.length > 0 && mode === "create" && !targetExistedAtStart && rollbackEligible && existsSync19(targetDir)) {
14274
14957
  const unsafe = unsafeToRemove(targetDir);
14275
14958
  if (unsafe) {
14276
14959
  errors.push(`fresh-target rollback refused: ${unsafe}`);
@@ -14281,10 +14964,10 @@ var ProjectRecipe = class extends Recipe {
14281
14964
  message: errors.at(-1)
14282
14965
  });
14283
14966
  } else try {
14284
- rmSync4(targetDir, { recursive: true, force: true });
14967
+ rmSync5(targetDir, { recursive: true, force: true });
14285
14968
  const insideTarget = (path) => {
14286
- const relative11 = relativePath(resolve13(targetDir), resolve13(path));
14287
- return relative11 === "" || !relative11.startsWith("..") && !isAbsolute5(relative11);
14969
+ const relative12 = relativePath(resolve13(targetDir), resolve13(path));
14970
+ return relative12 === "" || !relative12.startsWith("..") && !isAbsolute5(relative12);
14288
14971
  };
14289
14972
  const orphaned = [...new Set(changedFiles.filter((path) => !insideTarget(path)))].sort();
14290
14973
  changedFiles.length = 0;
@@ -14626,8 +15309,8 @@ var recipeRegistry = new RecipeRegistry([
14626
15309
 
14627
15310
  // src/commands/AgentHooksCommands.ts
14628
15311
  import { homedir as homedir8 } from "node:os";
14629
- import { join as join23, dirname as dirname12 } from "node:path";
14630
- import { existsSync as existsSync19, cpSync as cpSync2, mkdirSync as mkdirSync8, readFileSync as readFileSync17, writeFileSync as writeFileSync11 } from "node:fs";
15312
+ import { join as join25, dirname as dirname12 } from "node:path";
15313
+ import { existsSync as existsSync20, cpSync as cpSync2, mkdirSync as mkdirSync8, readFileSync as readFileSync20, writeFileSync as writeFileSync11 } from "node:fs";
14631
15314
  import { fileURLToPath as fileURLToPath6 } from "node:url";
14632
15315
  init_project();
14633
15316
  var AGENT_HOOKS_SKIP_MESSAGE = "\u21B7 agent-hooks layer skipped: global ~/.agents/hooks detected (these hooks already run globally).\n Set PJ_AGENT_HOOKS_LAYER=1 to install the project-scoped layer anyway.";
@@ -14639,16 +15322,16 @@ function resolveTemplateRoot() {
14639
15322
  try {
14640
15323
  let dir = dirname12(fileURLToPath6(import.meta.url));
14641
15324
  for (let i = 0; i < 8; i++) {
14642
- candidates.push(join23(dir, "templates", "commonproject", "template"));
15325
+ candidates.push(join25(dir, "templates", "commonproject", "template"));
14643
15326
  const parent = dirname12(dir);
14644
15327
  if (parent === dir) break;
14645
15328
  dir = parent;
14646
15329
  }
14647
15330
  } catch {
14648
15331
  }
14649
- candidates.push(join23(homedir8(), "code", "pjangler", "templates", "commonproject", "template"));
15332
+ candidates.push(join25(homedir8(), "code", "pjangler", "templates", "commonproject", "template"));
14650
15333
  for (const c of candidates) {
14651
- if (existsSync19(join23(c, ".agents", "hooks", "hooks.master.json"))) return c;
15334
+ if (existsSync20(join25(c, ".agents", "hooks", "hooks.master.json"))) return c;
14652
15335
  }
14653
15336
  throw new Error(
14654
15337
  "Could not locate the CommonProject template. Set PJANGLER_COMMONPROJECT_TEMPLATE to <repo>/templates/commonproject/template."
@@ -14674,10 +15357,10 @@ var CopyAgentHooksTree = class extends Command {
14674
15357
  const created = [];
14675
15358
  const skipped = [];
14676
15359
  for (const { rel, dir } of items) {
14677
- const src = join23(templateRoot, rel);
14678
- const dest = join23(this.context.targetDir, rel);
14679
- if (!existsSync19(src)) continue;
14680
- if (existsSync19(dest) && !this.context.force) {
15360
+ const src = join25(templateRoot, rel);
15361
+ const dest = join25(this.context.targetDir, rel);
15362
+ if (!existsSync20(src)) continue;
15363
+ if (existsSync20(dest) && !this.context.force) {
14681
15364
  skipped.push(rel);
14682
15365
  continue;
14683
15366
  }
@@ -14703,14 +15386,14 @@ var WireMiseAgentHooks = class _WireMiseAgentHooks extends Command {
14703
15386
  if (!resolveAgentHooksLayer2()) {
14704
15387
  return { success: true, message: this.formatMessage(AGENT_HOOKS_SKIP_MESSAGE) };
14705
15388
  }
14706
- const misePath = join23(this.context.targetDir, "mise.toml");
14707
- if (!existsSync19(misePath)) {
15389
+ const misePath = join25(this.context.targetDir, "mise.toml");
15390
+ if (!existsSync20(misePath)) {
14708
15391
  return {
14709
15392
  success: false,
14710
15393
  message: "\u26A0\uFE0F No mise.toml found \u2014 run `pjangler init mise` first, then re-run."
14711
15394
  };
14712
15395
  }
14713
- let content = readFileSync17(misePath, "utf8");
15396
+ let content = readFileSync20(misePath, "utf8");
14714
15397
  if (content.includes(_WireMiseAgentHooks.MARKER)) {
14715
15398
  return { success: true, message: this.formatMessage("\u2713 mise.toml already wired for agent-hooks") };
14716
15399
  }
@@ -14910,7 +15593,7 @@ if __name__ == "__main__":
14910
15593
 
14911
15594
  // src/commands/AddMiseCodegraphScript.ts
14912
15595
  import { chmodSync as chmodSync5 } from "fs";
14913
- import { join as join24 } from "path";
15596
+ import { join as join26 } from "path";
14914
15597
  var AddMiseCodegraphScript = class extends Command {
14915
15598
  async invoke() {
14916
15599
  const filePath = ".mise/scripts/codegraph.sh";
@@ -14964,7 +15647,7 @@ fi
14964
15647
  `;
14965
15648
  this.writeFile(filePath, content);
14966
15649
  if (!this.context.dryRun) {
14967
- chmodSync5(join24(this.context.targetDir, filePath), 493);
15650
+ chmodSync5(join26(this.context.targetDir, filePath), 493);
14968
15651
  }
14969
15652
  return {
14970
15653
  success: true,
@@ -15000,14 +15683,14 @@ SECRET_KEY=""
15000
15683
  };
15001
15684
 
15002
15685
  // src/parity/index.ts
15003
- import { existsSync as existsSync20 } from "node:fs";
15686
+ import { existsSync as existsSync21 } from "node:fs";
15004
15687
  import { homedir as homedir9 } from "node:os";
15005
- import { dirname as dirname13, join as join25, resolve as resolve14 } from "node:path";
15688
+ import { dirname as dirname13, join as join27, resolve as resolve14 } from "node:path";
15006
15689
  import { fileURLToPath as fileURLToPath7 } from "node:url";
15007
15690
  function resolvePjanglerRoot2() {
15008
15691
  let dir = dirname13(fileURLToPath7(import.meta.url));
15009
15692
  while (dir !== dirname13(dir)) {
15010
- if (existsSync20(join25(dir, "package.json")) && existsSync20(join25(dir, "templates", "commonproject", "copier.yml"))) return dir;
15693
+ if (existsSync21(join27(dir, "package.json")) && existsSync21(join27(dir, "templates", "commonproject", "copier.yml"))) return dir;
15011
15694
  dir = dirname13(dir);
15012
15695
  }
15013
15696
  return resolve14(process.cwd());
@@ -15169,15 +15852,18 @@ function getRecipeInfo(name) {
15169
15852
  return RECIPE_REGISTRY[name] || null;
15170
15853
  }
15171
15854
 
15855
+ // src/mcp-server.ts
15856
+ init_version();
15857
+
15172
15858
  // src/describe/index.ts
15173
- import { existsSync as existsSync21, readFileSync as readFileSync18, readdirSync as readdirSync8, statSync as statSync4 } from "node:fs";
15174
- import { join as join27, resolve as resolve15 } from "node:path";
15859
+ import { existsSync as existsSync22, readFileSync as readFileSync21, readdirSync as readdirSync9, statSync as statSync4 } from "node:fs";
15860
+ import { join as join29, resolve as resolve15 } from "node:path";
15175
15861
  init_project();
15176
15862
 
15177
15863
  // src/describe/activity.ts
15178
15864
  import { spawn as spawn2, spawnSync as spawnSync14 } from "node:child_process";
15179
15865
  import { statSync as statSync3 } from "node:fs";
15180
- import { join as join26 } from "node:path";
15866
+ import { join as join28 } from "node:path";
15181
15867
  var ACTIVE_WINDOW_SECONDS = 24 * 60 * 60;
15182
15868
  var MAX_DIRTY_STATS = 500;
15183
15869
  var GIT_TIMEOUT_MS = 5e3;
@@ -15306,7 +15992,7 @@ function uncommittedSource(repo, paths) {
15306
15992
  let newest = 0;
15307
15993
  for (const path of paths.slice(0, MAX_DIRTY_STATS)) {
15308
15994
  try {
15309
- const mtime = Math.floor(statSync3(join26(repo, path)).mtimeMs / 1e3);
15995
+ const mtime = Math.floor(statSync3(join28(repo, path)).mtimeMs / 1e3);
15310
15996
  if (mtime > newest) newest = mtime;
15311
15997
  } catch {
15312
15998
  }
@@ -15415,7 +16101,7 @@ var CONFIG_FILES = [
15415
16101
  ];
15416
16102
  function readJson(path) {
15417
16103
  try {
15418
- const parsed = JSON.parse(readFileSync18(path, "utf8"));
16104
+ const parsed = JSON.parse(readFileSync21(path, "utf8"));
15419
16105
  return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : void 0;
15420
16106
  } catch {
15421
16107
  return void 0;
@@ -15435,39 +16121,39 @@ function describeType(repo) {
15435
16121
  const languages = [];
15436
16122
  const roles = [];
15437
16123
  const evidence = [];
15438
- const note2 = (signal, file) => evidence.push(`${signal} (${file})`);
16124
+ const note = (signal, file) => evidence.push(`${signal} (${file})`);
15439
16125
  for (const marker of LANGUAGE_MARKERS) {
15440
- if (!existsSync21(join27(repo, marker.file))) continue;
16126
+ if (!existsSync22(join29(repo, marker.file))) continue;
15441
16127
  if (!languages.includes(marker.language)) {
15442
16128
  languages.push(marker.language);
15443
- note2(marker.language, marker.file);
16129
+ note(marker.language, marker.file);
15444
16130
  }
15445
16131
  }
15446
16132
  try {
15447
- const dotnet = readdirSync8(repo).find((entry) => entry.endsWith(".csproj") || entry.endsWith(".sln"));
16133
+ const dotnet = readdirSync9(repo).find((entry) => entry.endsWith(".csproj") || entry.endsWith(".sln"));
15448
16134
  if (dotnet && !languages.includes("dotnet")) {
15449
16135
  languages.push("dotnet");
15450
- note2("dotnet", dotnet);
16136
+ note("dotnet", dotnet);
15451
16137
  }
15452
16138
  } catch {
15453
16139
  }
15454
- const pkg = readJson(join27(repo, "package.json"));
16140
+ const pkg = readJson(join29(repo, "package.json"));
15455
16141
  if (pkg) {
15456
- if (existsSync21(join27(repo, "tsconfig.json"))) {
16142
+ if (existsSync22(join29(repo, "tsconfig.json"))) {
15457
16143
  const index = languages.indexOf("javascript");
15458
16144
  if (index >= 0) languages.splice(index, 1);
15459
16145
  if (!languages.includes("typescript")) {
15460
16146
  languages.unshift("typescript");
15461
- note2("typescript", "tsconfig.json");
16147
+ note("typescript", "tsconfig.json");
15462
16148
  }
15463
16149
  }
15464
16150
  if (pkg.bin) {
15465
16151
  roles.push("cli");
15466
- note2("cli", "package.json#bin");
16152
+ note("cli", "package.json#bin");
15467
16153
  }
15468
16154
  if (pkg.workspaces) {
15469
16155
  roles.push("monorepo");
15470
- note2("monorepo", "package.json#workspaces");
16156
+ note("monorepo", "package.json#workspaces");
15471
16157
  }
15472
16158
  const dependencies = {
15473
16159
  ...pkg.dependencies,
@@ -15475,7 +16161,7 @@ function describeType(repo) {
15475
16161
  };
15476
16162
  if (Object.keys(dependencies).some((name) => name.startsWith("@modelcontextprotocol/"))) {
15477
16163
  roles.push("mcp-server");
15478
- note2("mcp-server", "package.json#@modelcontextprotocol");
16164
+ note("mcp-server", "package.json#@modelcontextprotocol");
15479
16165
  }
15480
16166
  }
15481
16167
  const roleMarkers = [
@@ -15487,14 +16173,14 @@ function describeType(repo) {
15487
16173
  ["hermes-fleet-host", "agents/hermes"]
15488
16174
  ];
15489
16175
  for (const [role, marker] of roleMarkers) {
15490
- if (!existsSync21(join27(repo, marker))) continue;
16176
+ if (!existsSync22(join29(repo, marker))) continue;
15491
16177
  roles.push(role);
15492
- note2(role, marker);
16178
+ note(role, marker);
15493
16179
  }
15494
16180
  return { primaryLanguage: languages[0], languages, roles, evidence };
15495
16181
  }
15496
16182
  function describeIdentity(repo, registryPath2) {
15497
- const manifestPath = join27(repo, ".project.json");
16183
+ const manifestPath = join29(repo, ".project.json");
15498
16184
  const manifest = readJson(manifestPath);
15499
16185
  const drift = [];
15500
16186
  let record;
@@ -15581,12 +16267,12 @@ function describeSubsystems(repo, findings) {
15581
16267
  const markers = SUBSYSTEM_MARKERS[metadata.id] ?? [];
15582
16268
  const evidence = metadata.id === "notebook" ? (() => {
15583
16269
  try {
15584
- const manifest = JSON.parse(readFileSync18(join27(repo, ".project.json"), "utf8"));
16270
+ const manifest = JSON.parse(readFileSync21(join29(repo, ".project.json"), "utf8"));
15585
16271
  return manifest.notebook && typeof manifest.notebook === "object" ? [".project.json#notebook"] : [];
15586
16272
  } catch {
15587
16273
  return [];
15588
16274
  }
15589
- })() : markers.filter((marker) => existsSync21(join27(repo, marker)));
16275
+ })() : markers.filter((marker) => existsSync22(join29(repo, marker)));
15590
16276
  const rules = (byRecipe.get(metadata.id) ?? []).map((finding2) => ({
15591
16277
  id: finding2.id,
15592
16278
  title: finding2.title,
@@ -15605,7 +16291,7 @@ function describeNotebook(repo, registryPath2) {
15605
16291
  try {
15606
16292
  const registry = loadProjectRegistry(registryPath2);
15607
16293
  const project = Object.values(registry.projects).find((entry) => resolve15(entry.repo_path) === resolve15(repo));
15608
- const manifest = existsSync21(join27(repo, ".project.json")) ? JSON.parse(readFileSync18(join27(repo, ".project.json"), "utf8")) : void 0;
16294
+ const manifest = existsSync22(join29(repo, ".project.json")) ? JSON.parse(readFileSync21(join29(repo, ".project.json"), "utf8")) : void 0;
15609
16295
  const declared = Boolean(project?.notebook || manifest?.notebook && typeof manifest.notebook === "object");
15610
16296
  if (!declared) return { declared: false, bindingState: null, notebookId: null, overviewNoteId: null, health: null, remoteCheck: "skip", captureAdmission: null };
15611
16297
  const config = loadEffectiveNotebookConfig(repo, registryPath2);
@@ -15623,7 +16309,7 @@ function describeNotebook(repo, registryPath2) {
15623
16309
  }
15624
16310
  }
15625
16311
  function describeConfigFiles(repo) {
15626
- return CONFIG_FILES.filter((spec) => existsSync21(join27(repo, spec.path))).map((spec) => ({ path: spec.path, purpose: spec.purpose, subsystem: spec.subsystem }));
16312
+ return CONFIG_FILES.filter((spec) => existsSync22(join29(repo, spec.path))).map((spec) => ({ path: spec.path, purpose: spec.purpose, subsystem: spec.subsystem }));
15627
16313
  }
15628
16314
  function describeNextSteps(description, findings) {
15629
16315
  const steps = [];
@@ -15702,7 +16388,7 @@ function describeNextSteps(description, findings) {
15702
16388
  }
15703
16389
  function describeProject(input = {}) {
15704
16390
  const repo = resolve15(input.repoArg ?? process.cwd());
15705
- if (!existsSync21(repo)) throw new Error(`Path does not exist: ${repo}`);
16391
+ if (!existsSync22(repo)) throw new Error(`Path does not exist: ${repo}`);
15706
16392
  if (!statSync4(repo).isDirectory()) throw new Error(`Not a directory: ${repo}`);
15707
16393
  const registryPath2 = input.registryPath ?? projectRegistryPath();
15708
16394
  const report = recipeRegistry.auditRecipes(lifecycleContext(repo, true, false, { registryPath: registryPath2 }));
@@ -15855,6 +16541,7 @@ var server = new McpServer({
15855
16541
  });
15856
16542
  var TICKET_PROVIDER_SCHEMA = z.enum(["plane", "trello"]);
15857
16543
  var BOARD_URL_COMPAT_SCHEMA = z.string().optional().describe("Deprecated compatibility input. Ignored; board URLs are derived at runtime and are never persisted.").meta({ deprecated: true });
16544
+ var RUNTIME_REPO_COMPAT_SCHEMA = z.boolean().optional().describe("Deprecated no-op. Hermes always converges ignored role-local runtime state and never provisions a per-agent GitHub repository.").meta({ deprecated: true });
15858
16545
  function safePathSegmentSchema(label) {
15859
16546
  return z.string().superRefine((value, context) => {
15860
16547
  try {
@@ -15875,20 +16562,16 @@ var GENERIC_RECIPE_NAMES = getRecipeNames().filter((name) => !INTERACTIVE_RECIPE
15875
16562
  if (GENERIC_RECIPE_NAMES.length === 0) throw new Error("No non-interactive recipes are registered for generic MCP execution");
15876
16563
  function validateExternalEffectConsent(input, options) {
15877
16564
  const selected = {
15878
- runtimeRepo: input.provisionRuntimeRepo === true,
15879
16565
  ticketBoard: input.provisionTicketBoard === true,
15880
16566
  systemd: input.enableSystemd === true
15881
16567
  };
15882
- const anySelected = selected.runtimeRepo || selected.ticketBoard || selected.systemd;
16568
+ const anySelected = selected.ticketBoard || selected.systemd;
15883
16569
  if (anySelected && input.live !== true) {
15884
16570
  throw new Error("External Hermes effects require live=true in addition to explicit positive opt-ins");
15885
16571
  }
15886
16572
  if (anySelected && options.requireNonLocal && input.local !== false) {
15887
16573
  throw new Error("External Hermes effects require local=false in addition to live=true and explicit positive opt-ins");
15888
16574
  }
15889
- if (selected.runtimeRepo && input.skipRuntimeRepo === true) {
15890
- throw new Error("provisionRuntimeRepo=true contradicts skipRuntimeRepo=true");
15891
- }
15892
16575
  if (selected.ticketBoard && input.skipPlane === true) {
15893
16576
  throw new Error("provisionTicketBoard=true contradicts skipPlane=true");
15894
16577
  }
@@ -15902,7 +16585,7 @@ function validateExternalEffectConsent(input, options) {
15902
16585
  }
15903
16586
  function resolveTargetDir(targetDir) {
15904
16587
  const dir = resolve16(targetDir ?? process.cwd());
15905
- if (!existsSync22(dir)) {
16588
+ if (!existsSync23(dir)) {
15906
16589
  throw new Error(`Target directory does not exist: ${dir}`);
15907
16590
  }
15908
16591
  if (!statSync5(dir).isDirectory()) {
@@ -15913,7 +16596,7 @@ function resolveTargetDir(targetDir) {
15913
16596
  function resolvePjanglerRoot3() {
15914
16597
  let dir = dirname14(fileURLToPath8(import.meta.url));
15915
16598
  while (dir !== dirname14(dir)) {
15916
- if (existsSync22(join28(dir, "package.json")) && existsSync22(join28(dir, "templates", "commonproject", "copier.yml"))) {
16599
+ if (existsSync23(join30(dir, "package.json")) && existsSync23(join30(dir, "templates", "commonproject", "copier.yml"))) {
15917
16600
  return dir;
15918
16601
  }
15919
16602
  dir = dirname14(dir);
@@ -15934,7 +16617,6 @@ function publicProjectPlan(plan) {
15934
16617
  return {
15935
16618
  ...action,
15936
16619
  context: {
15937
- skipRuntimeRepo: action.context.skipRuntimeRepo,
15938
16620
  skipPlane: action.context.skipPlane,
15939
16621
  skipSystemd: action.context.skipSystemd
15940
16622
  }
@@ -15949,7 +16631,10 @@ function publicCompositeProjectResponse(payload, plan) {
15949
16631
  return {
15950
16632
  ...payload,
15951
16633
  ...hasNestedPlan ? { plan: projectedPlan } : {},
15952
- ...provisionsAgent ? { bloodbankMode: "fleet-shared" } : {}
16634
+ ...provisionsAgent ? {
16635
+ bloodbankMode: "fleet-shared",
16636
+ runtimeMode: "role-local-ignored"
16637
+ } : {}
15953
16638
  };
15954
16639
  }
15955
16640
  async function executeRegisteredProjectPlan(plan, agentContext, lifecycleOverrides = {}, trustedCopier) {
@@ -15967,7 +16652,6 @@ async function executeRegisteredProjectPlan(plan, agentContext, lifecycleOverrid
15967
16652
  ...agentContext,
15968
16653
  trustedCopier,
15969
16654
  deferredExternalEffects: {
15970
- runtimeRepo: !plannedAgent.context.skipRuntimeRepo,
15971
16655
  ticketBoard: !plannedAgent.context.skipPlane,
15972
16656
  systemd: !plannedAgent.context.skipSystemd,
15973
16657
  owner: "project"
@@ -16009,7 +16693,7 @@ function projectPreflightFailure(plan, errors, audit) {
16009
16693
  };
16010
16694
  }
16011
16695
  function preflightExistingHermesScaffold(targetDir) {
16012
- if (!existsSync22(join28(targetDir, "agents", "hermes"))) return void 0;
16696
+ if (!existsSync23(join30(targetDir, "agents", "hermes"))) return void 0;
16013
16697
  const owner = recipeRegistry.ownerOf("hermes.pm-scaffold");
16014
16698
  if (!owner) return "Hermes lifecycle owner is unavailable";
16015
16699
  const finding2 = owner.check.audit(lifecycleContext(targetDir, true));
@@ -16249,7 +16933,7 @@ server.registerTool(
16249
16933
  agentRole: AGENT_ROLE_SCHEMA.optional(),
16250
16934
  agentPurpose: z.string().optional(),
16251
16935
  local: z.boolean().optional(),
16252
- provisionRuntimeRepo: z.boolean().optional().describe("Explicitly opt in to runtime-repository provisioning; also requires live=true and local=false."),
16936
+ provisionRuntimeRepo: RUNTIME_REPO_COMPAT_SCHEMA,
16253
16937
  provisionTicketBoard: z.boolean().optional().describe("Explicitly opt in to ticket-board provisioning; also requires live=true, local=false, and skipPlane!=true."),
16254
16938
  enableSystemd: z.boolean().optional().describe("Explicitly opt in to systemd installation/enablement; also requires live=true and local=false."),
16255
16939
  force: z.boolean().optional(),
@@ -16270,10 +16954,10 @@ server.registerTool(
16270
16954
  const projectSlug = validateSafePathSegment(input.projectSlug ?? slugify(input.projectName), "Project slug");
16271
16955
  const explicitTargetDir = input.targetDir ? resolve16(input.targetDir) : void 0;
16272
16956
  const parentDir = resolve16(input.parentDir ?? (explicitTargetDir ? dirname14(explicitTargetDir) : process.cwd()));
16273
- if (!existsSync22(parentDir) || !statSync5(parentDir).isDirectory()) throw new Error(`Parent directory does not exist: ${parentDir}`);
16957
+ if (!existsSync23(parentDir) || !statSync5(parentDir).isDirectory()) throw new Error(`Parent directory does not exist: ${parentDir}`);
16274
16958
  const targetDir = resolveContainedPath(
16275
16959
  parentDir,
16276
- explicitTargetDir ?? join28(parentDir, projectSlug),
16960
+ explicitTargetDir ?? join30(parentDir, projectSlug),
16277
16961
  "Bootstrap target"
16278
16962
  );
16279
16963
  const overwrite = input.overwrite ?? input.force ?? false;
@@ -16285,7 +16969,7 @@ server.registerTool(
16285
16969
  if (externalEffects.ticketBoard && ticketProvider === "plane" && !boardId) {
16286
16970
  throw new Error("boardId or planeProjectId is required when skipPlane=false for Plane; keep skipPlane=true for safe local bootstrap");
16287
16971
  }
16288
- if (!dryRun && existsSync22(targetDir) && !overwrite) throw new Error(`Target already exists: ${targetDir} (set force/overwrite=true to re-render)`);
16972
+ if (!dryRun && existsSync23(targetDir) && !overwrite) throw new Error(`Target already exists: ${targetDir} (set force/overwrite=true to re-render)`);
16289
16973
  const plan = planProjectInit({
16290
16974
  name: input.projectName,
16291
16975
  description: input.projectDescription,
@@ -16297,12 +16981,13 @@ server.registerTool(
16297
16981
  agentRole: input.agentRole ?? "pm",
16298
16982
  apply: !dryRun,
16299
16983
  live: input.live ?? false,
16300
- provisionRuntimeRepo: externalEffects.runtimeRepo,
16301
16984
  provisionTicketBoard: externalEffects.ticketBoard,
16302
16985
  enableSystemd: externalEffects.systemd,
16303
16986
  skipPlane,
16304
16987
  registryPath: input.registryPath,
16305
- projectIdentifier: input.projectIdentifier ?? projectSlug.slice(0, 4).toUpperCase(),
16988
+ // A PROPOSAL only. The provider assigns the real identifier and
16989
+ // `pj project identity` reads it back; MCP never mints a board key.
16990
+ projectIdentifier: input.projectIdentifier ?? proposeProjectIdentifier(projectSlug),
16306
16991
  ticketProvider,
16307
16992
  boardId,
16308
16993
  boardUrl: input.boardUrl,
@@ -16334,7 +17019,6 @@ server.registerTool(
16334
17019
  force: overwrite,
16335
17020
  skipTelegram: true,
16336
17021
  skipEmail: true,
16337
- skipRuntimeRepo: plannedAgent?.kind === "hermes.provision-agent" ? plannedAgent.context.skipRuntimeRepo : true,
16338
17022
  skipPlane: plannedAgent?.kind === "hermes.provision-agent" ? plannedAgent.context.skipPlane : true,
16339
17023
  skipBloodbank: true,
16340
17024
  skipSystemd: plannedAgent?.kind === "hermes.provision-agent" ? plannedAgent.context.skipSystemd : true
@@ -16381,7 +17065,7 @@ server.registerTool(
16381
17065
  agentRole: AGENT_ROLE_SCHEMA.optional(),
16382
17066
  apply: z.boolean().optional(),
16383
17067
  live: z.boolean().optional(),
16384
- provisionRuntimeRepo: z.boolean().optional().describe("Explicitly opt in to Hermes runtime-repository provisioning; also requires live=true."),
17068
+ provisionRuntimeRepo: RUNTIME_REPO_COMPAT_SCHEMA,
16385
17069
  provisionTicketBoard: z.boolean().optional().describe("Explicitly opt in to ticket-board provisioning; also requires live=true and skipPlane!=true."),
16386
17070
  enableSystemd: z.boolean().optional().describe("Explicitly opt in to Hermes systemd installation/enablement; also requires live=true."),
16387
17071
  skipPlane: z.boolean().optional().describe("Disable project-board planning and provider invocation even when live=true."),
@@ -16408,7 +17092,6 @@ server.registerTool(
16408
17092
  agentRole: input.agentRole,
16409
17093
  apply: input.apply ?? false,
16410
17094
  live: input.live ?? false,
16411
- provisionRuntimeRepo: externalEffects.runtimeRepo,
16412
17095
  provisionTicketBoard: externalEffects.ticketBoard,
16413
17096
  enableSystemd: externalEffects.systemd,
16414
17097
  skipPlane: input.skipPlane ?? false,
@@ -16421,7 +17104,7 @@ server.registerTool(
16421
17104
  registryPath: input.registryPath,
16422
17105
  force: input.force ?? false,
16423
17106
  overwrite: input.force ?? false,
16424
- scaffold: !(input.targetDir && existsSync22(join28(resolve16(input.targetDir), ".git")))
17107
+ scaffold: !(input.targetDir && existsSync23(join30(resolve16(input.targetDir), ".git")))
16425
17108
  });
16426
17109
  if (!input.apply) return asText(publicCompositeProjectResponse(publicProjectPlan(plan), plan));
16427
17110
  const preflight = preflightProjectApply(plan, resolvePjanglerRoot3());
@@ -16589,11 +17272,11 @@ server.registerTool(
16589
17272
  local: z.boolean().optional(),
16590
17273
  apply: z.boolean().optional(),
16591
17274
  live: z.boolean().optional(),
16592
- provisionRuntimeRepo: z.boolean().optional().describe("Explicitly opt in to runtime-repository provisioning; requires live=true and local=false."),
17275
+ provisionRuntimeRepo: RUNTIME_REPO_COMPAT_SCHEMA,
16593
17276
  provisionTicketBoard: z.boolean().optional().describe("Explicitly opt in to ticket-board provisioning; requires live=true, local=false, and skipPlane!=true."),
16594
17277
  enableSystemd: z.boolean().optional().describe("Explicitly opt in to systemd installation/enablement; requires live=true, local=false, and skipSystemd!=true."),
16595
17278
  force: z.boolean().optional(),
16596
- skipRuntimeRepo: z.boolean().optional(),
17279
+ skipRuntimeRepo: RUNTIME_REPO_COMPAT_SCHEMA,
16597
17280
  skipPlane: z.boolean().optional(),
16598
17281
  skipSystemd: z.boolean().optional(),
16599
17282
  ticketProvider: TICKET_PROVIDER_SCHEMA.optional()
@@ -16665,7 +17348,7 @@ server.registerTool(
16665
17348
  quiet: true,
16666
17349
  local,
16667
17350
  live,
16668
- targetRepo: input.targetRepo ?? basename8(resolvedTarget),
17351
+ targetRepo: input.targetRepo ?? basename9(resolvedTarget),
16669
17352
  role: normalizeAgentRole(input.role),
16670
17353
  agentPurpose: input.agentPurpose,
16671
17354
  soulTone: input.soulTone,
@@ -16681,13 +17364,11 @@ server.registerTool(
16681
17364
  // unreachable and therefore cannot consume JSON-RPC stdin.
16682
17365
  skipTelegram: true,
16683
17366
  skipEmail: true,
16684
- skipRuntimeRepo: !externalEffects.runtimeRepo,
16685
17367
  skipPlane: !externalEffects.ticketBoard,
16686
17368
  skipBloodbank: true,
16687
17369
  skipSystemd: !externalEffects.systemd || process.platform === "darwin",
16688
17370
  trustedCopier,
16689
17371
  deferredExternalEffects: {
16690
- runtimeRepo: externalEffects.runtimeRepo,
16691
17372
  ticketBoard: externalEffects.ticketBoard,
16692
17373
  systemd: externalEffects.systemd,
16693
17374
  owner: "hermes"
@@ -16703,6 +17384,7 @@ server.registerTool(
16703
17384
  apply,
16704
17385
  live,
16705
17386
  bloodbankMode: "fleet-shared",
17387
+ runtimeMode: "role-local-ignored",
16706
17388
  guidance: parityGuidance(),
16707
17389
  context: {
16708
17390
  targetRepo: context.targetRepo,
@@ -16711,7 +17393,6 @@ server.registerTool(
16711
17393
  dryRun: context.dryRun,
16712
17394
  quiet: context.quiet,
16713
17395
  force: context.force,
16714
- skipRuntimeRepo: context.skipRuntimeRepo,
16715
17396
  skipPlane: context.skipPlane,
16716
17397
  skipSystemd: context.skipSystemd
16717
17398
  },
@@ -16729,3 +17410,4 @@ server.registerTool(
16729
17410
  );
16730
17411
  var transport = new StdioServerTransport();
16731
17412
  await server.connect(transport);
17413
+ //# sourceMappingURL=mcp-server.js.map