@delorenj/pjangler 1.5.36 → 1.5.38

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -343,8 +343,8 @@ var init_types = __esm({
343
343
  CREDENTIAL_KEY = /(?:^|[_-])(?:password|passwd|api[_-]?key|access[_-]?token|refresh[_-]?token|client[_-]?secret|private[_-]?key|credential|authorization)(?:$|[_-])/iu;
344
344
  SECRET_SHAPED_VALUE = /(?:-----BEGIN (?:RSA |EC |OPENSSH )?PRIVATE KEY-----|(?<![A-Za-z0-9])eyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}(?![A-Za-z0-9])|AKIA[0-9A-Z]{16}|gh[pousr]_[A-Za-z0-9]{20,}|(?:password|api[_-]?key|access[_-]?token|client[_-]?secret)\s*[:=]\s*[^\s]{8,})/iu;
345
345
  NotebookError = class extends Error {
346
- constructor(code, message, retryable = false, details = {}, options) {
347
- super(message, options);
346
+ constructor(code, message2, retryable = false, details = {}, options) {
347
+ super(message2, options);
348
348
  this.code = code;
349
349
  this.retryable = retryable;
350
350
  this.details = details;
@@ -501,8 +501,8 @@ var init_manifestIndex = __esm({
501
501
  init_registryClient();
502
502
  init_notebookSettingsValidation();
503
503
  RegistryError = class extends Error {
504
- constructor(message, status = 422, code = "invalid_manifest") {
505
- super(message);
504
+ constructor(message2, status = 422, code = "invalid_manifest") {
505
+ super(message2);
506
506
  this.status = status;
507
507
  this.code = code;
508
508
  }
@@ -1023,8 +1023,8 @@ var init_boardQuery = __esm({
1023
1023
  init_version();
1024
1024
  BOARD_PROVIDERS = ["plane", "trello", "linear"];
1025
1025
  BoardError = class extends Error {
1026
- constructor(message) {
1027
- super(message);
1026
+ constructor(message2) {
1027
+ super(message2);
1028
1028
  this.name = "BoardError";
1029
1029
  }
1030
1030
  };
@@ -4561,14 +4561,14 @@ function parseNote(value, noteMaxBytes) {
4561
4561
  if (note.content === null) throw new NotebookError("REMOTE_PROTOCOL_ERROR", "Open Notebook returned invalid note content");
4562
4562
  return note;
4563
4563
  }
4564
- function errorForStatus(status, message) {
4565
- if (status === 400 || status === 422) return new NotebookError("INVALID_INPUT", message, false, { http_status: status, definitive_rejection: true });
4566
- if (status === 401 || status === 403) return new NotebookError("AUTHENTICATION_FAILED", message);
4567
- if (status === 404) return new NotebookError("NOT_FOUND", message);
4568
- if (status === 409) return new NotebookError("CONFLICT", message);
4569
- if (status === 429) return new NotebookError("THROTTLED", message, true);
4570
- if (status >= 500) return new NotebookError("SERVICE_UNAVAILABLE", message, true);
4571
- return new NotebookError("REMOTE_PROTOCOL_ERROR", message);
4564
+ function errorForStatus(status, message2) {
4565
+ if (status === 400 || status === 422) return new NotebookError("INVALID_INPUT", message2, false, { http_status: status, definitive_rejection: true });
4566
+ if (status === 401 || status === 403) return new NotebookError("AUTHENTICATION_FAILED", message2);
4567
+ if (status === 404) return new NotebookError("NOT_FOUND", message2);
4568
+ if (status === 409) return new NotebookError("CONFLICT", message2);
4569
+ if (status === 429) return new NotebookError("THROTTLED", message2, true);
4570
+ if (status >= 500) return new NotebookError("SERVICE_UNAVAILABLE", message2, true);
4571
+ return new NotebookError("REMOTE_PROTOCOL_ERROR", message2);
4572
4572
  }
4573
4573
  async function readResponseBody(response, maxBytes) {
4574
4574
  const declared = Number(response.headers.get("content-length") ?? "0");
@@ -6063,9 +6063,9 @@ var init_capture = __esm({
6063
6063
  });
6064
6064
 
6065
6065
  // src/index.ts
6066
- import { spawnSync as spawnSync17 } from "node:child_process";
6067
- import { existsSync as existsSync26, readdirSync as readdirSync12, readFileSync as readFileSync29, statSync as statSync8 } from "node:fs";
6068
- import { basename as basename11, join as join34, resolve as resolve25 } from "node:path";
6066
+ import { spawnSync as spawnSync18 } from "node:child_process";
6067
+ import { existsSync as existsSync27, readdirSync as readdirSync13, readFileSync as readFileSync30, statSync as statSync9 } from "node:fs";
6068
+ import { basename as basename11, join as join35, resolve as resolve25 } from "node:path";
6069
6069
  import { Command as Command3, CommanderError, Option } from "commander";
6070
6070
 
6071
6071
  // src/recipes/Recipe.ts
@@ -6083,8 +6083,8 @@ var Command = class {
6083
6083
  /**
6084
6084
  * Format message with [DRY RUN] prefix if in dry-run mode
6085
6085
  */
6086
- formatMessage(message) {
6087
- return this.context.dryRun ? `[DRY RUN] ${message}` : message;
6086
+ formatMessage(message2) {
6087
+ return this.context.dryRun ? `[DRY RUN] ${message2}` : message2;
6088
6088
  }
6089
6089
  fileExists(filePath) {
6090
6090
  const fullPath = join(this.context.targetDir, filePath);
@@ -9884,11 +9884,11 @@ async function runDeclaredBmadUpdate(root, options, scope) {
9884
9884
  try {
9885
9885
  sb = captureScopedBackup(root, scope);
9886
9886
  } catch (error) {
9887
- const message = error instanceof UnreadableSurfaceError ? error.message : `could not capture the BMAD update surface: ${error instanceof Error ? error.message : String(error)}`;
9887
+ const message2 = error instanceof UnreadableSurfaceError ? error.message : `could not capture the BMAD update surface: ${error instanceof Error ? error.message : String(error)}`;
9888
9888
  return {
9889
9889
  ok: false,
9890
- error: message,
9891
- details: [message, "No eviction or installer ran; nothing was written.", scopeNote],
9890
+ error: message2,
9891
+ details: [message2, "No eviction or installer ran; nothing was written.", scopeNote],
9892
9892
  changedFiles: [],
9893
9893
  companion: preflight
9894
9894
  };
@@ -10133,8 +10133,8 @@ function scanToml(text2) {
10133
10133
  let i = 0;
10134
10134
  let line = 0;
10135
10135
  let table = -1;
10136
- const fail2 = (message) => {
10137
- throw new TomlScanError(`line ${line + 1}: ${message}`);
10136
+ const fail2 = (message2) => {
10137
+ throw new TomlScanError(`line ${line + 1}: ${message2}`);
10138
10138
  };
10139
10139
  const skipBlank = () => {
10140
10140
  while (i < n && (text2[i] === " " || text2[i] === " " || text2[i] === "\r")) i++;
@@ -14598,6 +14598,18 @@ init_project();
14598
14598
  import { spawnSync as spawnSync14 } from "node:child_process";
14599
14599
  import { existsSync as existsSync18, lstatSync as lstatSync16, readFileSync as readFileSync22, rmSync as rmSync8 } from "node:fs";
14600
14600
  import { dirname as dirname18, isAbsolute as isAbsolute10, join as join24, relative as relativePath, resolve as resolve19 } from "node:path";
14601
+
14602
+ // src/project/ticketScope.ts
14603
+ function ticketScope(ticket) {
14604
+ const block = ticket !== null && typeof ticket === "object" ? ticket : {};
14605
+ const label = typeof block.scope_label === "string" ? block.scope_label.trim() : "";
14606
+ return {
14607
+ ...label ? { scope_label: label } : {},
14608
+ ...block.scope_default === true ? { scope_default: true } : {}
14609
+ };
14610
+ }
14611
+
14612
+ // src/recipes/ProjectRecipe.ts
14601
14613
  init_tree_diff();
14602
14614
 
14603
14615
  // src/recipes/NotebookRecipe.ts
@@ -15666,8 +15678,8 @@ function installProjectNotebookIntegration(input = {}) {
15666
15678
  return { skill, hooksChanged: /\bchanged\b/u.test(result2.stdout) };
15667
15679
  }
15668
15680
  function boundedDiagnostic(value, max = 512) {
15669
- const message = value instanceof NotebookError ? value.message : "Project Notebook encountered an unexpected internal error";
15670
- return message.replace(/[\u0000-\u001f\u007f]/gu, " ").replace(/\s+/gu, " ").slice(0, max);
15681
+ const message2 = value instanceof NotebookError ? value.message : "Project Notebook encountered an unexpected internal error";
15682
+ return message2.replace(/[\u0000-\u001f\u007f]/gu, " ").replace(/\s+/gu, " ").slice(0, max);
15671
15683
  }
15672
15684
  function parsePayload(value, maxBytes) {
15673
15685
  if (Buffer.byteLength(value, "utf8") > maxBytes) throw new NotebookError("INVALID_INPUT", "Hook payload exceeds its configured ceiling");
@@ -16563,8 +16575,8 @@ var NotebookRecipe = class extends Recipe {
16563
16575
  return { recipeId: this.metadata.id, ok: true, dryRun: Boolean(ctx.dryRun), changedFiles: [], logs: ["notebook: local skill/hooks skipped"], errors: [], phases: [{ id: "notebook.local", status: "skipped", changedFiles: [], message: "Notebook is disabled or this is a plan-only invocation" }] };
16564
16576
  }
16565
16577
  if (notebookPlan.remote_effect === "none") {
16566
- const message = `Notebook has no configured endpoint (${notebookPlan.reason}); global skill and hook projection deferred to \`pj notebook migrate --apply\``;
16567
- return { recipeId: this.metadata.id, ok: true, dryRun: false, changedFiles: [], logs: [`notebook: ${message}`], errors: [], phases: [{ id: "notebook.local", status: "skipped", changedFiles: [], message }] };
16578
+ const message2 = `Notebook has no configured endpoint (${notebookPlan.reason}); global skill and hook projection deferred to \`pj notebook migrate --apply\``;
16579
+ return { recipeId: this.metadata.id, ok: true, dryRun: false, changedFiles: [], logs: [`notebook: ${message2}`], errors: [], phases: [{ id: "notebook.local", status: "skipped", changedFiles: [], message: message2 }] };
16568
16580
  }
16569
16581
  try {
16570
16582
  const env2 = integrationEnvironment(this.module, ctx);
@@ -16593,8 +16605,8 @@ var NotebookRecipe = class extends Recipe {
16593
16605
  phases: [{ id: "notebook.local", status: applied.changedFiles.length ? "changed" : "unchanged", changedFiles: applied.changedFiles, message: "Canonical Project Notebook skill and hooks verified" }]
16594
16606
  };
16595
16607
  } catch (error) {
16596
- const message = error instanceof Error ? error.message : String(error);
16597
- return { recipeId: this.metadata.id, ok: false, dryRun: false, changedFiles: [], logs: [], errors: [message], phases: [{ id: "notebook.local", status: "failed", changedFiles: [], message }] };
16608
+ const message2 = error instanceof Error ? error.message : String(error);
16609
+ return { recipeId: this.metadata.id, ok: false, dryRun: false, changedFiles: [], logs: [], errors: [message2], phases: [{ id: "notebook.local", status: "failed", changedFiles: [], message: message2 }] };
16598
16610
  }
16599
16611
  }
16600
16612
  async applyExternal(plan, notebookPlan) {
@@ -16720,6 +16732,7 @@ function refreshPlanFromCanonicalManifest(plan) {
16720
16732
  ...carriesProvenance && identifierSource === "provider" && recorded?.identifier_fetched_at ? { identifier_fetched_at: recorded.identifier_fetched_at } : {},
16721
16733
  board_id: manifestBoardId,
16722
16734
  ...boardConfirmedAt ? { board_confirmed_at: boardConfirmedAt } : {},
16735
+ ...ticketScope(manifestTicket),
16723
16736
  state: manifestState === "skipped" ? "skipped" : manifestBoardId && boardConfirmedAt && keyIsProven ? "linked" : "planned"
16724
16737
  };
16725
16738
  plan.manifest = manifest;
@@ -17077,8 +17090,8 @@ var ProjectRecipe = class extends Recipe {
17077
17090
  } else try {
17078
17091
  rmSync8(targetDir, { recursive: true, force: true });
17079
17092
  const insideTarget = (path) => {
17080
- const relative16 = relativePath(resolve19(targetDir), resolve19(path));
17081
- return relative16 === "" || !relative16.startsWith("..") && !isAbsolute10(relative16);
17093
+ const relative17 = relativePath(resolve19(targetDir), resolve19(path));
17094
+ return relative17 === "" || !relative17.startsWith("..") && !isAbsolute10(relative17);
17082
17095
  };
17083
17096
  const orphaned = [...new Set(changedFiles.filter((path) => !insideTarget(path)))].sort();
17084
17097
  changedFiles.length = 0;
@@ -18231,6 +18244,43 @@ async function fetchPlaneBoards(workspace, env2 = process.env, home = homedir12(
18231
18244
  }
18232
18245
  return boards;
18233
18246
  }
18247
+ async function patchPlaneBoardIdentifier(workspace, boardId, identifier, env2 = process.env, home = homedir12()) {
18248
+ const apiKey = planeApiKeyFor(workspace, env2, home);
18249
+ if (!apiKey) {
18250
+ throw new BoardError(
18251
+ `no Plane API key for workspace "${workspace}": set PLANE_API_KEY, or add one to ${env2.HERMES_FLEET_ENV?.trim() || join30(home, ".hermes", "fleet.env")}`
18252
+ );
18253
+ }
18254
+ const base = planeBase(env2, home);
18255
+ const url = `${base}/api/v1/workspaces/${encodeURIComponent(workspace)}/projects/${encodeURIComponent(boardId)}/`;
18256
+ let response;
18257
+ try {
18258
+ response = await fetch(url, {
18259
+ method: "PATCH",
18260
+ headers: {
18261
+ "X-API-Key": apiKey,
18262
+ "User-Agent": `pjangler/${PJANGLER_VERSION}`,
18263
+ "Content-Type": "application/json"
18264
+ },
18265
+ body: JSON.stringify({ identifier }),
18266
+ signal: AbortSignal.timeout(2e4)
18267
+ });
18268
+ } catch (error) {
18269
+ throw new BoardError(
18270
+ `could not reach Plane at ${base} to rename board ${boardId}: ` + (error instanceof Error ? error.message : String(error))
18271
+ );
18272
+ }
18273
+ if (!response.ok) {
18274
+ let detail = "";
18275
+ try {
18276
+ detail = (await response.text()).replace(/\s+/g, " ").trim().slice(0, 200);
18277
+ } catch {
18278
+ }
18279
+ throw new BoardError(
18280
+ `Plane refused identifier "${identifier}" for board ${boardId} (HTTP ${response.status})${detail ? `: ${detail}` : ""}`
18281
+ );
18282
+ }
18283
+ }
18234
18284
  function trelloCredential(name, env2, home) {
18235
18285
  const fleetEnv = env2.HERMES_FLEET_ENV?.trim() || join30(home, ".hermes", "fleet.env");
18236
18286
  const raw = env2[name]?.trim() || dotenvValue(fleetEnv, name)?.trim();
@@ -18931,19 +18981,499 @@ async function linkProjectBoard(options) {
18931
18981
  };
18932
18982
  }
18933
18983
 
18984
+ // src/project/rename.ts
18985
+ init_boardUrl();
18986
+ import { spawnSync as spawnSync15 } from "node:child_process";
18987
+ import { existsSync as existsSync22, readdirSync as readdirSync11, readFileSync as readFileSync27, realpathSync as realpathSync11, statSync as statSync6 } from "node:fs";
18988
+ import { homedir as homedir13 } from "node:os";
18989
+ import { isAbsolute as isAbsolute11, join as join31, relative as relative16 } from "node:path";
18990
+ import YAML8 from "yaml";
18991
+ init_project();
18992
+ init_registryClient();
18993
+ var KEY_PATTERN = /^[a-z0-9]{1,12}$/;
18994
+ var RenameError = class extends Error {
18995
+ constructor(code, message2) {
18996
+ super(message2);
18997
+ this.code = code;
18998
+ this.name = "RenameError";
18999
+ }
19000
+ };
19001
+ function isRecord7(value) {
19002
+ return typeof value === "object" && value !== null && !Array.isArray(value);
19003
+ }
19004
+ function message(error) {
19005
+ return error instanceof Error ? error.message : String(error);
19006
+ }
19007
+ function readJsonObject(path) {
19008
+ let parsed;
19009
+ try {
19010
+ parsed = JSON.parse(readFileSync27(path, "utf8"));
19011
+ } catch {
19012
+ throw new RenameError("manifest_invalid", `${path} is not readable JSON; fix it before renaming`);
19013
+ }
19014
+ if (!isRecord7(parsed)) throw new RenameError("manifest_invalid", `${path} must hold a JSON object`);
19015
+ return parsed;
19016
+ }
19017
+ function escapeRegExp2(value) {
19018
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
19019
+ }
19020
+ var YAML_WORDS = /* @__PURE__ */ new Set(["Y", "N", "YES", "NO", "ON", "OFF", "NULL", "TRUE", "FALSE"]);
19021
+ function yamlScalarFor(identifier, originalSlice) {
19022
+ const plainSafe = /^[A-Z][A-Z0-9]*$/.test(identifier) && !YAML_WORDS.has(identifier);
19023
+ if (originalSlice.startsWith("'") && !originalSlice.startsWith("''")) return `'${identifier}'`;
19024
+ if (originalSlice.startsWith('"')) return `"${identifier}"`;
19025
+ return plainSafe ? identifier : `"${identifier}"`;
19026
+ }
19027
+ async function renameProject(options) {
19028
+ const newId = String(options.newId ?? "").trim().toLowerCase();
19029
+ if (!KEY_PATTERN.test(newId)) {
19030
+ throw new RenameError(
19031
+ "invalid_key",
19032
+ `invalid key ${JSON.stringify(String(options.newId ?? ""))}: use 1-12 lowercase letters and digits (Plane identifiers are alphanumeric, max 12)`
19033
+ );
19034
+ }
19035
+ const NEW = newId.toUpperCase();
19036
+ const oldInput = String(options.oldId ?? "").trim().toLowerCase();
19037
+ const env2 = options.env ?? process.env;
19038
+ const home = options.home ?? homedir13();
19039
+ const apply = options.apply ?? false;
19040
+ const now = (options.now ?? /* @__PURE__ */ new Date()).toISOString();
19041
+ const registryPath = resolveRegistryLocation(options.registryPath, env2);
19042
+ const service = isRegistryServiceLocation(registryPath);
19043
+ const warnings = [];
19044
+ let registry;
19045
+ try {
19046
+ registry = loadProjectRegistry(registryPath);
19047
+ } catch (error) {
19048
+ throw new RenameError("registry_unavailable", message(error));
19049
+ }
19050
+ let oldId = oldInput;
19051
+ let resumed = false;
19052
+ let renamedFromIdentifier = "";
19053
+ let project = oldInput ? getOwnRecordValue(registry.projects, oldInput) : void 0;
19054
+ if (project) {
19055
+ if (oldInput !== newId && getOwnRecordValue(registry.projects, newId)) {
19056
+ throw new RenameError("identity_collision", `project_id "${newId}" is already registered to another project`);
19057
+ }
19058
+ } else {
19059
+ const existing = getOwnRecordValue(registry.projects, newId);
19060
+ let from;
19061
+ if (existing && oldInput) {
19062
+ try {
19063
+ const candidate = readJsonObject(join31(existing.repo_path, ".project.json")).renamed_from;
19064
+ if (isRecord7(candidate)) from = candidate;
19065
+ } catch {
19066
+ }
19067
+ }
19068
+ if (!existing || !from || from.project_id !== oldInput) {
19069
+ throw new RenameError("unknown_project", `Project not found in registry: ${options.oldId}`);
19070
+ }
19071
+ project = existing;
19072
+ resumed = true;
19073
+ renamedFromIdentifier = typeof from.identifier === "string" ? from.identifier : "";
19074
+ }
19075
+ const record3 = project;
19076
+ const provider = record3.ticket_provider;
19077
+ const type = String(provider.type ?? "").trim().toLowerCase();
19078
+ const boardId = String(provider.board_id ?? "").trim();
19079
+ const isPlane = type === "plane";
19080
+ const workspace = isPlane ? String(provider.workspace ?? "").trim() || DEFAULT_PLANE_WORKSPACE : String(provider.workspace ?? "").trim();
19081
+ const repoPath = record3.repo_path;
19082
+ const manifestPath = join31(repoPath, ".project.json");
19083
+ const manifestExists = existsSync22(manifestPath);
19084
+ if (service && !manifestExists) {
19085
+ throw new RenameError("manifest_missing", `${manifestPath} is missing; the registry service indexes manifests, so there is nothing to rename`);
19086
+ }
19087
+ const manifestBefore = manifestExists ? readJsonObject(manifestPath) : void 0;
19088
+ const fetchBoards = options.fetchBoards ?? ((ws) => fetchPlaneBoards(ws, env2, home));
19089
+ let live;
19090
+ if (isPlane && boardId) {
19091
+ let boards;
19092
+ try {
19093
+ boards = await fetchBoards(workspace);
19094
+ } catch (error) {
19095
+ throw new RenameError("provider_unreachable", message(error));
19096
+ }
19097
+ live = boards.get(boardId);
19098
+ if (!live) throw new RenameError("board_not_found", `Plane workspace "${workspace}" has no board ${boardId}`);
19099
+ if (live.archived) {
19100
+ throw new RenameError("board_archived", `Plane board ${boardId} ("${live.name}") is archived; an archived board answers but is not usable`);
19101
+ }
19102
+ const owner = liveKeyIndex(boards).get(NEW);
19103
+ if (owner && owner.id !== boardId) {
19104
+ throw new RenameError("identity_collision", `board identifier ${NEW} already belongs to Plane board ${owner.id} ("${owner.name}") in workspace "${workspace}"`);
19105
+ }
19106
+ } else if (type === "trello" && boardId) {
19107
+ try {
19108
+ const facts = await (options.fetchTrello ?? ((id) => fetchTrelloBoard(id, env2, home)))(boardId);
19109
+ if (!facts) throw new RenameError("board_not_found", `Trello has no visible board ${boardId}`);
19110
+ if (facts.closed) throw new RenameError("board_archived", `Trello board ${boardId} ("${facts.name}") is archived`);
19111
+ } catch (error) {
19112
+ if (error instanceof RenameError) throw error;
19113
+ warnings.push(`Trello board ${boardId} could not be verified (${message(error)}); renamed locally`);
19114
+ }
19115
+ }
19116
+ const manifestProvider = manifestBefore?.ticket_provider;
19117
+ const manifestIdentifier = isRecord7(manifestProvider) ? String(manifestProvider.identifier ?? "") : "";
19118
+ const oldIdentifiers = [
19119
+ ...new Set(
19120
+ [String(provider.identifier ?? ""), manifestIdentifier, live?.identifier ?? "", renamedFromIdentifier].map((value) => value.trim()).filter((value) => value && value.toUpperCase() !== NEW)
19121
+ )
19122
+ ];
19123
+ const oldIdentifier = oldIdentifiers[0] ?? "";
19124
+ const providerConfirmed = isPlane && Boolean(boardId);
19125
+ function nextProvider(current) {
19126
+ if (!current.identifier && !providerConfirmed) return current;
19127
+ const next = { ...current, identifier: NEW };
19128
+ if (providerConfirmed) {
19129
+ const unchanged = String(current.identifier ?? "").toUpperCase() === NEW && current.identifier_source === "provider" && typeof current.identifier_fetched_at === "string" && current.identifier_fetched_at !== "";
19130
+ next.identifier_source = "provider";
19131
+ next.identifier_fetched_at = unchanged ? current.identifier_fetched_at : now;
19132
+ }
19133
+ return next;
19134
+ }
19135
+ const planeCall = options.patchBoard ?? ((ws, id, identifier) => patchPlaneBoardIdentifier(ws, id, identifier, env2, home));
19136
+ const planeStep = async (doApply) => {
19137
+ if (!isPlane) return { status: "skipped", detail: type === "trello" ? "trello assigns no key; no provider write" : "no Plane board; no provider write" };
19138
+ if (!boardId || !live) return { status: "skipped", detail: "no board bound; nothing to rename at the provider" };
19139
+ if (live.identifier.toUpperCase() === NEW) return { status: "already", detail: `board ${boardId} already reads ${live.identifier}` };
19140
+ if (!doApply) return { status: "planned", detail: `Plane board ${boardId}: ${live.identifier} -> ${NEW}` };
19141
+ let patchError;
19142
+ try {
19143
+ await planeCall(workspace, boardId, NEW);
19144
+ } catch (error) {
19145
+ patchError = message(error);
19146
+ }
19147
+ try {
19148
+ const after = (await fetchBoards(workspace)).get(boardId);
19149
+ if (after && after.identifier.toUpperCase() === NEW) {
19150
+ return { status: "done", detail: `Plane board ${boardId}: ${live.identifier} -> ${NEW}${patchError ? ` (the PATCH reported: ${patchError})` : ""}` };
19151
+ }
19152
+ if (patchError) {
19153
+ return { status: "failed", detail: `${patchError}; the board still reads ${after?.identifier ?? "(missing)"}, but the request may yet land: re-running is safe` };
19154
+ }
19155
+ return { status: "failed", detail: `Plane accepted the rename but board ${boardId} reads ${after?.identifier ?? "(missing)"}` };
19156
+ } catch (error) {
19157
+ return {
19158
+ status: "failed",
19159
+ detail: `${patchError ? `${patchError}; ` : ""}the read-back failed (${message(error)}), so the outcome is unknown; re-running is safe`
19160
+ };
19161
+ }
19162
+ };
19163
+ const manifestStep = async (doApply) => {
19164
+ if (!manifestExists) return { status: "skipped", detail: `no .project.json at ${repoPath}` };
19165
+ let current;
19166
+ try {
19167
+ current = readJsonObject(manifestPath);
19168
+ } catch (error) {
19169
+ return { status: "failed", detail: message(error) };
19170
+ }
19171
+ const next = { ...current, project_id: newId };
19172
+ delete next.project_slug;
19173
+ if (isRecord7(current.ticket_provider)) next.ticket_provider = nextProvider(current.ticket_provider);
19174
+ if (!resumed && (oldId !== newId || oldIdentifier !== "")) {
19175
+ const previous = current.renamed_from;
19176
+ const same = isRecord7(previous) && previous.project_id === oldId && String(previous.identifier ?? "") === oldIdentifier;
19177
+ next.renamed_from = same ? previous : { project_id: oldId, ...oldIdentifier ? { identifier: oldIdentifier } : {}, at: now };
19178
+ }
19179
+ if (JSON.stringify(next) === JSON.stringify(current)) {
19180
+ return { status: "already", detail: `project_id ${newId}, identifier ${NEW}` };
19181
+ }
19182
+ const detail = `project_id ${String(current.project_id ?? current.project_slug ?? "(none)")} -> ${newId}, identifier -> ${NEW}`;
19183
+ if (!doApply) return { status: "planned", detail, files: [manifestPath] };
19184
+ try {
19185
+ atomicWrite(manifestPath, `${JSON.stringify(next, null, 2)}
19186
+ `);
19187
+ } catch (error) {
19188
+ return { status: "failed", detail: `could not write ${manifestPath}: ${message(error)}` };
19189
+ }
19190
+ return { status: "done", detail, files: [manifestPath] };
19191
+ };
19192
+ const registryStep = async (doApply) => {
19193
+ const identifierNow = (r) => String(r?.ticket_provider?.identifier ?? "").toUpperCase();
19194
+ const keyed = getOwnRecordValue(registry.projects, newId);
19195
+ const keyedIdentifier = identifierNow(keyed);
19196
+ const settled = Boolean(keyed) && (oldId === newId || !getOwnRecordValue(registry.projects, oldId)) && (keyedIdentifier === NEW || keyedIdentifier === "" && !providerConfirmed);
19197
+ if (settled) return { status: "already", detail: `registry already keys ${newId}` };
19198
+ const detail = `${oldId} -> ${newId}${service ? " (service re-index)" : ""}`;
19199
+ if (!doApply) return { status: "planned", detail };
19200
+ try {
19201
+ if (service) {
19202
+ registryRequest(registryPath, "POST", "/v1/index", { manifest_path: manifestPath });
19203
+ const after = loadProjectRegistry(registryPath);
19204
+ if (!getOwnRecordValue(after.projects, newId)) throw new Error(`the service index does not list ${newId} after re-indexing`);
19205
+ if (oldId !== newId && getOwnRecordValue(after.projects, oldId)) throw new Error(`the service index still lists ${oldId} after re-indexing`);
19206
+ } else {
19207
+ const fresh = loadProjectRegistry(registryPath);
19208
+ const current = getOwnRecordValue(fresh.projects, oldId);
19209
+ if (!current) throw new Error(`${oldId} vanished from ${registryPath} mid-rename`);
19210
+ const renamed = {
19211
+ ...current,
19212
+ slug: newId,
19213
+ ...Object.hasOwn(current, "project_id") ? { project_id: newId } : {},
19214
+ ticket_provider: nextProvider(current.ticket_provider),
19215
+ updated_at: now
19216
+ };
19217
+ delete fresh.projects[oldId];
19218
+ fresh.projects[newId] = renamed;
19219
+ saveProjectRegistry(fresh, registryPath);
19220
+ }
19221
+ } catch (error) {
19222
+ return { status: "failed", detail: message(error) };
19223
+ }
19224
+ return { status: "done", detail, files: service ? [] : [registryPath] };
19225
+ };
19226
+ const roleStep = async (doApply) => {
19227
+ const hermesDir = join31(repoPath, "agents", "hermes");
19228
+ if (!existsSync22(hermesDir)) return { status: "skipped", detail: "no agents/hermes directory" };
19229
+ if (!isPlane) return { status: "skipped", detail: "role.yaml plane.identifier is a Plane field" };
19230
+ const files = [];
19231
+ let touched2 = 0;
19232
+ let settled = 0;
19233
+ let entries;
19234
+ try {
19235
+ entries = readdirSync11(hermesDir, { withFileTypes: true });
19236
+ } catch (error) {
19237
+ return { status: "failed", detail: `${hermesDir}: ${message(error)}` };
19238
+ }
19239
+ for (const entry of entries) {
19240
+ if (!entry.isDirectory()) continue;
19241
+ const path = join31(hermesDir, entry.name, "role.yaml");
19242
+ if (!existsSync22(path)) continue;
19243
+ try {
19244
+ const original = readFileSync27(path, "utf8");
19245
+ const document = YAML8.parseDocument(original);
19246
+ if (document.errors.length) throw new Error("YAML is invalid");
19247
+ const node = document.getIn(["plane", "identifier"], true);
19248
+ if (!YAML8.isScalar(node) || !node.range) continue;
19249
+ if (String(node.value) === NEW) {
19250
+ settled += 1;
19251
+ continue;
19252
+ }
19253
+ touched2 += 1;
19254
+ files.push(path);
19255
+ if (!doApply) continue;
19256
+ const [start, end] = node.range;
19257
+ const text2 = `${original.slice(0, start)}${yamlScalarFor(NEW, original.slice(start, end))}${original.slice(end)}`;
19258
+ const before = document.toJS();
19259
+ const after = YAML8.parse(text2);
19260
+ if (isRecord7(before.plane)) before.plane.identifier = NEW;
19261
+ if (JSON.stringify(after) !== JSON.stringify(before)) throw new Error("the edit changed more than plane.identifier");
19262
+ atomicWrite(path, text2);
19263
+ } catch (error) {
19264
+ return { status: "failed", detail: `${path}: ${message(error)}` };
19265
+ }
19266
+ }
19267
+ if (!touched2 && !settled) return { status: "skipped", detail: "no role.yaml carries plane.identifier" };
19268
+ if (!touched2) return { status: "already", detail: `${settled} role.yaml already read ${NEW}` };
19269
+ return { status: doApply ? "done" : "planned", detail: `${touched2} role.yaml: plane.identifier -> ${NEW}`, files };
19270
+ };
19271
+ const fleetStep = async (doApply) => {
19272
+ if (!isPlane || !boardId) return { status: "skipped", detail: "no Plane board bound" };
19273
+ const hermesPath = hermesAgentsRegistryPath(env2, home);
19274
+ if (!existsSync22(hermesPath)) return { status: "skipped", detail: `no fleet registry at ${hermesPath}` };
19275
+ try {
19276
+ const bound = readHermesAgentBoards(hermesPath).filter((agent) => agent.boardId === boardId);
19277
+ if (!bound.length) return { status: "skipped", detail: "no fleet agent is bound to this board" };
19278
+ const stale = bound.filter((agent) => agent.identifier !== NEW);
19279
+ if (!stale.length) return { status: "already", detail: `${bound.length} fleet agent(s) already read ${NEW}` };
19280
+ if (!doApply) return { status: "planned", detail: `${stale.length} fleet agent(s): plane.identifier -> ${NEW} (${hermesPath})`, files: [hermesPath] };
19281
+ const report = await reconcileProjectIdentity({
19282
+ target: newId,
19283
+ apply: true,
19284
+ hermesRegistryPath: hermesPath,
19285
+ registryPath,
19286
+ env: env2,
19287
+ home,
19288
+ now: new Date(now),
19289
+ ...options.fetchBoards ? { fetchBoards: options.fetchBoards } : {}
19290
+ });
19291
+ if (!report.ok) return { status: "failed", detail: report.errors.join("; ") || "identity reconcile reported errors" };
19292
+ const lagging = readHermesAgentBoards(hermesPath).filter((agent) => agent.boardId === boardId && agent.identifier !== NEW);
19293
+ if (lagging.length) {
19294
+ return { status: "failed", detail: `fleet agent(s) still read another identifier: ${lagging.map((agent) => `${agent.agentId} (${agent.identifier || "empty"})`).join(", ")}` };
19295
+ }
19296
+ return { status: "done", detail: `${stale.length} fleet agent(s): plane.identifier -> ${NEW} (${hermesPath})`, files: [hermesPath] };
19297
+ } catch (error) {
19298
+ return { status: "failed", detail: message(error) };
19299
+ }
19300
+ };
19301
+ const agentsMdStep = async (doApply) => {
19302
+ const link = join31(repoPath, "AGENTS.md");
19303
+ if (!existsSync22(link)) return { status: "skipped", detail: "no AGENTS.md" };
19304
+ try {
19305
+ const path = realpathSync11(link);
19306
+ const inside = relative16(realpathSync11(repoPath), path);
19307
+ if (inside.startsWith("..") || isAbsolute11(inside)) {
19308
+ return { status: "skipped", detail: `AGENTS.md resolves outside the repo (${path}); left untouched` };
19309
+ }
19310
+ const original = readFileSync27(path, "utf8");
19311
+ const lines = original.split("\n");
19312
+ let boardLines = 0;
19313
+ let replaced = 0;
19314
+ let current = 0;
19315
+ const out = lines.map((line) => {
19316
+ if (!/^\s*Board:/.test(line)) return line;
19317
+ boardLines += 1;
19318
+ if (line.includes(`(${NEW})`)) current += 1;
19319
+ let next = line;
19320
+ for (const old of oldIdentifiers) {
19321
+ const swapped = next.replace(new RegExp(`\\(${escapeRegExp2(old)}\\)`, "g"), `(${NEW})`);
19322
+ if (swapped !== next) {
19323
+ replaced += 1;
19324
+ next = swapped;
19325
+ }
19326
+ }
19327
+ return next;
19328
+ });
19329
+ if (!boardLines) return { status: "skipped", detail: "AGENTS.md has no Board: line" };
19330
+ if (!replaced) {
19331
+ return current ? { status: "already", detail: `Board: line already reads (${NEW})` } : { status: "skipped", detail: `no (${oldIdentifier || "OLD"}) on the Board: line` };
19332
+ }
19333
+ if (doApply) atomicWrite(path, out.join("\n"));
19334
+ return { status: doApply ? "done" : "planned", detail: `Board: (${oldIdentifier}) -> (${NEW})`, files: [path] };
19335
+ } catch (error) {
19336
+ return { status: "failed", detail: message(error) };
19337
+ }
19338
+ };
19339
+ const hintsStep = async (doApply) => {
19340
+ const configDir = env2.PILOT_CONFIG_DIR?.trim() || join31(env2.XDG_CONFIG_HOME?.trim() || join31(home, ".config"), "pilot");
19341
+ const path = join31(configDir, "harvest", "project-hints.json");
19342
+ if (!existsSync22(path)) return { status: "skipped", detail: `no harvest hints at ${path}` };
19343
+ try {
19344
+ const hints = readJsonObject(path);
19345
+ const projects = hints.projects;
19346
+ if (!isRecord7(projects)) return { status: "skipped", detail: "hints file has no projects map" };
19347
+ if (oldId === newId || !Object.hasOwn(projects, oldId)) {
19348
+ return Object.hasOwn(projects, newId) ? { status: "already", detail: `hints already keyed ${newId}` } : { status: "skipped", detail: `no hints entry for ${oldId}` };
19349
+ }
19350
+ const collides = Object.hasOwn(projects, newId);
19351
+ const detail = collides ? `projects.${oldId} merged into the existing projects.${newId}; "${oldId}" kept as an alias` : `projects.${oldId} -> projects.${newId}, "${oldId}" kept as an alias`;
19352
+ if (!doApply) return { status: "planned", detail, files: [path] };
19353
+ const aliasesOf = (value) => isRecord7(value) && Array.isArray(value.aliases) ? value.aliases.map(String) : [];
19354
+ const moved = projects[oldId];
19355
+ const base = collides ? isRecord7(projects[newId]) ? { ...projects[newId] } : {} : isRecord7(moved) ? { ...moved } : {};
19356
+ base.aliases = [.../* @__PURE__ */ new Set([...aliasesOf(collides ? projects[newId] : moved), ...collides ? aliasesOf(moved) : [], oldId])];
19357
+ const rekeyed = {};
19358
+ for (const [key, value] of Object.entries(projects)) {
19359
+ if (key === oldId) {
19360
+ if (!collides) rekeyed[newId] = base;
19361
+ } else if (key === newId) rekeyed[newId] = base;
19362
+ else rekeyed[key] = value;
19363
+ }
19364
+ atomicWrite(path, `${JSON.stringify({ ...hints, projects: rekeyed }, null, 2)}
19365
+ `);
19366
+ return { status: "done", detail, files: [path] };
19367
+ } catch (error) {
19368
+ return { status: "failed", detail: message(error) };
19369
+ }
19370
+ };
19371
+ const candystoreStep = async (doApply) => {
19372
+ if (oldInput === newId && steps.every((step) => step.status === "already" || step.status === "skipped")) {
19373
+ return { status: "already", detail: "nothing changed; projection not re-synced" };
19374
+ }
19375
+ const cwd = join31(home, "code", "33GOD", "candystore");
19376
+ try {
19377
+ if (!statSync6(cwd).isDirectory()) throw new Error("not a directory");
19378
+ } catch {
19379
+ return { status: "skipped", detail: `no candystore checkout at ${cwd}` };
19380
+ }
19381
+ if (!doApply) return { status: "planned", detail: `mise run project:sync in ${cwd}` };
19382
+ const run = spawnSync15("mise", ["run", "project:sync"], { cwd, env: env2, shell: false, encoding: "utf8", timeout: 18e4, maxBuffer: 8 * 1024 * 1024 });
19383
+ if (run.error) {
19384
+ return run.error.code === "ENOENT" ? { status: "skipped", detail: "mise is not installed; run `mise run project:sync` in candystore" } : { status: "failed", detail: `mise run project:sync: ${run.error.message}` };
19385
+ }
19386
+ if (run.status !== 0) {
19387
+ const tail = `${run.stderr ?? ""}${run.stdout ?? ""}`.trim().split("\n").slice(-3).join(" | ").slice(0, 300);
19388
+ return { status: "failed", detail: `mise run project:sync exited ${run.status}${tail ? `: ${tail}` : ""}` };
19389
+ }
19390
+ return { status: "done", detail: "candystore project projection re-synced" };
19391
+ };
19392
+ const steps = [];
19393
+ const changedFiles = [];
19394
+ const note = (step, outcome) => {
19395
+ steps.push({ step, status: outcome.status, detail: outcome.detail });
19396
+ if (outcome.status === "done" || outcome.status === "planned") changedFiles.push(...outcome.files ?? []);
19397
+ };
19398
+ const critical = [
19399
+ ["plane", planeStep],
19400
+ ["manifest", manifestStep],
19401
+ ["registry", registryStep]
19402
+ ];
19403
+ let blockedBy;
19404
+ for (const [name, run] of critical) {
19405
+ if (blockedBy) {
19406
+ note(name, { status: "blocked", detail: `not attempted: ${blockedBy} failed` });
19407
+ continue;
19408
+ }
19409
+ const outcome = await run(apply);
19410
+ note(name, outcome);
19411
+ if (outcome.status === "failed") blockedBy = name;
19412
+ }
19413
+ const downstream = [
19414
+ ["role.yaml", roleStep],
19415
+ ["fleet-registry", fleetStep],
19416
+ ["AGENTS.md", agentsMdStep],
19417
+ ["harvest-hints", hintsStep],
19418
+ ["candystore", candystoreStep]
19419
+ ];
19420
+ for (const [name, run] of downstream) {
19421
+ if (blockedBy) note(name, { status: "blocked", detail: `not attempted: ${blockedBy} failed` });
19422
+ else note(name, await run(apply));
19423
+ }
19424
+ const changed = (name) => steps.some((step) => step.step === name && (step.status === "done" || step.status === "planned"));
19425
+ if (oldId !== newId && (changed("manifest") || changed("registry"))) {
19426
+ warnings.push("notebook state is keyed by the project hash and restarts fresh under the new id");
19427
+ }
19428
+ if (isPlane && changed("plane")) {
19429
+ warnings.push(`old ticket refs (${oldIdentifier || "OLD"}-n) stop resolving once the board prefix changes`);
19430
+ }
19431
+ const insideRepo = (file) => {
19432
+ const rel = relative16(repoPath, file);
19433
+ return rel !== "" && !rel.startsWith("..") && !isAbsolute11(rel);
19434
+ };
19435
+ if (changedFiles.some(insideRepo)) {
19436
+ warnings.push(`files changed inside ${repoPath} must be committed and pushed`);
19437
+ }
19438
+ const failed = steps.find((step) => step.status === "failed");
19439
+ const noop = steps.every((step) => step.status === "already" || step.status === "skipped");
19440
+ const result2 = {
19441
+ ok: !failed,
19442
+ apply,
19443
+ noop,
19444
+ resumed,
19445
+ oldId,
19446
+ newId,
19447
+ oldIdentifier,
19448
+ newIdentifier: NEW,
19449
+ provider: type || "none",
19450
+ workspace,
19451
+ boardId,
19452
+ registryPath,
19453
+ steps,
19454
+ warnings,
19455
+ changedFiles: [...new Set(changedFiles)]
19456
+ };
19457
+ if (failed) {
19458
+ result2.error = apply ? `${failed.step} failed: ${failed.detail}; re-run the same command to resume` : `${failed.step} would fail: ${failed.detail}; fix this before running with --apply`;
19459
+ result2.code = "step_failed";
19460
+ }
19461
+ return result2;
19462
+ }
19463
+
18934
19464
  // src/index.ts
18935
19465
  init_boardQuery();
18936
19466
 
18937
19467
  // src/describe/activity.ts
18938
- import { spawn as spawn3, spawnSync as spawnSync15 } from "node:child_process";
18939
- import { statSync as statSync6 } from "node:fs";
18940
- import { join as join31 } from "node:path";
19468
+ import { spawn as spawn3, spawnSync as spawnSync16 } from "node:child_process";
19469
+ import { statSync as statSync7 } from "node:fs";
19470
+ import { join as join32 } from "node:path";
18941
19471
  var ACTIVE_WINDOW_SECONDS = 24 * 60 * 60;
18942
19472
  var MAX_DIRTY_STATS = 500;
18943
19473
  var GIT_TIMEOUT_MS = 5e3;
18944
19474
  var GIT_MAX_BUFFER = 16 * 1024 * 1024;
18945
19475
  function git3(repo, args) {
18946
- const result2 = spawnSync15("git", ["-C", repo, ...args], {
19476
+ const result2 = spawnSync16("git", ["-C", repo, ...args], {
18947
19477
  encoding: "utf8",
18948
19478
  timeout: GIT_TIMEOUT_MS,
18949
19479
  maxBuffer: GIT_MAX_BUFFER
@@ -19102,7 +19632,7 @@ function uncommittedSource(repo, paths) {
19102
19632
  let newest = 0;
19103
19633
  for (const path of paths.slice(0, MAX_DIRTY_STATS)) {
19104
19634
  try {
19105
- const mtime = Math.floor(statSync6(join31(repo, path)).mtimeMs / 1e3);
19635
+ const mtime = Math.floor(statSync7(join32(repo, path)).mtimeMs / 1e3);
19106
19636
  if (mtime > newest) newest = mtime;
19107
19637
  } catch {
19108
19638
  }
@@ -19300,8 +19830,8 @@ function openUrl(url, env2 = process.env, platform = process.platform) {
19300
19830
  }
19301
19831
 
19302
19832
  // src/describe/index.ts
19303
- import { existsSync as existsSync22, readFileSync as readFileSync27, readdirSync as readdirSync11, statSync as statSync7 } from "node:fs";
19304
- import { join as join32, resolve as resolve23 } from "node:path";
19833
+ import { existsSync as existsSync23, readFileSync as readFileSync28, readdirSync as readdirSync12, statSync as statSync8 } from "node:fs";
19834
+ import { join as join33, resolve as resolve23 } from "node:path";
19305
19835
  init_project();
19306
19836
  init_config();
19307
19837
  init_state();
@@ -19357,7 +19887,7 @@ var CONFIG_FILES = [
19357
19887
  ];
19358
19888
  function readJson(path) {
19359
19889
  try {
19360
- const parsed = JSON.parse(readFileSync27(path, "utf8"));
19890
+ const parsed = JSON.parse(readFileSync28(path, "utf8"));
19361
19891
  return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : void 0;
19362
19892
  } catch {
19363
19893
  return void 0;
@@ -19379,23 +19909,23 @@ function describeType(repo) {
19379
19909
  const evidence = [];
19380
19910
  const note = (signal, file) => evidence.push(`${signal} (${file})`);
19381
19911
  for (const marker of LANGUAGE_MARKERS) {
19382
- if (!existsSync22(join32(repo, marker.file))) continue;
19912
+ if (!existsSync23(join33(repo, marker.file))) continue;
19383
19913
  if (!languages.includes(marker.language)) {
19384
19914
  languages.push(marker.language);
19385
19915
  note(marker.language, marker.file);
19386
19916
  }
19387
19917
  }
19388
19918
  try {
19389
- const dotnet = readdirSync11(repo).find((entry) => entry.endsWith(".csproj") || entry.endsWith(".sln"));
19919
+ const dotnet = readdirSync12(repo).find((entry) => entry.endsWith(".csproj") || entry.endsWith(".sln"));
19390
19920
  if (dotnet && !languages.includes("dotnet")) {
19391
19921
  languages.push("dotnet");
19392
19922
  note("dotnet", dotnet);
19393
19923
  }
19394
19924
  } catch {
19395
19925
  }
19396
- const pkg = readJson(join32(repo, "package.json"));
19926
+ const pkg = readJson(join33(repo, "package.json"));
19397
19927
  if (pkg) {
19398
- if (existsSync22(join32(repo, "tsconfig.json"))) {
19928
+ if (existsSync23(join33(repo, "tsconfig.json"))) {
19399
19929
  const index = languages.indexOf("javascript");
19400
19930
  if (index >= 0) languages.splice(index, 1);
19401
19931
  if (!languages.includes("typescript")) {
@@ -19429,14 +19959,14 @@ function describeType(repo) {
19429
19959
  ["hermes-fleet-host", "agents/hermes"]
19430
19960
  ];
19431
19961
  for (const [role, marker] of roleMarkers) {
19432
- if (!existsSync22(join32(repo, marker))) continue;
19962
+ if (!existsSync23(join33(repo, marker))) continue;
19433
19963
  roles.push(role);
19434
19964
  note(role, marker);
19435
19965
  }
19436
19966
  return { primaryLanguage: languages[0], languages, roles, evidence };
19437
19967
  }
19438
19968
  function describeIdentity(repo, registryPath) {
19439
- const manifestPath = join32(repo, ".project.json");
19969
+ const manifestPath = join33(repo, ".project.json");
19440
19970
  const manifest = readJson(manifestPath);
19441
19971
  const drift = [];
19442
19972
  let record3;
@@ -19529,12 +20059,12 @@ function describeSubsystems(repo, findings) {
19529
20059
  const markers = SUBSYSTEM_MARKERS[metadata.id] ?? [];
19530
20060
  const evidence = metadata.id === "notebook" ? (() => {
19531
20061
  try {
19532
- const manifest = JSON.parse(readFileSync27(join32(repo, ".project.json"), "utf8"));
20062
+ const manifest = JSON.parse(readFileSync28(join33(repo, ".project.json"), "utf8"));
19533
20063
  return manifest.notebook && typeof manifest.notebook === "object" ? [".project.json#notebook"] : [];
19534
20064
  } catch {
19535
20065
  return [];
19536
20066
  }
19537
- })() : markers.filter((marker) => existsSync22(join32(repo, marker)));
20067
+ })() : markers.filter((marker) => existsSync23(join33(repo, marker)));
19538
20068
  const rules = (byRecipe.get(metadata.id) ?? []).map((finding2) => ({
19539
20069
  id: finding2.id,
19540
20070
  title: finding2.title,
@@ -19553,7 +20083,7 @@ function describeNotebook(repo, registryPath) {
19553
20083
  try {
19554
20084
  const registry = loadProjectRegistry(registryPath);
19555
20085
  const project = Object.values(registry.projects).find((entry) => resolve23(entry.repo_path) === resolve23(repo));
19556
- const manifest = existsSync22(join32(repo, ".project.json")) ? JSON.parse(readFileSync27(join32(repo, ".project.json"), "utf8")) : void 0;
20086
+ const manifest = existsSync23(join33(repo, ".project.json")) ? JSON.parse(readFileSync28(join33(repo, ".project.json"), "utf8")) : void 0;
19557
20087
  const declared = Boolean(project?.notebook || manifest?.notebook && typeof manifest.notebook === "object");
19558
20088
  if (!declared) return { declared: false, bindingState: null, notebookId: null, overviewNoteId: null, health: null, remoteCheck: "skip", captureAdmission: null };
19559
20089
  const config = loadEffectiveNotebookConfig(repo, registryPath);
@@ -19571,7 +20101,7 @@ function describeNotebook(repo, registryPath) {
19571
20101
  }
19572
20102
  }
19573
20103
  function describeConfigFiles(repo) {
19574
- return CONFIG_FILES.filter((spec) => existsSync22(join32(repo, spec.path))).map((spec) => ({ path: spec.path, purpose: spec.purpose, subsystem: spec.subsystem }));
20104
+ return CONFIG_FILES.filter((spec) => existsSync23(join33(repo, spec.path))).map((spec) => ({ path: spec.path, purpose: spec.purpose, subsystem: spec.subsystem }));
19575
20105
  }
19576
20106
  function describeNextSteps(description, findings) {
19577
20107
  const steps = [];
@@ -19650,8 +20180,8 @@ function describeNextSteps(description, findings) {
19650
20180
  }
19651
20181
  async function describeProject(input = {}) {
19652
20182
  const repo = resolve23(input.repoArg ?? process.cwd());
19653
- if (!existsSync22(repo)) throw new Error(`Path does not exist: ${repo}`);
19654
- if (!statSync7(repo).isDirectory()) throw new Error(`Not a directory: ${repo}`);
20183
+ if (!existsSync23(repo)) throw new Error(`Path does not exist: ${repo}`);
20184
+ if (!statSync8(repo).isDirectory()) throw new Error(`Not a directory: ${repo}`);
19655
20185
  const registryPath = input.registryPath ?? projectRegistryPath();
19656
20186
  const report = await recipeRegistry.auditRecipes(lifecycleContext(repo, true, false, { registryPath }));
19657
20187
  const findings = report.rules;
@@ -19995,7 +20525,7 @@ init_style();
19995
20525
  // src/notebook/cli.ts
19996
20526
  init_project();
19997
20527
  init_capture();
19998
- import { closeSync as closeSync8, constants as constants6, existsSync as existsSync23, fstatSync as fstatSync4, lstatSync as lstatSync18, openSync as openSync8, readSync as readSync4 } from "node:fs";
20528
+ import { closeSync as closeSync8, constants as constants6, existsSync as existsSync24, fstatSync as fstatSync4, lstatSync as lstatSync18, openSync as openSync8, readSync as readSync4 } from "node:fs";
19999
20529
  import { resolve as resolve24 } from "node:path";
20000
20530
  import { createInterface } from "node:readline/promises";
20001
20531
 
@@ -20137,7 +20667,7 @@ async function execute(input) {
20137
20667
  }
20138
20668
  function readBoundedRegularFile(path, maxBytes) {
20139
20669
  const absolute = resolve24(path);
20140
- if (!existsSync23(absolute)) throw new NotebookError("INVALID_INPUT", `File not found: ${absolute}`);
20670
+ if (!existsSync24(absolute)) throw new NotebookError("INVALID_INPUT", `File not found: ${absolute}`);
20141
20671
  const before = lstatSync18(absolute);
20142
20672
  if (!before.isFile() || before.isSymbolicLink()) throw new NotebookError("INVALID_INPUT", `File must be a regular non-symlink: ${absolute}`);
20143
20673
  if (before.size > maxBytes) throw new NotebookError("INVALID_INPUT", `File exceeds the configured ceiling: ${absolute}`);
@@ -20321,7 +20851,7 @@ function registerNotebookCli(program2, module = new NotebookModule()) {
20321
20851
  const registry = loadProjectRegistry(registryFile);
20322
20852
  const root = notebookStateRoot();
20323
20853
  const explicit = process.env.PJ_NOTEBOOK_WORKER_PROJECT_SLUG?.toLowerCase();
20324
- const slug = explicit && registry.projects[explicit] ? explicit : Object.keys(registry.projects).find((candidate) => existsSync23(statePathForReceipt(root, candidate, options.receiptId)));
20854
+ const slug = explicit && registry.projects[explicit] ? explicit : Object.keys(registry.projects).find((candidate) => existsSync24(statePathForReceipt(root, candidate, options.receiptId)));
20325
20855
  if (!slug) throw new NotebookError("NOT_FOUND", "Receipt does not belong to a registered project");
20326
20856
  await runCaptureWorker(new NotebookModule({ registryPath: registryFile, stateRoot: root }), slug, options.receiptId);
20327
20857
  } catch (error) {
@@ -20377,7 +20907,7 @@ init_project();
20377
20907
  init_style();
20378
20908
 
20379
20909
  // src/tui/load.ts
20380
- import { existsSync as existsSync24 } from "node:fs";
20910
+ import { existsSync as existsSync25 } from "node:fs";
20381
20911
  import { fileURLToPath as fileURLToPath6 } from "node:url";
20382
20912
  var TuiUnavailableError = class extends Error {
20383
20913
  };
@@ -20399,7 +20929,7 @@ async function loadScreen(relativeToDist, base) {
20399
20929
  } catch (error) {
20400
20930
  const err = error;
20401
20931
  if (err?.code !== "ERR_MODULE_NOT_FOUND") throw error;
20402
- if (!existsSync24(fileURLToPath6(url))) {
20932
+ if (!existsSync25(fileURLToPath6(url))) {
20403
20933
  throw new TuiUnavailableError(`TUI screen ${relativeToDist} is not built (run: npm run build); looked for ${url.pathname}`);
20404
20934
  }
20405
20935
  if (typeof err.stack !== "string" || !RESOLVER_FRAME.test(err.stack)) throw error;
@@ -20412,9 +20942,9 @@ async function loadScreen(relativeToDist, base) {
20412
20942
  // src/crank/index.ts
20413
20943
  init_boardQuery();
20414
20944
  init_style();
20415
- import { spawnSync as spawnSync16 } from "node:child_process";
20416
- import { readFileSync as readFileSync28 } from "node:fs";
20417
- import { join as join33 } from "node:path";
20945
+ import { spawnSync as spawnSync17 } from "node:child_process";
20946
+ import { readFileSync as readFileSync29 } from "node:fs";
20947
+ import { join as join34 } from "node:path";
20418
20948
  var ZCRANK_REDIS_HOST = "127.0.0.1";
20419
20949
  var ZCRANK_REDIS_PORT = 6379;
20420
20950
  function redisEndpoint(env2 = process.env) {
@@ -20428,7 +20958,7 @@ var ZCRANK_SESSION = "Workspace";
20428
20958
  var ZCRANK_LOG_CAP = 500;
20429
20959
  var TAB_STATUS_SUFFIX = /[\s🟢🟡🔴🔔⚫✳✻●·─]+$/u;
20430
20960
  var defaultShell = (command, args) => {
20431
- const result2 = spawnSync16(command, args, { encoding: "utf8", timeout: 1e4 });
20961
+ const result2 = spawnSync17(command, args, { encoding: "utf8", timeout: 1e4 });
20432
20962
  if (result2.error) return { stdout: "", status: 127 };
20433
20963
  return { stdout: result2.stdout ?? "", status: result2.status ?? 1, stderr: result2.stderr ?? "" };
20434
20964
  };
@@ -20694,7 +21224,7 @@ function resolveTabForProject(project, live, storedTabs) {
20694
21224
  function projectNameFor(handle, registryName) {
20695
21225
  if (registryName && registryName.trim()) return registryName.trim();
20696
21226
  try {
20697
- const manifest = JSON.parse(readFileSync28(join33(handle.root, ".project.json"), "utf8"));
21227
+ const manifest = JSON.parse(readFileSync29(join34(handle.root, ".project.json"), "utf8"));
20698
21228
  const name = manifest.project_name;
20699
21229
  if (typeof name === "string" && name.trim()) return name.trim();
20700
21230
  } catch {
@@ -20705,7 +21235,7 @@ function projectAgentsFor(handle, registryAgents) {
20705
21235
  const fromRecord = Object.entries(registryAgents ?? {}).map(([agentHandle, value]) => ({ handle: agentHandle, role: String(value?.role ?? "") }));
20706
21236
  if (fromRecord.length) return fromRecord;
20707
21237
  try {
20708
- const manifest = JSON.parse(readFileSync28(join33(handle.root, ".project.json"), "utf8"));
21238
+ const manifest = JSON.parse(readFileSync29(join34(handle.root, ".project.json"), "utf8"));
20709
21239
  const agents = manifest.agents;
20710
21240
  if (agents && typeof agents === "object" && !Array.isArray(agents)) {
20711
21241
  return Object.entries(agents).map(([agentHandle, value]) => ({ handle: agentHandle, role: typeof value?.role === "string" ? value.role : "" }));
@@ -21112,13 +21642,13 @@ function printMigrationReport(report, asJson) {
21112
21642
  }
21113
21643
  async function promptForRuleIds(rules) {
21114
21644
  const fixable = rules.filter((rule) => rule.fixable);
21115
- const { message, options } = formatRulePicker(fixable);
21645
+ const { message: message2, options } = formatRulePicker(fixable);
21116
21646
  if (!options.length) {
21117
21647
  return [];
21118
21648
  }
21119
21649
  const initialValues = fixable.filter((rule) => rule.status !== "pass" && rule.status !== "skip").map((rule) => rule.id);
21120
21650
  const selected = await multiselect({
21121
- message,
21651
+ message: message2,
21122
21652
  options,
21123
21653
  initialValues
21124
21654
  });
@@ -21128,16 +21658,16 @@ async function promptForRuleIds(rules) {
21128
21658
  return selected;
21129
21659
  }
21130
21660
  function readJson2(path) {
21131
- if (!existsSync26(path)) return void 0;
21661
+ if (!existsSync27(path)) return void 0;
21132
21662
  try {
21133
- const parsed = JSON.parse(readFileSync29(path, "utf8"));
21663
+ const parsed = JSON.parse(readFileSync30(path, "utf8"));
21134
21664
  return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : void 0;
21135
21665
  } catch {
21136
21666
  return void 0;
21137
21667
  }
21138
21668
  }
21139
21669
  function findGitRoot(cwd) {
21140
- const result2 = spawnSync17("git", ["rev-parse", "--show-toplevel"], { cwd, encoding: "utf8" });
21670
+ const result2 = spawnSync18("git", ["rev-parse", "--show-toplevel"], { cwd, encoding: "utf8" });
21141
21671
  if (result2.status !== 0) return void 0;
21142
21672
  return resolve25(result2.stdout.trim());
21143
21673
  }
@@ -21147,8 +21677,8 @@ function packageNameToProjectName(value) {
21147
21677
  return name.replace(/[-_]+/g, " ").replace(/\b\w/g, (letter) => letter.toUpperCase()).trim();
21148
21678
  }
21149
21679
  function deriveProjectDefaults(targetDir) {
21150
- const manifest = readJson2(join34(targetDir, ".project.json"));
21151
- const pkg = readJson2(join34(targetDir, "package.json"));
21680
+ const manifest = readJson2(join35(targetDir, ".project.json"));
21681
+ const pkg = readJson2(join35(targetDir, "package.json"));
21152
21682
  const name = String(manifest?.project_name ?? "").trim() || packageNameToProjectName(typeof pkg?.name === "string" ? pkg.name : void 0) || packageNameToProjectName(basename11(targetDir)) || "Project";
21153
21683
  const ticketProvider = manifest?.ticket_provider && typeof manifest.ticket_provider === "object" ? manifest.ticket_provider : {};
21154
21684
  return {
@@ -21164,9 +21694,9 @@ function deriveProjectDefaults(targetDir) {
21164
21694
  function isInteractiveProjectInit(options) {
21165
21695
  return !options.json && !options.yes && options.tui !== false && Boolean(process.stdin.isTTY && process.stdout.isTTY);
21166
21696
  }
21167
- async function promptTextValue(message, initialValue) {
21697
+ async function promptTextValue(message2, initialValue) {
21168
21698
  const value = await text({
21169
- message,
21699
+ message: message2,
21170
21700
  initialValue,
21171
21701
  validate: (input) => input?.trim() ? void 0 : "Required"
21172
21702
  });
@@ -21223,7 +21753,7 @@ function actionNeedsRun(plan, kind, syncMode) {
21223
21753
  if (!action || action.kind !== "project.write-manifest") return false;
21224
21754
  const next = `${JSON.stringify(action.manifest, null, 2)}
21225
21755
  `;
21226
- return !existsSync26(action.path) || readFileSync29(action.path, "utf8") !== next;
21756
+ return !existsSync27(action.path) || readFileSync30(action.path, "utf8") !== next;
21227
21757
  }
21228
21758
  if (kind === "copier.copy.commonproject") return true;
21229
21759
  if (kind === "ticket-provider.create-or-link") return plan.actions.some((action) => action.kind === kind && action.enabled);
@@ -21280,7 +21810,7 @@ async function resolveProjectInitTarget(name, options) {
21280
21810
  if (!targetDir && cwdGitRoot) {
21281
21811
  targetDir = cwdGitRoot;
21282
21812
  }
21283
- if (!targetDir && !name && existsSync26(join34(cwd, ".project.json"))) {
21813
+ if (!targetDir && !name && existsSync27(join35(cwd, ".project.json"))) {
21284
21814
  targetDir = cwd;
21285
21815
  }
21286
21816
  if (!targetDir && interactive) {
@@ -21291,14 +21821,14 @@ async function resolveProjectInitTarget(name, options) {
21291
21821
  if (!targetDir) {
21292
21822
  throw new Error("Project name or --target-dir is required when project init is not run inside a git repo or a directory carrying .project.json");
21293
21823
  }
21294
- const targetExists = existsSync26(targetDir);
21295
- if (targetExists && !statSync8(targetDir).isDirectory()) throw new Error(`Target path is not a directory: ${targetDir}`);
21824
+ const targetExists = existsSync27(targetDir);
21825
+ if (targetExists && !statSync9(targetDir).isDirectory()) throw new Error(`Target path is not a directory: ${targetDir}`);
21296
21826
  const targetGitRoot = targetExists ? findGitRoot(targetDir) : void 0;
21297
- const alreadyScaffolded = targetExists && (existsSync26(join34(targetDir, ".project.json")) || existsSync26(join34(targetDir, ".copier-answers.yml")));
21827
+ const alreadyScaffolded = targetExists && (existsSync27(join35(targetDir, ".project.json")) || existsSync27(join35(targetDir, ".copier-answers.yml")));
21298
21828
  const syncMode = Boolean(
21299
21829
  targetGitRoot && resolve25(targetGitRoot) === resolve25(targetDir) || alreadyScaffolded
21300
21830
  );
21301
- if (targetExists && !syncMode && !options.force && readdirSync12(targetDir).length > 0) {
21831
+ if (targetExists && !syncMode && !options.force && readdirSync13(targetDir).length > 0) {
21302
21832
  throw new Error(`Target already exists and is not empty: ${targetDir} (pass --force to render over it)`);
21303
21833
  }
21304
21834
  const defaults = targetExists ? deriveProjectDefaults(targetDir) : { name: packageNameToProjectName(basename11(targetDir)) ?? "Project", description: "" };
@@ -21384,8 +21914,8 @@ program.command("subsystems").description("List available subsystems").action(()
21384
21914
  console.log("");
21385
21915
  });
21386
21916
  function boardFailure(error) {
21387
- const message = error instanceof BoardError || error instanceof Error ? error.message : String(error);
21388
- console.error(`${xmark2} ${message}`);
21917
+ const message2 = error instanceof BoardError || error instanceof Error ? error.message : String(error);
21918
+ console.error(`${xmark2} ${message2}`);
21389
21919
  process.exit(1);
21390
21920
  }
21391
21921
  function parseTicketCount(raw, fallback2) {
@@ -21601,6 +22131,39 @@ async function runProjectInit(name, options) {
21601
22131
  process.exit(1);
21602
22132
  }
21603
22133
  }
22134
+ var RENAME_MARK = {
22135
+ planned: "plan",
22136
+ done: "done",
22137
+ already: "ok",
22138
+ skipped: "skip",
22139
+ blocked: "blocked",
22140
+ failed: "FAIL"
22141
+ };
22142
+ function formatRenameResult(result2) {
22143
+ const lines = [""];
22144
+ const headline = result2.noop ? "Nothing to rename" : result2.apply ? result2.ok ? "Renamed" : "Rename incomplete" : "Would rename";
22145
+ const mark = !result2.ok ? red(glyph.fail) : result2.apply && !result2.noop ? green(glyph.pass) : yellow(glyph.warn);
22146
+ lines.push(` ${mark} ${bold(headline)} ${dim(glyph.dot)} ${cyan(result2.oldId)} ${dim(glyph.pointer)} ${cyan(result2.newId)}${result2.resumed ? dim(" (resuming an earlier run)") : ""}`);
22147
+ lines.push(` ${dim("board".padEnd(14))} ${result2.oldIdentifier || dim("(none)")} ${dim(glyph.pointer)} ${result2.newIdentifier}${result2.boardId ? dim(` ${result2.provider}/${result2.workspace} ${result2.boardId}`) : ""}`);
22148
+ lines.push("");
22149
+ const width = Math.max(...result2.steps.map((step) => step.step.length));
22150
+ for (const step of result2.steps) {
22151
+ const label = RENAME_MARK[step.status].padEnd(7);
22152
+ const colored = step.status === "failed" ? red(label) : step.status === "done" ? green(label) : step.status === "planned" ? yellow(label) : dim(label);
22153
+ lines.push(` ${colored} ${step.step.padEnd(width)} ${step.status === "failed" ? step.detail : dim(step.detail)}`);
22154
+ }
22155
+ if (result2.changedFiles.length) {
22156
+ lines.push("");
22157
+ lines.push(` ${dim(result2.apply ? "files changed" : "files that would change")}`);
22158
+ for (const file of result2.changedFiles) lines.push(` ${dim(file)}`);
22159
+ }
22160
+ lines.push("");
22161
+ for (const warning of result2.warnings) lines.push(` ${yellow(glyph.warn)} ${dim(warning)}`);
22162
+ if (result2.error) lines.push(` ${red(glyph.fail)} ${result2.error}`);
22163
+ if (!result2.apply) lines.push(` ${dim("dry run \u2014 re-run with --apply to write")}`);
22164
+ lines.push("");
22165
+ return lines.join("\n");
22166
+ }
21604
22167
  function registerProjectCommands(parent, legacy = false) {
21605
22168
  parent.command("list").description("List projects in the pjangler registry").option("--registry <location>", `Registry service URL or fixture path (default: ${projectRegistryPath()})`).option("--json", "Output machine-parseable JSON").action(async (options) => {
21606
22169
  try {
@@ -21658,9 +22221,9 @@ function registerProjectCommands(parent, legacy = false) {
21658
22221
  if (!result2.apply) console.log(` ${dim("dry run \u2014 re-run with --apply to write")}`);
21659
22222
  console.log("");
21660
22223
  } catch (err) {
21661
- const message = err instanceof Error ? err.message : String(err);
21662
- if (options.json) console.log(JSON.stringify({ ok: false, error: message }, null, 2));
21663
- else console.error(`${xmark2} project remove failed: ${message}`);
22224
+ const message2 = err instanceof Error ? err.message : String(err);
22225
+ if (options.json) console.log(JSON.stringify({ ok: false, error: message2 }, null, 2));
22226
+ else console.error(`${xmark2} project remove failed: ${message2}`);
21664
22227
  process.exit(1);
21665
22228
  }
21666
22229
  });
@@ -21690,9 +22253,28 @@ function registerProjectCommands(parent, legacy = false) {
21690
22253
  if (!result2.apply) console.log(` ${dim("dry run \u2014 re-run with --apply to write")}`);
21691
22254
  console.log("");
21692
22255
  } catch (err) {
21693
- const message = err instanceof Error ? err.message : String(err);
21694
- if (options.json) console.log(JSON.stringify({ ok: false, error: message }, null, 2));
21695
- else console.error(`${xmark2} project link failed: ${message}`);
22256
+ const message2 = err instanceof Error ? err.message : String(err);
22257
+ if (options.json) console.log(JSON.stringify({ ok: false, error: message2 }, null, 2));
22258
+ else console.error(`${xmark2} project link failed: ${message2}`);
22259
+ process.exit(1);
22260
+ }
22261
+ });
22262
+ parent.command("rename").argument("<old-id>", "Current project ID").argument("<new-id>", "New project ID; also becomes the board identifier, upper-cased (1-12 lowercase letters/digits)").description("Rename a project: one key becomes the project ID and the board identifier, across registry, manifest and provider").option("--apply", "Write the rename (default is a dry run)").option("--registry <location>", `Registry service URL or fixture path (default: ${projectRegistryPath()})`).option("--json", "Output machine-parseable JSON").action(async (oldId, newId, options) => {
22263
+ try {
22264
+ const result2 = await renameProject({
22265
+ oldId,
22266
+ newId,
22267
+ apply: Boolean(options.apply),
22268
+ registryPath: options.registry
22269
+ });
22270
+ if (options.json) console.log(JSON.stringify(result2, null, 2));
22271
+ else console.log(formatRenameResult(result2));
22272
+ if (!result2.ok) process.exit(1);
22273
+ } catch (err) {
22274
+ const code = err instanceof RenameError ? err.code : "error";
22275
+ const text2 = err instanceof Error ? err.message : String(err);
22276
+ if (options.json) console.log(JSON.stringify({ ok: false, error: text2, code }, null, 2));
22277
+ else console.error(`${xmark2} project rename failed: [${code}] ${text2}`);
21696
22278
  process.exit(1);
21697
22279
  }
21698
22280
  });
@@ -21764,7 +22346,7 @@ program.command("reindex").argument("[project-id]", "Canonical project ID (defau
21764
22346
  else if (options.all) result2 = registryRequest(location, "POST", "/v1/reindex", {});
21765
22347
  else {
21766
22348
  const repo = projectId ? getProject(loadProjectRegistry(location), projectId).repo_path : resolveProject(process.cwd()).root;
21767
- result2 = registryRequest(location, "POST", "/v1/index", { manifest_path: join34(repo, ".project.json") });
22349
+ result2 = registryRequest(location, "POST", "/v1/index", { manifest_path: join35(repo, ".project.json") });
21768
22350
  }
21769
22351
  const statuses = result2.__registry_status;
21770
22352
  const stale = Object.entries(statuses ?? {}).filter(([, item]) => item.status !== "ok");
@@ -13329,6 +13329,18 @@ init_project();
13329
13329
  import { spawnSync as spawnSync13 } from "node:child_process";
13330
13330
  import { existsSync as existsSync18, lstatSync as lstatSync16, readFileSync as readFileSync20, rmSync as rmSync8 } from "node:fs";
13331
13331
  import { dirname as dirname17, isAbsolute as isAbsolute10, join as join23, relative as relativePath, resolve as resolve18 } from "node:path";
13332
+
13333
+ // src/project/ticketScope.ts
13334
+ function ticketScope(ticket) {
13335
+ const block = ticket !== null && typeof ticket === "object" ? ticket : {};
13336
+ const label = typeof block.scope_label === "string" ? block.scope_label.trim() : "";
13337
+ return {
13338
+ ...label ? { scope_label: label } : {},
13339
+ ...block.scope_default === true ? { scope_default: true } : {}
13340
+ };
13341
+ }
13342
+
13343
+ // src/recipes/ProjectRecipe.ts
13332
13344
  init_tree_diff();
13333
13345
 
13334
13346
  // src/recipes/NotebookRecipe.ts
@@ -15214,6 +15226,7 @@ function refreshPlanFromCanonicalManifest(plan) {
15214
15226
  ...carriesProvenance && identifierSource === "provider" && recorded?.identifier_fetched_at ? { identifier_fetched_at: recorded.identifier_fetched_at } : {},
15215
15227
  board_id: manifestBoardId,
15216
15228
  ...boardConfirmedAt ? { board_confirmed_at: boardConfirmedAt } : {},
15229
+ ...ticketScope(manifestTicket),
15217
15230
  state: manifestState === "skipped" ? "skipped" : manifestBoardId && boardConfirmedAt && keyIsProven ? "linked" : "planned"
15218
15231
  };
15219
15232
  plan.manifest = manifest;
@@ -225,7 +225,8 @@ function validateManifestFields(manifest, path) {
225
225
  if (!object(artifact)) fail(`source_artifacts.${index}`, "an object");
226
226
  strings(artifact, ["kind", "path", "package_name"], `source_artifacts.${index}.`);
227
227
  }
228
- if (manifest.ticket_provider) strings(manifest.ticket_provider, ["type", "workspace", "identifier", "identifier_source", "identifier_fetched_at", "board_id", "board_confirmed_at", "board_url", "state"], "ticket_provider.");
228
+ if (manifest.ticket_provider) strings(manifest.ticket_provider, ["type", "workspace", "identifier", "identifier_source", "identifier_fetched_at", "board_id", "board_confirmed_at", "board_url", "state", "scope_label"], "ticket_provider.");
229
+ if (manifest.ticket_provider?.scope_default !== void 0 && typeof manifest.ticket_provider.scope_default !== "boolean") fail("ticket_provider.scope_default", "a boolean");
229
230
  for (const [name, agent] of Object.entries(manifest.agents ?? {})) {
230
231
  if (!object(agent)) fail(`agents.${name}`, "an object");
231
232
  strings(agent, ["role", "role_dir", "provisioning_state"], `agents.${name}.`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@delorenj/pjangler",
3
- "version": "1.5.36",
3
+ "version": "1.5.38",
4
4
  "description": "Project subsystem bootstrapper CLI",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",