@delorenj/pjangler 1.5.52 → 1.5.54

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
@@ -457,9 +457,44 @@ var init_notebookSettingsValidation = __esm({
457
457
  });
458
458
 
459
459
  // src/project/executionBinding.ts
460
+ function executionReadiness(manifest) {
461
+ const execution = manifest.execution;
462
+ if (!execution || execution.mode === "legacy") return [];
463
+ const errors = [];
464
+ if (!["shadow", "managed"].includes(execution.mode)) errors.push("execution.mode must be legacy, shadow or managed");
465
+ if (!manifest.project_id) errors.push("canonical project_id missing");
466
+ if (manifest.ticket_provider?.type !== "plane" || !manifest.ticket_provider.workspace || !manifest.ticket_provider.board_id) errors.push("exact Plane binding missing");
467
+ if (execution.policy_version !== 2 || !execution.skill_version) errors.push("execution policy or skill pin missing");
468
+ for (const lane of EXECUTION_LANES) if (!execution.states?.[lane]) errors.push(`lane binding missing: ${lane}`);
469
+ for (const pin of ["pilot_bundle_sha256", "momo_bundle_sha256"]) if (!/^[a-f0-9]{64}$/.test(execution[pin] ?? "")) errors.push(`${pin} missing`);
470
+ if (!execution.working_label) errors.push("working label binding missing");
471
+ if (!execution.actors?.[execution.pm_actor] || execution.actors[execution.pm_actor].role !== "pm") errors.push("PM actor enrollment missing");
472
+ if (!execution.actors?.[execution.controller_actor] || execution.actors[execution.controller_actor].role !== "operator") errors.push("controller repair actor enrollment missing");
473
+ const ids = /* @__PURE__ */ new Set();
474
+ for (const [name, actor] of Object.entries(execution.actors ?? {})) {
475
+ if (!actor.native_user_id || !actor.key_ref?.startsWith("op://") || !actor.runtime_id) errors.push(`actor ${name}: native identity, op reference and runtime required`);
476
+ if (ids.has(actor.native_user_id)) errors.push(`actor ${name}: duplicate native identity`);
477
+ ids.add(actor.native_user_id);
478
+ if (!["pm", "operator", "reviewer", "interactive"].includes(actor.role)) errors.push(`actor ${name}: invalid role`);
479
+ if (name === execution.pm_actor && (!Array.isArray(actor.runtime?.planner_argv) || !actor.runtime.planner_argv.length || actor.runtime.planner_argv.some((x) => typeof x !== "string" || !x || x.includes("\0")))) errors.push("PM planner argv missing");
480
+ if (actor.runtime?.adapter !== "systemd" || !/^[a-zA-Z0-9_-]+$/.test(actor.runtime?.unit_prefix ?? "")) errors.push(`actor ${name}: supervised runtime missing`);
481
+ }
482
+ if (execution.legacy_writers_fenced !== true) errors.push("legacy writers not fenced");
483
+ return errors;
484
+ }
485
+ function validateExecutionBinding(manifest) {
486
+ if (!manifest.execution) return;
487
+ if (typeof manifest.execution !== "object" || Array.isArray(manifest.execution)) throw new Error("execution must be an object");
488
+ if (manifest.execution.mode === "managed") {
489
+ const errors = executionReadiness(manifest);
490
+ if (errors.length) throw new Error(`Managed execution not ready: ${errors.join("; ")}`);
491
+ } else if (!["legacy", "shadow"].includes(manifest.execution.mode)) throw new Error("invalid execution mode");
492
+ }
493
+ var EXECUTION_LANES;
460
494
  var init_executionBinding = __esm({
461
495
  "src/project/executionBinding.ts"() {
462
496
  "use strict";
497
+ EXECUTION_LANES = ["Backlog", "Needs Re-evaluation", "Todo", "In Progress", "E2E Testing & QA", "Ready for Documentation", "Done", "Needs Attention", "Cancelled"];
463
498
  }
464
499
  });
465
500
 
@@ -472,6 +507,46 @@ function normalizeProjectId2(value) {
472
507
  throw new RegistryError(error instanceof Error ? error.message : String(error));
473
508
  }
474
509
  }
510
+ function object(value) {
511
+ return !!value && typeof value === "object" && !Array.isArray(value);
512
+ }
513
+ function validateManifestShape(manifest, path) {
514
+ validateExecutionBinding(manifest);
515
+ const fail2 = (field2, shape) => {
516
+ throw new RegistryError(`Manifest ${path}: ${field2} must be ${shape}`);
517
+ };
518
+ const strings = (value, names, prefix = "") => {
519
+ for (const name of names) if (value[name] !== void 0 && typeof value[name] !== "string") fail2(`${prefix}${name}`, "a string");
520
+ };
521
+ strings(manifest, ["project_name", "name", "project_description", "description", "status", "repo_path", "created_at", "updated_at"]);
522
+ for (const field2 of ["ticket_provider", "agents", "template", "notebook", "automation"]) if (manifest[field2] !== void 0 && !object(manifest[field2])) fail2(field2, "an object");
523
+ if (manifest.source_artifacts !== void 0 && !Array.isArray(manifest.source_artifacts)) fail2("source_artifacts", "an array");
524
+ for (const [index, artifact] of (manifest.source_artifacts ?? []).entries()) {
525
+ if (!object(artifact)) fail2(`source_artifacts.${index}`, "an object");
526
+ strings(artifact, ["kind", "path", "package_name"], `source_artifacts.${index}.`);
527
+ }
528
+ 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.");
529
+ if (manifest.ticket_provider?.scope_default !== void 0 && typeof manifest.ticket_provider.scope_default !== "boolean") fail2("ticket_provider.scope_default", "a boolean");
530
+ for (const [name, agent] of Object.entries(manifest.agents ?? {})) {
531
+ if (!object(agent)) fail2(`agents.${name}`, "an object");
532
+ strings(agent, ["role", "role_dir", "provisioning_state"], `agents.${name}.`);
533
+ }
534
+ if (manifest.template?.commonproject !== void 0) {
535
+ if (!object(manifest.template.commonproject)) fail2("template.commonproject", "an object");
536
+ const template = manifest.template.commonproject;
537
+ if (template.enabled !== void 0 && typeof template.enabled !== "boolean") fail2("template.commonproject.enabled", "boolean");
538
+ strings(template, ["primary_language", "project_type"], "template.commonproject.");
539
+ }
540
+ for (const field2 of ["binding", "policy"]) if (manifest.notebook?.[field2] !== void 0 && !object(manifest.notebook[field2])) fail2(`notebook.${field2}`, "an object");
541
+ if (manifest.notebook?.binding) strings(manifest.notebook.binding, ["provider", "state", "notebook_name", "blocked_reason", "notebook_id", "overview_note_id", "created_at", "updated_at"], "notebook.binding.");
542
+ if (manifest.notebook?.policy) {
543
+ try {
544
+ validateGlobalNotebookConfig({ defaults: manifest.notebook.policy });
545
+ } catch (error) {
546
+ throw new RegistryError(`Manifest ${path}: invalid notebook policy: ${String(error)}`);
547
+ }
548
+ }
549
+ }
475
550
  function recordFromManifest(manifest, path) {
476
551
  const id = normalizeProjectId2(manifest.project_id ?? manifest.project_slug);
477
552
  const { project_slug: _old, project_name: _name, project_description: _description, notebook, ...rest } = manifest;
@@ -1641,12 +1716,40 @@ function resolveSourceSkillPath(sourceSkill, env2 = process.env) {
1641
1716
  const hint = `${searched} Add project-specific roots with ${PROJECT_SOURCE_SKILL_ROOTS_ENV}.`;
1642
1717
  throw new Error(`Source skill not found: ${sourceSkill}.${hint}`);
1643
1718
  }
1719
+ function looksLikePath(value) {
1720
+ return /^(?:\/|~\/|\.{1,2}\/)/.test(value);
1721
+ }
1722
+ function normalizeLegacyManifest(manifest) {
1723
+ const artifacts = manifest.source_artifacts;
1724
+ if (!Array.isArray(artifacts)) return { manifest, notes: [] };
1725
+ const notes = [];
1726
+ const normalized = artifacts.map((artifact, index) => {
1727
+ if (typeof artifact !== "string" || !looksLikePath(artifact)) return artifact;
1728
+ notes.push(`.project.json source_artifacts.${index}: bare path normalized to {"kind": "${LEGACY_SOURCE_ARTIFACT_KIND}", "path": "${artifact}"}`);
1729
+ return { kind: LEGACY_SOURCE_ARTIFACT_KIND, path: artifact };
1730
+ });
1731
+ return notes.length ? { manifest: { ...manifest, source_artifacts: normalized }, notes } : { manifest, notes };
1732
+ }
1644
1733
  function planProjectInit(input) {
1645
1734
  if (!input.name.trim()) throw new Error("Project name is required");
1646
1735
  const targetDir = resolve11(input.targetDir ?? defaultProjectTargetDir(input.name, input.cwd));
1647
1736
  const localManifestPath = join18(targetDir, ".project.json");
1648
1737
  const localManifestContent = existsSync11(localManifestPath) ? readFileSync17(localManifestPath, "utf8") : void 0;
1649
- const localManifest = localManifestContent === void 0 ? void 0 : JSON.parse(localManifestContent);
1738
+ let localManifest;
1739
+ const manifestNotes = [];
1740
+ if (localManifestContent !== void 0) {
1741
+ let parsed;
1742
+ try {
1743
+ parsed = JSON.parse(localManifestContent);
1744
+ } catch {
1745
+ throw new Error(`Malformed JSON: ${localManifestPath}`);
1746
+ }
1747
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error(`Expected a JSON object: ${localManifestPath}`);
1748
+ const normalized = normalizeLegacyManifest(parsed);
1749
+ localManifest = normalized.manifest;
1750
+ manifestNotes.push(...normalized.notes);
1751
+ validateManifestShape(localManifest, localManifestPath);
1752
+ }
1650
1753
  const localId = localManifest?.project_id ?? localManifest?.project_slug;
1651
1754
  const slug = normalizeProjectId(input.projectSlug ?? localId ?? slugifyProjectName(input.name));
1652
1755
  if (localId && normalizeProjectId(localId) !== slug) throw new Error(`Project ID ${slug} conflicts with authoritative manifest ${localId}`);
@@ -1741,6 +1844,7 @@ function planProjectInit(input) {
1741
1844
  validateNoDuplicateProject(registry, project, overwrite);
1742
1845
  const pjanglerRoot = resolve11(input.pjanglerRoot ?? resolvePjanglerRoot());
1743
1846
  const manifest = projectManifestFromRegistryProject(project);
1847
+ validateManifestShape(manifest, localManifestPath);
1744
1848
  const apply = input.apply ?? false;
1745
1849
  const live = input.live ?? false;
1746
1850
  const provisionTicketBoard = input.provisionTicketBoard ?? true;
@@ -1796,7 +1900,7 @@ function planProjectInit(input) {
1796
1900
  project,
1797
1901
  manifest,
1798
1902
  actions,
1799
- ...input.boardUrl !== void 0 ? { warnings: [BOARD_URL_DEPRECATION_WARNING] } : {}
1903
+ ...input.boardUrl !== void 0 || manifestNotes.length ? { warnings: [...input.boardUrl !== void 0 ? [BOARD_URL_DEPRECATION_WARNING] : [], ...manifestNotes] } : {}
1800
1904
  };
1801
1905
  }
1802
1906
  function linkTicketProviderBoard(plan, action, boardId, identifier, now = /* @__PURE__ */ new Date()) {
@@ -2264,7 +2368,7 @@ function expandHome(path) {
2264
2368
  function isRecord3(value) {
2265
2369
  return typeof value === "object" && value !== null && !Array.isArray(value);
2266
2370
  }
2267
- var PROJECT_SOURCE_SKILL_ROOTS_ENV, TICKET_PROVIDER_ADAPTERS_ENV, PROJECT_REGISTRY_SCHEMA_VERSION, DEFAULT_NEW_PROJECT_STATUS, PROJECT_TYPES, PROJECT_TYPE_LANGUAGES, PROJECT_TYPE_SENTINELS, 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;
2371
+ var PROJECT_SOURCE_SKILL_ROOTS_ENV, TICKET_PROVIDER_ADAPTERS_ENV, PROJECT_REGISTRY_SCHEMA_VERSION, DEFAULT_NEW_PROJECT_STATUS, PROJECT_TYPES, PROJECT_TYPE_LANGUAGES, PROJECT_TYPE_SENTINELS, 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, LEGACY_SOURCE_ARTIFACT_KIND;
2268
2372
  var init_project = __esm({
2269
2373
  "src/project/index.ts"() {
2270
2374
  "use strict";
@@ -2320,6 +2424,7 @@ var init_project = __esm({
2320
2424
  GLOBAL_NOTEBOOK_LIMITS_OWNED_KEYS = Object.keys(DEFAULT_NOTEBOOK_LIMITS);
2321
2425
  GLOBAL_NOTEBOOK_SUMMARIZER_OWNED_KEYS = ["executable", "args"];
2322
2426
  SAFE_PATH_SEGMENT = /^[A-Za-z0-9](?:[A-Za-z0-9._-]*[A-Za-z0-9])?$/;
2427
+ LEGACY_SOURCE_ARTIFACT_KIND = "reference";
2323
2428
  }
2324
2429
  });
2325
2430
 
@@ -6353,6 +6458,10 @@ function backupToSnapshot(backup) {
6353
6458
  }
6354
6459
  return snapshot;
6355
6460
  }
6461
+ function removeEntry(full, stat) {
6462
+ if (stat.isDirectory() && !stat.isSymbolicLink()) rmSync(full, { recursive: true, force: true });
6463
+ else unlinkSync(full);
6464
+ }
6356
6465
  function restoreSingleEntry(root, rel, entry) {
6357
6466
  const full = rel === "." ? root : join3(root, rel);
6358
6467
  let currentStat;
@@ -6363,7 +6472,7 @@ function restoreSingleEntry(root, rel, entry) {
6363
6472
  }
6364
6473
  if (entry.kind === "dir") {
6365
6474
  if (currentStat && (!currentStat.isDirectory() || currentStat.isSymbolicLink())) {
6366
- rmSync(full, { recursive: true, force: true });
6475
+ removeEntry(full, currentStat);
6367
6476
  currentStat = void 0;
6368
6477
  }
6369
6478
  if (!currentStat) mkdirSync2(full, { recursive: true });
@@ -6374,7 +6483,7 @@ function restoreSingleEntry(root, rel, entry) {
6374
6483
  if (entry.kind === "link") {
6375
6484
  if (currentStat) {
6376
6485
  if (currentStat.isSymbolicLink() && readlinkSync2(full) === entry.target) return;
6377
- rmSync(full, { recursive: true, force: true });
6486
+ removeEntry(full, currentStat);
6378
6487
  }
6379
6488
  symlinkSync(entry.target, full);
6380
6489
  return;
@@ -6383,7 +6492,7 @@ function restoreSingleEntry(root, rel, entry) {
6383
6492
  if ((currentStat.mode & 4095) === entry.mode && readFileSync2(full).equals(entry.bytes)) return;
6384
6493
  unlinkSync(full);
6385
6494
  } else if (currentStat) {
6386
- rmSync(full, { recursive: true, force: true });
6495
+ removeEntry(full, currentStat);
6387
6496
  }
6388
6497
  writeFileSync2(full, entry.bytes);
6389
6498
  chmodSync(full, entry.mode);
@@ -6992,10 +7101,10 @@ function openDisplacedStash(projectRoot) {
6992
7101
  index.push({ rel, kind: "link", target });
6993
7102
  count++;
6994
7103
  } else if (bytes) {
6995
- const object = createHash4("sha256").update(bytes).digest("hex");
6996
- const objectPath = join5(open(), "objects", object);
7104
+ const object2 = createHash4("sha256").update(bytes).digest("hex");
7105
+ const objectPath = join5(open(), "objects", object2);
6997
7106
  if (!existsSync3(objectPath)) writeFileSync3(objectPath, bytes);
6998
- index.push({ rel, kind: "file", mode: stat.mode & 4095, object });
7107
+ index.push({ rel, kind: "file", mode: stat.mode & 4095, object: object2 });
6999
7108
  count++;
7000
7109
  } else if (stat.isDirectory()) {
7001
7110
  open();
@@ -9033,9 +9142,13 @@ async function predictRootCollisions(ctx, incoming) {
9033
9142
  async function auditProjectSkills(ctx) {
9034
9143
  const result2 = await inspectStatus(skillCoreOptions(ctx));
9035
9144
  const details = skillDiagnostics(result2.findings);
9145
+ let findings = result2.findings.filter((finding2) => finding2.severity !== "info").length;
9036
9146
  details.push(...(result2.data?.changes ?? []).map((change) => `${change.action}: ${change.path}`));
9147
+ const pendingChanges = (result2.data?.changes ?? []).length;
9037
9148
  const plan = planSkillRoots(ctx.repoRoot);
9038
- details.push(...skillRootsSummary(plan));
9149
+ const rootLines = skillRootsSummary(plan);
9150
+ details.push(...rootLines);
9151
+ findings += rootLines.length;
9039
9152
  const conflict = activationConflict(ctx, result2.findings);
9040
9153
  let conflictResolvable = false;
9041
9154
  if (conflict?.kind === "alias") conflictResolvable = plan.clean;
@@ -9043,6 +9156,7 @@ async function auditProjectSkills(ctx) {
9043
9156
  const predictions = await predictRootCollisions(ctx, []);
9044
9157
  conflictResolvable = predictions.length > 0 && predictions.every(({ decision }) => decision.action !== "block");
9045
9158
  for (const { decision } of predictions) {
9159
+ if (decision.action === "block") findings += 1;
9046
9160
  details.push(decision.action === "block" ? `blocked: ${decision.detail}` : `resolvable root collision: ${decision.detail.replace(/^replaced/, "replace")}`);
9047
9161
  }
9048
9162
  }
@@ -9051,7 +9165,10 @@ async function auditProjectSkills(ctx) {
9051
9165
  const installerOnly = errors.length > 0 && await installerRefusalsResolvable(ctx, errors, handoff);
9052
9166
  if (installerOnly) details.push("resolvable: every refusal is at a skill the bmad-method installer provides; migrate excludes the inherited selection and releases the replaced links");
9053
9167
  if (handoff.skipped) details.push(`installer skill handoff unavailable: ${handoff.skipped}`);
9054
- if (handoff.restored.length) details.push(...handoff.restored.map((name) => `stale exclusion: ${name} was excluded for the bmad-method installer's copy, which is gone; migrate restores the inherited catalog skill`));
9168
+ if (handoff.restored.length) {
9169
+ findings += handoff.restored.length;
9170
+ details.push(...handoff.restored.map((name) => `stale exclusion: ${name} was excluded for the bmad-method installer's copy, which is gone; migrate restores the inherited catalog skill`));
9171
+ }
9055
9172
  if (result2.exit === 2 || result2.exit === 3) details.push(migrationGuidance(ctx));
9056
9173
  const changes = result2.data?.changes ?? [];
9057
9174
  const receiptOnly = result2.exit === 6 && changes.length > 0 && changes.every((change) => change.action === "write-receipt") && !result2.findings.some((finding2) => finding2.severity === "error");
@@ -9063,6 +9180,8 @@ async function auditProjectSkills(ctx) {
9063
9180
  status: result2.exit === 0 ? staleHandoff ? "warn" : "pass" : result2.exit === 4 || receiptOnly ? "warn" : "fail",
9064
9181
  summary: result2.exit === 0 ? "Skillex declaration and activation are in parity" : receiptOnly ? "Skillex activation is in parity; its receipt predates the current catalog commit" : `Skillex inspection returned exit ${result2.exit}`,
9065
9182
  details,
9183
+ findings,
9184
+ pendingChanges,
9066
9185
  fixable: result2.exit === 0 || result2.exit === 6 || conflictResolvable || installerOnly || staleHandoff || result2.findings.some((finding2) => finding2.code === "E_NO_PROJECT_MANIFEST")
9067
9186
  };
9068
9187
  }
@@ -12104,28 +12223,54 @@ function consolidatedBmadConfig(repoRoot) {
12104
12223
  if (!existsSync8(path)) return { status: "absent" };
12105
12224
  let parsed;
12106
12225
  try {
12107
- parsed = YAML4.parse(readText(path));
12226
+ parsed = YAML4.parse(readText(path), { uniqueKeys: false });
12108
12227
  } catch (error) {
12109
- return { status: "unusable", reason: `could not be parsed: ${error instanceof Error ? error.message.split("\n")[0] : String(error)}` };
12228
+ const reason = error instanceof Error ? error.message.split("\n")[0].replace(/:\s*$/, "") : String(error);
12229
+ return { status: "unusable", reason: `could not be parsed: ${reason}` };
12110
12230
  }
12111
12231
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
12112
12232
  return { status: "unusable", reason: "is not a YAML mapping" };
12113
12233
  }
12114
12234
  return { status: "mapping", sections: new Set(Object.keys(parsed)) };
12115
12235
  }
12236
+ var BMAD_SCAFFOLD_REMEDY = "pj migrate bmad.scaffold";
12237
+ function gitIgnoresPath(repoRoot, rel) {
12238
+ return spawnSync5("git", ["check-ignore", "-q", "--", rel], { cwd: repoRoot, encoding: "utf8", timeout: 5e3 }).status === 0;
12239
+ }
12240
+ function bmadManifestGapDetail(repoRoot) {
12241
+ const path = "_bmad/_config/manifest.yaml";
12242
+ const bmad = join12(repoRoot, "_bmad");
12243
+ if (!existsSync8(bmad)) return path;
12244
+ const present = (...segments) => {
12245
+ try {
12246
+ lstatSync10(join12(bmad, ...segments));
12247
+ return true;
12248
+ } catch {
12249
+ return false;
12250
+ }
12251
+ };
12252
+ const record3 = `${path} (the installer's install record)`;
12253
+ const remedy = `Restore it with: ${BMAD_SCAFFOLD_REMEDY}`;
12254
+ if (!present("_config") && !present("core") && existsSync8(join12(bmad, "config.yaml"))) {
12255
+ return `${record3}. BMB setup's \`--also-remove _config\` cleanup is the likely cause: it deletes all of _bmad/_config. ${remedy}`;
12256
+ }
12257
+ if (gitIgnoresPath(repoRoot, path)) return `${record3} is git-ignored in this checkout, so a fresh clone or worktree lacks it. ${remedy}`;
12258
+ return `${record3} is missing. ${remedy}`;
12259
+ }
12116
12260
  function bmadSentinelGaps(repoRoot, modules = selectedBmadModules(repoRoot)) {
12117
12261
  const bmad = join12(repoRoot, "_bmad");
12118
12262
  const gaps = [];
12119
12263
  const absent = (...segments) => !existsSync8(join12(bmad, ...segments));
12264
+ const installed = existsSync8(bmad);
12120
12265
  if (absent("config.toml")) gaps.push({ path: "config.toml", detail: "_bmad/config.toml" });
12121
12266
  for (const script of BMAD_RUNTIME_SCRIPT_SENTINELS) {
12122
- if (absent("scripts", script)) gaps.push({ path: join12("scripts", script), detail: `_bmad/scripts/${script}` });
12267
+ if (!absent("scripts", script)) continue;
12268
+ const rel = `_bmad/scripts/${script}`;
12269
+ const detail = !installed ? rel : gitIgnoresPath(repoRoot, rel) ? `${rel} (git-ignored in this checkout, so a fresh clone or worktree lacks it; restore it with: ${BMAD_SCAFFOLD_REMEDY})` : `${rel} (restore it with: ${BMAD_SCAFFOLD_REMEDY})`;
12270
+ gaps.push({ path: join12("scripts", script), detail });
12123
12271
  }
12124
12272
  if (absent("_config", "manifest.yaml")) {
12125
- gaps.push({
12126
- path: join12("_config", "manifest.yaml"),
12127
- detail: existsSync8(bmad) ? "_bmad/_config/manifest.yaml (the installer's install record). BMB setup's `--also-remove _config` cleanup is the likely cause: it deletes all of _bmad/_config. Restore it with: pj migrate bmad.scaffold" : "_bmad/_config/manifest.yaml"
12128
- });
12273
+ gaps.push({ path: join12("_config", "manifest.yaml"), detail: bmadManifestGapDetail(repoRoot) });
12129
12274
  }
12130
12275
  let consolidated;
12131
12276
  for (const module of modules) {
@@ -13281,11 +13426,14 @@ function createAgentHooksChecks() {
13281
13426
  const unsafe = retired.filter((path) => lstatIfPresent(path)?.isDirectory());
13282
13427
  const details = [...finding2.details, ...wiring, ...registryBlocked, ...retired.map((path) => unsafe.includes(path) ? `Inspect and preserve retired script directory: ${path}` : `${path} is a retired skill writer`)];
13283
13428
  const miseDrift = mise !== null && upsertLinkAgentfilesBlock(mise, ctx) !== mise;
13429
+ const { findings: auditFindings, pendingChanges, ...audited } = finding2;
13430
+ const problems = auditFindings + wiring.length + registryBlocked.length + retired.length;
13431
+ const summary = problems ? `${problems} Skillex finding(s)${pendingChanges ? `, ${pendingChanges} pending change(s)` : ""}` : pendingChanges && audited.status !== "pass" ? `${pendingChanges} pending Skillex change(s)` : audited.summary;
13284
13432
  return {
13285
- ...finding2,
13286
- status: wiring.length || retired.length || registryBlocked.length ? "fail" : finding2.status,
13287
- fixable: unsafe.length ? false : finding2.fixable || wiring.length > 0 || retired.length > 0 || miseDrift,
13288
- summary: details.length ? `${details.length} Skillex finding(s)` : finding2.summary,
13433
+ ...audited,
13434
+ status: wiring.length || retired.length || registryBlocked.length ? "fail" : audited.status,
13435
+ fixable: unsafe.length ? false : audited.fixable || wiring.length > 0 || retired.length > 0 || miseDrift,
13436
+ summary,
13289
13437
  details
13290
13438
  };
13291
13439
  },
@@ -13858,24 +14006,52 @@ function withInstallerSkillHandoff(check) {
13858
14006
  }
13859
14007
  };
13860
14008
  }
13861
- function withScaffoldPostcondition(check) {
14009
+ function withScaffoldScriptRestore(check) {
13862
14010
  if (check.id !== "bmad.scaffold") return check;
13863
14011
  return {
13864
14012
  ...check,
13865
14013
  migrate: async (ctx, finding2) => {
13866
- const result2 = await check.migrate(ctx, finding2);
13867
- if (ctx.dryRun || result2.status !== "applied" && result2.status !== "noop") return result2;
13868
- const after = await check.audit(ctx);
13869
- if (after.status === "pass" || after.status === "skip") return result2;
13870
- return {
13871
- ...result2,
13872
- status: "partial",
13873
- summary: after.summary,
13874
- details: [...result2.details, `still failing after the install: ${after.summary}`, ...after.details]
13875
- };
14014
+ const attempt = restoreScaffoldScripts(ctx, finding2);
14015
+ if (attempt.result) return attempt.result;
14016
+ const viaInstaller = await check.migrate(ctx, finding2);
14017
+ return attempt.note ? { ...viaInstaller, details: [attempt.note, ...viaInstaller.details] } : viaInstaller;
13876
14018
  }
13877
14019
  };
13878
14020
  }
14021
+ function restoreScaffoldScripts(ctx, finding2) {
14022
+ const manifest = manifestBmadModules(ctx.repoRoot);
14023
+ if (manifest.status === "invalid") return {};
14024
+ const modules = manifest.status === "valid" ? manifest.modules : configuredBmadModuleSelection(ctx.repoRoot)?.modules ?? [...DEFAULT_BMAD_MODULES];
14025
+ const issues = bmadScaffoldIssues(ctx.repoRoot, modules);
14026
+ const scriptGap = (path) => BMAD_RUNTIME_SCRIPT_SENTINELS.some((script) => path === join12("scripts", script));
14027
+ if (!issues.gaps.length || issues.nameIssues.details.length || !issues.gaps.every((gap) => scriptGap(gap.path))) return {};
14028
+ const state = bmadSetupScriptsState(ctx.repoRoot);
14029
+ if (state.kind === "present") return {};
14030
+ if (state.kind !== "missing") {
14031
+ return { note: `targeted _bmad/scripts restore not possible, so this takes the installer path: ${state.kind === "decline" ? state.summary : state.error}` };
14032
+ }
14033
+ const source = `bmad-method@${BMAD_INSTALLER_VERSION} (${state.pkg.root})`;
14034
+ if (ctx.dryRun) {
14035
+ return { result: {
14036
+ id: finding2.id,
14037
+ title: finding2.title,
14038
+ status: "applied",
14039
+ summary: "Would restore the missing _bmad/scripts from the verified pinned bmad-method package (no reinstall)",
14040
+ changedFiles: state.files,
14041
+ details: [source]
14042
+ } };
14043
+ }
14044
+ const applied = applyBmadSetupScripts(ctx.repoRoot, state.pkg, BMAD_INSTALLER_VERSION);
14045
+ if (applied.error) return { note: `targeted _bmad/scripts restore failed, so this takes the installer path: ${applied.error}` };
14046
+ return { result: {
14047
+ id: finding2.id,
14048
+ title: finding2.title,
14049
+ status: applied.files.length ? "applied" : "noop",
14050
+ summary: applied.files.length ? "Restored the missing _bmad/scripts from the verified pinned bmad-method package (no reinstall)" : "No changes required",
14051
+ changedFiles: applied.files,
14052
+ details: [source]
14053
+ } };
14054
+ }
13879
14055
  function createBmadChecks() {
13880
14056
  return [
13881
14057
  {
@@ -14152,6 +14328,22 @@ function createBmadChecks() {
14152
14328
  };
14153
14329
  }
14154
14330
  const behindStable = !pinned && stable ? compareBmadVersions(installed, stable) < 0 : false;
14331
+ if (behindStable && compareBmadVersions(installed, BMAD_INSTALLER_VERSION) === 0) {
14332
+ return {
14333
+ id: "bmad.version",
14334
+ title: "BMAD version currency",
14335
+ status: "pass",
14336
+ summary: `BMAD ${installed} matches the verified installer pin; stable ${stable} is newer (advisory)${staleNote}`,
14337
+ details: [
14338
+ `installed: ${installed}`,
14339
+ `verified installer pin: ${BMAD_INSTALLER_VERSION}`,
14340
+ `stable latest: ${stable}`,
14341
+ "pjangler advances its pin only through the verified-installer path (`mise run bmad:verify-surface <version>`)",
14342
+ "run `pj migrate bmad.version` to take the newer release anyway"
14343
+ ],
14344
+ fixable: false
14345
+ };
14346
+ }
14155
14347
  if (!pinned && !behindStable) {
14156
14348
  return {
14157
14349
  id: "bmad.version",
@@ -14387,7 +14579,7 @@ function createBmadChecks() {
14387
14579
  };
14388
14580
  }
14389
14581
  }
14390
- ].map(withInstallerSkillHandoff).map(withScaffoldPostcondition);
14582
+ ].map(withInstallerSkillHandoff).map(withScaffoldScriptRestore);
14391
14583
  }
14392
14584
  function createProjectMomoChecks() {
14393
14585
  return [
@@ -14598,16 +14790,17 @@ function formatAuditReport(report) {
14598
14790
  }
14599
14791
  function formatMigrationReport(report) {
14600
14792
  const idWidth = report.results.reduce((width, result2) => Math.max(width, result2.id.length), 0);
14601
- const blocked = report.results.filter((result2) => result2.status === "blocked").length;
14602
- const partial = report.results.filter((result2) => result2.status === "partial").length;
14603
- const overall = report.ok ? `${green(glyph.pass)} ${bold(report.dryRun ? "Migration preview complete" : "Migration complete")}` : blocked ? `${red(glyph.fail)} ${bold("Migration finished with blockers")}` : `${yellow(glyph.warn)} ${bold(`Migration incomplete ${glyph.dot} ${partial} rule${partial === 1 ? "" : "s"} still failing`)}`;
14793
+ const advisory = report.results.filter((result2) => result2.advisory).length;
14794
+ const blocked = report.results.filter((result2) => result2.status === "blocked" && !result2.advisory).length;
14795
+ const partial = report.results.filter((result2) => result2.status === "partial" && !result2.advisory).length;
14796
+ const overall = report.ok ? `${green(glyph.pass)} ${bold(report.dryRun ? "Migration preview complete" : "Migration complete")}${advisory ? ` ${dim(glyph.dot)} ${yellow(`${advisory} warning${advisory === 1 ? "" : "s"}`)}` : ""}` : blocked ? `${red(glyph.fail)} ${bold("Migration finished with blockers")}` : `${yellow(glyph.warn)} ${bold(`Migration incomplete ${glyph.dot} ${partial} rule${partial === 1 ? "" : "s"} still failing`)}`;
14604
14797
  const lines = [""];
14605
14798
  lines.push(` ${overall}${report.dryRun ? ` ${dim(glyph.dot)} ${yellow("dry run")}` : ""}`);
14606
14799
  lines.push(` ${dim(report.repo)}`);
14607
14800
  if (report.selectedRules.length) lines.push(` ${dim(`rules: ${report.selectedRules.join(", ")}`)}`);
14608
14801
  lines.push("");
14609
14802
  for (const result2 of report.results) {
14610
- const style = statusStyle(result2.status);
14803
+ const style = result2.advisory ? { ...statusStyle("warn"), label: `warning, ${result2.status}` } : statusStyle(result2.status);
14611
14804
  lines.push(` ${style.color(style.glyph)} ${style.color(result2.id.padEnd(idWidth))} ${result2.summary} ${dim(`[${style.label}]`)}`);
14612
14805
  for (const detail of result2.details) lines.push(` ${dim(glyph.arrow)} ${dim(detail)}`);
14613
14806
  for (const file of result2.changedFiles) lines.push(` ${green(glyph.add)} ${file}`);
@@ -14621,6 +14814,9 @@ function formatMigrationReport(report) {
14621
14814
  if (unresolved) {
14622
14815
  lines.push("");
14623
14816
  lines.push(` ${dim(`Run \`pjangler audit\` for the full detail on the ${unresolved} rule${unresolved === 1 ? "" : "s"} still failing.`)}`);
14817
+ } else if (advisory) {
14818
+ lines.push("");
14819
+ lines.push(` ${dim(`${advisory} rule${advisory === 1 ? "" : "s"} could not finish but only warn${advisory === 1 ? "s" : ""}; they do not block. \`pjangler audit\` has the detail.`)}`);
14624
14820
  }
14625
14821
  lines.push("");
14626
14822
  return lines.join("\n");
@@ -15090,6 +15286,7 @@ function ticketScope(ticket) {
15090
15286
  }
15091
15287
 
15092
15288
  // src/recipes/ProjectRecipe.ts
15289
+ init_manifestIndex();
15093
15290
  init_tree_diff();
15094
15291
 
15095
15292
  // src/recipes/NotebookRecipe.ts
@@ -17156,6 +17353,17 @@ function unsafeToRemove(targetDir) {
17156
17353
  }
17157
17354
  return null;
17158
17355
  }
17356
+ function manifestRefusal(manifestPath) {
17357
+ if (!existsSync18(manifestPath)) return void 0;
17358
+ try {
17359
+ const parsed = JSON.parse(readFileSync23(manifestPath, "utf8"));
17360
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return `Expected a JSON object: ${manifestPath}`;
17361
+ validateManifestShape(parsed, manifestPath);
17362
+ return void 0;
17363
+ } catch (error) {
17364
+ return error instanceof SyntaxError ? `Malformed JSON: ${manifestPath}` : error instanceof Error ? error.message : String(error);
17365
+ }
17366
+ }
17159
17367
  function publicAudit(report) {
17160
17368
  return {
17161
17369
  ...report,
@@ -17281,6 +17489,8 @@ var ProjectRecipe = class extends Recipe {
17281
17489
  });
17282
17490
  };
17283
17491
  let migrationReport;
17492
+ let unfinishedMigrations = [];
17493
+ const warnings = [];
17284
17494
  let audit;
17285
17495
  let notebookPlan;
17286
17496
  let notebookRecipeForCommit;
@@ -17380,20 +17590,37 @@ var ProjectRecipe = class extends Recipe {
17380
17590
  normalized.selectedRuleIds
17381
17591
  ));
17382
17592
  changedFiles.push(...migrationReport.changedFiles);
17383
- phases.push(...migrationReport.results.map((result2) => ({
17593
+ const migrationPhases = migrationReport.results.map((result2) => ({
17384
17594
  id: result2.id,
17385
17595
  status: result2.status === "applied" ? "changed" : result2.status === "noop" ? "unchanged" : result2.status === "skipped" ? "skipped" : "failed",
17386
17596
  // A `partial` failed, but its writes really happened, so they stay
17387
17597
  // accounted for rather than vanishing from the transaction record.
17388
17598
  changedFiles: result2.status === "applied" || result2.status === "partial" ? result2.changedFiles : [],
17389
17599
  message: result2.summary
17390
- })));
17391
- errors.push(...migrationReport.results.filter((result2) => result2.status === "blocked" || result2.status === "partial").map((result2) => describeFinding(result2)));
17600
+ }));
17601
+ phases.push(...migrationPhases);
17602
+ unfinishedMigrations = migrationReport.results.map((result2, index) => ({ result: result2, phase: migrationPhases[index] })).filter(({ result: result2 }) => result2.status === "blocked" || result2.status === "partial");
17392
17603
  }
17393
17604
  const eligibilityAudit = errors.length === 0 ? publicAudit(await this.registry.auditRecipes({ ...transactionContext, dryRun: true })) : void 0;
17394
17605
  audit = eligibilityAudit;
17606
+ const gating = new Set((eligibilityAudit?.rules ?? []).filter((finding2) => gatesProject(finding2)).map((finding2) => finding2.id));
17607
+ const reportedByMigration = /* @__PURE__ */ new Set();
17608
+ for (const { result: result2, phase } of unfinishedMigrations) {
17609
+ if (!eligibilityAudit || gating.has(result2.id)) {
17610
+ errors.push(describeFinding(result2));
17611
+ reportedByMigration.add(result2.id);
17612
+ } else {
17613
+ result2.advisory = true;
17614
+ phase.status = "skipped";
17615
+ phase.message = `advisory (does not block init): ${result2.summary}`;
17616
+ warnings.push(`${result2.id}: ${result2.summary}`);
17617
+ }
17618
+ }
17619
+ if (migrationReport && unfinishedMigrations.length && !unfinishedMigrations.some(({ result: result2 }) => !result2.advisory)) {
17620
+ migrationReport.ok = true;
17621
+ }
17395
17622
  if (eligibilityAudit && !eligibilityAudit.ok) {
17396
- errors.push(...eligibilityAudit.rules.filter((finding2) => gatesProject(finding2)).map((finding2) => describeFinding(finding2)));
17623
+ errors.push(...eligibilityAudit.rules.filter((finding2) => gatesProject(finding2) && !reportedByMigration.has(finding2.id)).map((finding2) => describeFinding(finding2)));
17397
17624
  }
17398
17625
  phases.push({
17399
17626
  id: "project.audit:eligibility",
@@ -17437,6 +17664,13 @@ var ProjectRecipe = class extends Recipe {
17437
17664
  }
17438
17665
  }
17439
17666
  }
17667
+ if (errors.length === 0) {
17668
+ const refusal = manifestRefusal(join25(targetDir, ".project.json"));
17669
+ if (refusal) {
17670
+ errors.push(`manifest refused before external effects: ${refusal}`);
17671
+ phases.push({ id: "project.manifest:validate", status: "failed", changedFiles: [], message: refusal });
17672
+ }
17673
+ }
17440
17674
  registryFinalizerEligible = errors.length === 0;
17441
17675
  if (errors.length === 0 && externalPlanActions.length) {
17442
17676
  const dispatching = externalPlanActions.some((action) => action.kind === "ticket-provider.create-or-link" && action.enabled);
@@ -17608,6 +17842,7 @@ var ProjectRecipe = class extends Recipe {
17608
17842
  selectedOperations: normalized.selectedOperations ?? [],
17609
17843
  selectedParityRules: normalized.selectedRuleIds ?? [],
17610
17844
  migrationReport,
17845
+ warnings,
17611
17846
  ...bmadCompanion ? { bmadCompanion } : {}
17612
17847
  };
17613
17848
  }
@@ -430,9 +430,44 @@ var init_notebookSettingsValidation = __esm({
430
430
  });
431
431
 
432
432
  // src/project/executionBinding.ts
433
+ function executionReadiness(manifest) {
434
+ const execution = manifest.execution;
435
+ if (!execution || execution.mode === "legacy") return [];
436
+ const errors = [];
437
+ if (!["shadow", "managed"].includes(execution.mode)) errors.push("execution.mode must be legacy, shadow or managed");
438
+ if (!manifest.project_id) errors.push("canonical project_id missing");
439
+ if (manifest.ticket_provider?.type !== "plane" || !manifest.ticket_provider.workspace || !manifest.ticket_provider.board_id) errors.push("exact Plane binding missing");
440
+ if (execution.policy_version !== 2 || !execution.skill_version) errors.push("execution policy or skill pin missing");
441
+ for (const lane of EXECUTION_LANES) if (!execution.states?.[lane]) errors.push(`lane binding missing: ${lane}`);
442
+ for (const pin of ["pilot_bundle_sha256", "momo_bundle_sha256"]) if (!/^[a-f0-9]{64}$/.test(execution[pin] ?? "")) errors.push(`${pin} missing`);
443
+ if (!execution.working_label) errors.push("working label binding missing");
444
+ if (!execution.actors?.[execution.pm_actor] || execution.actors[execution.pm_actor].role !== "pm") errors.push("PM actor enrollment missing");
445
+ if (!execution.actors?.[execution.controller_actor] || execution.actors[execution.controller_actor].role !== "operator") errors.push("controller repair actor enrollment missing");
446
+ const ids = /* @__PURE__ */ new Set();
447
+ for (const [name, actor] of Object.entries(execution.actors ?? {})) {
448
+ if (!actor.native_user_id || !actor.key_ref?.startsWith("op://") || !actor.runtime_id) errors.push(`actor ${name}: native identity, op reference and runtime required`);
449
+ if (ids.has(actor.native_user_id)) errors.push(`actor ${name}: duplicate native identity`);
450
+ ids.add(actor.native_user_id);
451
+ if (!["pm", "operator", "reviewer", "interactive"].includes(actor.role)) errors.push(`actor ${name}: invalid role`);
452
+ if (name === execution.pm_actor && (!Array.isArray(actor.runtime?.planner_argv) || !actor.runtime.planner_argv.length || actor.runtime.planner_argv.some((x) => typeof x !== "string" || !x || x.includes("\0")))) errors.push("PM planner argv missing");
453
+ if (actor.runtime?.adapter !== "systemd" || !/^[a-zA-Z0-9_-]+$/.test(actor.runtime?.unit_prefix ?? "")) errors.push(`actor ${name}: supervised runtime missing`);
454
+ }
455
+ if (execution.legacy_writers_fenced !== true) errors.push("legacy writers not fenced");
456
+ return errors;
457
+ }
458
+ function validateExecutionBinding(manifest) {
459
+ if (!manifest.execution) return;
460
+ if (typeof manifest.execution !== "object" || Array.isArray(manifest.execution)) throw new Error("execution must be an object");
461
+ if (manifest.execution.mode === "managed") {
462
+ const errors = executionReadiness(manifest);
463
+ if (errors.length) throw new Error(`Managed execution not ready: ${errors.join("; ")}`);
464
+ } else if (!["legacy", "shadow"].includes(manifest.execution.mode)) throw new Error("invalid execution mode");
465
+ }
466
+ var EXECUTION_LANES;
433
467
  var init_executionBinding = __esm({
434
468
  "src/project/executionBinding.ts"() {
435
469
  "use strict";
470
+ EXECUTION_LANES = ["Backlog", "Needs Re-evaluation", "Todo", "In Progress", "E2E Testing & QA", "Ready for Documentation", "Done", "Needs Attention", "Cancelled"];
436
471
  }
437
472
  });
438
473
 
@@ -445,6 +480,46 @@ function normalizeProjectId2(value) {
445
480
  throw new RegistryError(error instanceof Error ? error.message : String(error));
446
481
  }
447
482
  }
483
+ function object(value) {
484
+ return !!value && typeof value === "object" && !Array.isArray(value);
485
+ }
486
+ function validateManifestShape(manifest, path) {
487
+ validateExecutionBinding(manifest);
488
+ const fail = (field2, shape) => {
489
+ throw new RegistryError(`Manifest ${path}: ${field2} must be ${shape}`);
490
+ };
491
+ const strings = (value, names, prefix = "") => {
492
+ for (const name of names) if (value[name] !== void 0 && typeof value[name] !== "string") fail(`${prefix}${name}`, "a string");
493
+ };
494
+ strings(manifest, ["project_name", "name", "project_description", "description", "status", "repo_path", "created_at", "updated_at"]);
495
+ for (const field2 of ["ticket_provider", "agents", "template", "notebook", "automation"]) if (manifest[field2] !== void 0 && !object(manifest[field2])) fail(field2, "an object");
496
+ if (manifest.source_artifacts !== void 0 && !Array.isArray(manifest.source_artifacts)) fail("source_artifacts", "an array");
497
+ for (const [index, artifact] of (manifest.source_artifacts ?? []).entries()) {
498
+ if (!object(artifact)) fail(`source_artifacts.${index}`, "an object");
499
+ strings(artifact, ["kind", "path", "package_name"], `source_artifacts.${index}.`);
500
+ }
501
+ 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.");
502
+ if (manifest.ticket_provider?.scope_default !== void 0 && typeof manifest.ticket_provider.scope_default !== "boolean") fail("ticket_provider.scope_default", "a boolean");
503
+ for (const [name, agent] of Object.entries(manifest.agents ?? {})) {
504
+ if (!object(agent)) fail(`agents.${name}`, "an object");
505
+ strings(agent, ["role", "role_dir", "provisioning_state"], `agents.${name}.`);
506
+ }
507
+ if (manifest.template?.commonproject !== void 0) {
508
+ if (!object(manifest.template.commonproject)) fail("template.commonproject", "an object");
509
+ const template = manifest.template.commonproject;
510
+ if (template.enabled !== void 0 && typeof template.enabled !== "boolean") fail("template.commonproject.enabled", "boolean");
511
+ strings(template, ["primary_language", "project_type"], "template.commonproject.");
512
+ }
513
+ for (const field2 of ["binding", "policy"]) if (manifest.notebook?.[field2] !== void 0 && !object(manifest.notebook[field2])) fail(`notebook.${field2}`, "an object");
514
+ if (manifest.notebook?.binding) strings(manifest.notebook.binding, ["provider", "state", "notebook_name", "blocked_reason", "notebook_id", "overview_note_id", "created_at", "updated_at"], "notebook.binding.");
515
+ if (manifest.notebook?.policy) {
516
+ try {
517
+ validateGlobalNotebookConfig({ defaults: manifest.notebook.policy });
518
+ } catch (error) {
519
+ throw new RegistryError(`Manifest ${path}: invalid notebook policy: ${String(error)}`);
520
+ }
521
+ }
522
+ }
448
523
  function recordFromManifest(manifest, path) {
449
524
  const id = normalizeProjectId2(manifest.project_id ?? manifest.project_slug);
450
525
  const { project_slug: _old, project_name: _name, project_description: _description, notebook, ...rest } = manifest;
@@ -1512,12 +1587,40 @@ function resolveSourceSkillPath(sourceSkill, env2 = process.env) {
1512
1587
  const hint = `${searched} Add project-specific roots with ${PROJECT_SOURCE_SKILL_ROOTS_ENV}.`;
1513
1588
  throw new Error(`Source skill not found: ${sourceSkill}.${hint}`);
1514
1589
  }
1590
+ function looksLikePath(value) {
1591
+ return /^(?:\/|~\/|\.{1,2}\/)/.test(value);
1592
+ }
1593
+ function normalizeLegacyManifest(manifest) {
1594
+ const artifacts = manifest.source_artifacts;
1595
+ if (!Array.isArray(artifacts)) return { manifest, notes: [] };
1596
+ const notes = [];
1597
+ const normalized = artifacts.map((artifact, index) => {
1598
+ if (typeof artifact !== "string" || !looksLikePath(artifact)) return artifact;
1599
+ notes.push(`.project.json source_artifacts.${index}: bare path normalized to {"kind": "${LEGACY_SOURCE_ARTIFACT_KIND}", "path": "${artifact}"}`);
1600
+ return { kind: LEGACY_SOURCE_ARTIFACT_KIND, path: artifact };
1601
+ });
1602
+ return notes.length ? { manifest: { ...manifest, source_artifacts: normalized }, notes } : { manifest, notes };
1603
+ }
1515
1604
  function planProjectInit(input) {
1516
1605
  if (!input.name.trim()) throw new Error("Project name is required");
1517
1606
  const targetDir = resolve10(input.targetDir ?? defaultProjectTargetDir(input.name, input.cwd));
1518
1607
  const localManifestPath = join17(targetDir, ".project.json");
1519
1608
  const localManifestContent = existsSync11(localManifestPath) ? readFileSync15(localManifestPath, "utf8") : void 0;
1520
- const localManifest = localManifestContent === void 0 ? void 0 : JSON.parse(localManifestContent);
1609
+ let localManifest;
1610
+ const manifestNotes = [];
1611
+ if (localManifestContent !== void 0) {
1612
+ let parsed;
1613
+ try {
1614
+ parsed = JSON.parse(localManifestContent);
1615
+ } catch {
1616
+ throw new Error(`Malformed JSON: ${localManifestPath}`);
1617
+ }
1618
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error(`Expected a JSON object: ${localManifestPath}`);
1619
+ const normalized = normalizeLegacyManifest(parsed);
1620
+ localManifest = normalized.manifest;
1621
+ manifestNotes.push(...normalized.notes);
1622
+ validateManifestShape(localManifest, localManifestPath);
1623
+ }
1521
1624
  const localId = localManifest?.project_id ?? localManifest?.project_slug;
1522
1625
  const slug = normalizeProjectId(input.projectSlug ?? localId ?? slugifyProjectName(input.name));
1523
1626
  if (localId && normalizeProjectId(localId) !== slug) throw new Error(`Project ID ${slug} conflicts with authoritative manifest ${localId}`);
@@ -1612,6 +1715,7 @@ function planProjectInit(input) {
1612
1715
  validateNoDuplicateProject(registry, project, overwrite);
1613
1716
  const pjanglerRoot = resolve10(input.pjanglerRoot ?? resolvePjanglerRoot());
1614
1717
  const manifest = projectManifestFromRegistryProject(project);
1718
+ validateManifestShape(manifest, localManifestPath);
1615
1719
  const apply = input.apply ?? false;
1616
1720
  const live = input.live ?? false;
1617
1721
  const provisionTicketBoard = input.provisionTicketBoard ?? true;
@@ -1667,7 +1771,7 @@ function planProjectInit(input) {
1667
1771
  project,
1668
1772
  manifest,
1669
1773
  actions,
1670
- ...input.boardUrl !== void 0 ? { warnings: [BOARD_URL_DEPRECATION_WARNING] } : {}
1774
+ ...input.boardUrl !== void 0 || manifestNotes.length ? { warnings: [...input.boardUrl !== void 0 ? [BOARD_URL_DEPRECATION_WARNING] : [], ...manifestNotes] } : {}
1671
1775
  };
1672
1776
  }
1673
1777
  function linkTicketProviderBoard(plan, action, boardId, identifier, now = /* @__PURE__ */ new Date()) {
@@ -2016,7 +2120,7 @@ function expandHome(path) {
2016
2120
  function isRecord2(value) {
2017
2121
  return typeof value === "object" && value !== null && !Array.isArray(value);
2018
2122
  }
2019
- var PROJECT_SOURCE_SKILL_ROOTS_ENV, TICKET_PROVIDER_ADAPTERS_ENV, PROJECT_REGISTRY_SCHEMA_VERSION, DEFAULT_NEW_PROJECT_STATUS, PROJECT_TYPES, PROJECT_TYPE_LANGUAGES, PROJECT_TYPE_SENTINELS, 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;
2123
+ var PROJECT_SOURCE_SKILL_ROOTS_ENV, TICKET_PROVIDER_ADAPTERS_ENV, PROJECT_REGISTRY_SCHEMA_VERSION, DEFAULT_NEW_PROJECT_STATUS, PROJECT_TYPES, PROJECT_TYPE_LANGUAGES, PROJECT_TYPE_SENTINELS, 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, LEGACY_SOURCE_ARTIFACT_KIND;
2020
2124
  var init_project = __esm({
2021
2125
  "src/project/index.ts"() {
2022
2126
  "use strict";
@@ -2072,6 +2176,7 @@ var init_project = __esm({
2072
2176
  GLOBAL_NOTEBOOK_LIMITS_OWNED_KEYS = Object.keys(DEFAULT_NOTEBOOK_LIMITS);
2073
2177
  GLOBAL_NOTEBOOK_SUMMARIZER_OWNED_KEYS = ["executable", "args"];
2074
2178
  SAFE_PATH_SEGMENT = /^[A-Za-z0-9](?:[A-Za-z0-9._-]*[A-Za-z0-9])?$/;
2179
+ LEGACY_SOURCE_ARTIFACT_KIND = "reference";
2075
2180
  }
2076
2181
  });
2077
2182
 
@@ -5585,6 +5690,10 @@ function backupToSnapshot(backup) {
5585
5690
  }
5586
5691
  return snapshot;
5587
5692
  }
5693
+ function removeEntry(full, stat) {
5694
+ if (stat.isDirectory() && !stat.isSymbolicLink()) rmSync(full, { recursive: true, force: true });
5695
+ else unlinkSync(full);
5696
+ }
5588
5697
  function restoreSingleEntry(root, rel, entry) {
5589
5698
  const full = rel === "." ? root : join3(root, rel);
5590
5699
  let currentStat;
@@ -5595,7 +5704,7 @@ function restoreSingleEntry(root, rel, entry) {
5595
5704
  }
5596
5705
  if (entry.kind === "dir") {
5597
5706
  if (currentStat && (!currentStat.isDirectory() || currentStat.isSymbolicLink())) {
5598
- rmSync(full, { recursive: true, force: true });
5707
+ removeEntry(full, currentStat);
5599
5708
  currentStat = void 0;
5600
5709
  }
5601
5710
  if (!currentStat) mkdirSync2(full, { recursive: true });
@@ -5606,7 +5715,7 @@ function restoreSingleEntry(root, rel, entry) {
5606
5715
  if (entry.kind === "link") {
5607
5716
  if (currentStat) {
5608
5717
  if (currentStat.isSymbolicLink() && readlinkSync2(full) === entry.target) return;
5609
- rmSync(full, { recursive: true, force: true });
5718
+ removeEntry(full, currentStat);
5610
5719
  }
5611
5720
  symlinkSync(entry.target, full);
5612
5721
  return;
@@ -5615,7 +5724,7 @@ function restoreSingleEntry(root, rel, entry) {
5615
5724
  if ((currentStat.mode & 4095) === entry.mode && readFileSync2(full).equals(entry.bytes)) return;
5616
5725
  unlinkSync(full);
5617
5726
  } else if (currentStat) {
5618
- rmSync(full, { recursive: true, force: true });
5727
+ removeEntry(full, currentStat);
5619
5728
  }
5620
5729
  writeFileSync2(full, entry.bytes);
5621
5730
  chmodSync(full, entry.mode);
@@ -6224,10 +6333,10 @@ function openDisplacedStash(projectRoot) {
6224
6333
  index.push({ rel, kind: "link", target });
6225
6334
  count++;
6226
6335
  } else if (bytes) {
6227
- const object = createHash4("sha256").update(bytes).digest("hex");
6228
- const objectPath = join5(open(), "objects", object);
6336
+ const object2 = createHash4("sha256").update(bytes).digest("hex");
6337
+ const objectPath = join5(open(), "objects", object2);
6229
6338
  if (!existsSync3(objectPath)) writeFileSync3(objectPath, bytes);
6230
- index.push({ rel, kind: "file", mode: stat.mode & 4095, object });
6339
+ index.push({ rel, kind: "file", mode: stat.mode & 4095, object: object2 });
6231
6340
  count++;
6232
6341
  } else if (stat.isDirectory()) {
6233
6342
  open();
@@ -8196,9 +8305,13 @@ async function predictRootCollisions(ctx, incoming) {
8196
8305
  async function auditProjectSkills(ctx) {
8197
8306
  const result2 = await inspectStatus(skillCoreOptions(ctx));
8198
8307
  const details = skillDiagnostics(result2.findings);
8308
+ let findings = result2.findings.filter((finding2) => finding2.severity !== "info").length;
8199
8309
  details.push(...(result2.data?.changes ?? []).map((change) => `${change.action}: ${change.path}`));
8310
+ const pendingChanges = (result2.data?.changes ?? []).length;
8200
8311
  const plan = planSkillRoots(ctx.repoRoot);
8201
- details.push(...skillRootsSummary(plan));
8312
+ const rootLines = skillRootsSummary(plan);
8313
+ details.push(...rootLines);
8314
+ findings += rootLines.length;
8202
8315
  const conflict = activationConflict(ctx, result2.findings);
8203
8316
  let conflictResolvable = false;
8204
8317
  if (conflict?.kind === "alias") conflictResolvable = plan.clean;
@@ -8206,6 +8319,7 @@ async function auditProjectSkills(ctx) {
8206
8319
  const predictions = await predictRootCollisions(ctx, []);
8207
8320
  conflictResolvable = predictions.length > 0 && predictions.every(({ decision }) => decision.action !== "block");
8208
8321
  for (const { decision } of predictions) {
8322
+ if (decision.action === "block") findings += 1;
8209
8323
  details.push(decision.action === "block" ? `blocked: ${decision.detail}` : `resolvable root collision: ${decision.detail.replace(/^replaced/, "replace")}`);
8210
8324
  }
8211
8325
  }
@@ -8214,7 +8328,10 @@ async function auditProjectSkills(ctx) {
8214
8328
  const installerOnly = errors.length > 0 && await installerRefusalsResolvable(ctx, errors, handoff);
8215
8329
  if (installerOnly) details.push("resolvable: every refusal is at a skill the bmad-method installer provides; migrate excludes the inherited selection and releases the replaced links");
8216
8330
  if (handoff.skipped) details.push(`installer skill handoff unavailable: ${handoff.skipped}`);
8217
- if (handoff.restored.length) details.push(...handoff.restored.map((name) => `stale exclusion: ${name} was excluded for the bmad-method installer's copy, which is gone; migrate restores the inherited catalog skill`));
8331
+ if (handoff.restored.length) {
8332
+ findings += handoff.restored.length;
8333
+ details.push(...handoff.restored.map((name) => `stale exclusion: ${name} was excluded for the bmad-method installer's copy, which is gone; migrate restores the inherited catalog skill`));
8334
+ }
8218
8335
  if (result2.exit === 2 || result2.exit === 3) details.push(migrationGuidance(ctx));
8219
8336
  const changes = result2.data?.changes ?? [];
8220
8337
  const receiptOnly = result2.exit === 6 && changes.length > 0 && changes.every((change) => change.action === "write-receipt") && !result2.findings.some((finding2) => finding2.severity === "error");
@@ -8226,6 +8343,8 @@ async function auditProjectSkills(ctx) {
8226
8343
  status: result2.exit === 0 ? staleHandoff ? "warn" : "pass" : result2.exit === 4 || receiptOnly ? "warn" : "fail",
8227
8344
  summary: result2.exit === 0 ? "Skillex declaration and activation are in parity" : receiptOnly ? "Skillex activation is in parity; its receipt predates the current catalog commit" : `Skillex inspection returned exit ${result2.exit}`,
8228
8345
  details,
8346
+ findings,
8347
+ pendingChanges,
8229
8348
  fixable: result2.exit === 0 || result2.exit === 6 || conflictResolvable || installerOnly || staleHandoff || result2.findings.some((finding2) => finding2.code === "E_NO_PROJECT_MANIFEST")
8230
8349
  };
8231
8350
  }
@@ -11267,28 +11386,54 @@ function consolidatedBmadConfig(repoRoot) {
11267
11386
  if (!existsSync8(path)) return { status: "absent" };
11268
11387
  let parsed;
11269
11388
  try {
11270
- parsed = YAML4.parse(readText(path));
11389
+ parsed = YAML4.parse(readText(path), { uniqueKeys: false });
11271
11390
  } catch (error) {
11272
- return { status: "unusable", reason: `could not be parsed: ${error instanceof Error ? error.message.split("\n")[0] : String(error)}` };
11391
+ const reason = error instanceof Error ? error.message.split("\n")[0].replace(/:\s*$/, "") : String(error);
11392
+ return { status: "unusable", reason: `could not be parsed: ${reason}` };
11273
11393
  }
11274
11394
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
11275
11395
  return { status: "unusable", reason: "is not a YAML mapping" };
11276
11396
  }
11277
11397
  return { status: "mapping", sections: new Set(Object.keys(parsed)) };
11278
11398
  }
11399
+ var BMAD_SCAFFOLD_REMEDY = "pj migrate bmad.scaffold";
11400
+ function gitIgnoresPath(repoRoot, rel) {
11401
+ return spawnSync5("git", ["check-ignore", "-q", "--", rel], { cwd: repoRoot, encoding: "utf8", timeout: 5e3 }).status === 0;
11402
+ }
11403
+ function bmadManifestGapDetail(repoRoot) {
11404
+ const path = "_bmad/_config/manifest.yaml";
11405
+ const bmad = join12(repoRoot, "_bmad");
11406
+ if (!existsSync8(bmad)) return path;
11407
+ const present = (...segments) => {
11408
+ try {
11409
+ lstatSync10(join12(bmad, ...segments));
11410
+ return true;
11411
+ } catch {
11412
+ return false;
11413
+ }
11414
+ };
11415
+ const record3 = `${path} (the installer's install record)`;
11416
+ const remedy = `Restore it with: ${BMAD_SCAFFOLD_REMEDY}`;
11417
+ if (!present("_config") && !present("core") && existsSync8(join12(bmad, "config.yaml"))) {
11418
+ return `${record3}. BMB setup's \`--also-remove _config\` cleanup is the likely cause: it deletes all of _bmad/_config. ${remedy}`;
11419
+ }
11420
+ if (gitIgnoresPath(repoRoot, path)) return `${record3} is git-ignored in this checkout, so a fresh clone or worktree lacks it. ${remedy}`;
11421
+ return `${record3} is missing. ${remedy}`;
11422
+ }
11279
11423
  function bmadSentinelGaps(repoRoot, modules = selectedBmadModules(repoRoot)) {
11280
11424
  const bmad = join12(repoRoot, "_bmad");
11281
11425
  const gaps = [];
11282
11426
  const absent = (...segments) => !existsSync8(join12(bmad, ...segments));
11427
+ const installed = existsSync8(bmad);
11283
11428
  if (absent("config.toml")) gaps.push({ path: "config.toml", detail: "_bmad/config.toml" });
11284
11429
  for (const script of BMAD_RUNTIME_SCRIPT_SENTINELS) {
11285
- if (absent("scripts", script)) gaps.push({ path: join12("scripts", script), detail: `_bmad/scripts/${script}` });
11430
+ if (!absent("scripts", script)) continue;
11431
+ const rel = `_bmad/scripts/${script}`;
11432
+ const detail = !installed ? rel : gitIgnoresPath(repoRoot, rel) ? `${rel} (git-ignored in this checkout, so a fresh clone or worktree lacks it; restore it with: ${BMAD_SCAFFOLD_REMEDY})` : `${rel} (restore it with: ${BMAD_SCAFFOLD_REMEDY})`;
11433
+ gaps.push({ path: join12("scripts", script), detail });
11286
11434
  }
11287
11435
  if (absent("_config", "manifest.yaml")) {
11288
- gaps.push({
11289
- path: join12("_config", "manifest.yaml"),
11290
- detail: existsSync8(bmad) ? "_bmad/_config/manifest.yaml (the installer's install record). BMB setup's `--also-remove _config` cleanup is the likely cause: it deletes all of _bmad/_config. Restore it with: pj migrate bmad.scaffold" : "_bmad/_config/manifest.yaml"
11291
- });
11436
+ gaps.push({ path: join12("_config", "manifest.yaml"), detail: bmadManifestGapDetail(repoRoot) });
11292
11437
  }
11293
11438
  let consolidated;
11294
11439
  for (const module of modules) {
@@ -12092,11 +12237,14 @@ function createAgentHooksChecks() {
12092
12237
  const unsafe = retired.filter((path) => lstatIfPresent(path)?.isDirectory());
12093
12238
  const details = [...finding2.details, ...wiring, ...registryBlocked, ...retired.map((path) => unsafe.includes(path) ? `Inspect and preserve retired script directory: ${path}` : `${path} is a retired skill writer`)];
12094
12239
  const miseDrift = mise !== null && upsertLinkAgentfilesBlock(mise, ctx) !== mise;
12240
+ const { findings: auditFindings, pendingChanges, ...audited } = finding2;
12241
+ const problems = auditFindings + wiring.length + registryBlocked.length + retired.length;
12242
+ const summary = problems ? `${problems} Skillex finding(s)${pendingChanges ? `, ${pendingChanges} pending change(s)` : ""}` : pendingChanges && audited.status !== "pass" ? `${pendingChanges} pending Skillex change(s)` : audited.summary;
12095
12243
  return {
12096
- ...finding2,
12097
- status: wiring.length || retired.length || registryBlocked.length ? "fail" : finding2.status,
12098
- fixable: unsafe.length ? false : finding2.fixable || wiring.length > 0 || retired.length > 0 || miseDrift,
12099
- summary: details.length ? `${details.length} Skillex finding(s)` : finding2.summary,
12244
+ ...audited,
12245
+ status: wiring.length || retired.length || registryBlocked.length ? "fail" : audited.status,
12246
+ fixable: unsafe.length ? false : audited.fixable || wiring.length > 0 || retired.length > 0 || miseDrift,
12247
+ summary,
12100
12248
  details
12101
12249
  };
12102
12250
  },
@@ -12669,24 +12817,52 @@ function withInstallerSkillHandoff(check) {
12669
12817
  }
12670
12818
  };
12671
12819
  }
12672
- function withScaffoldPostcondition(check) {
12820
+ function withScaffoldScriptRestore(check) {
12673
12821
  if (check.id !== "bmad.scaffold") return check;
12674
12822
  return {
12675
12823
  ...check,
12676
12824
  migrate: async (ctx, finding2) => {
12677
- const result2 = await check.migrate(ctx, finding2);
12678
- if (ctx.dryRun || result2.status !== "applied" && result2.status !== "noop") return result2;
12679
- const after = await check.audit(ctx);
12680
- if (after.status === "pass" || after.status === "skip") return result2;
12681
- return {
12682
- ...result2,
12683
- status: "partial",
12684
- summary: after.summary,
12685
- details: [...result2.details, `still failing after the install: ${after.summary}`, ...after.details]
12686
- };
12825
+ const attempt = restoreScaffoldScripts(ctx, finding2);
12826
+ if (attempt.result) return attempt.result;
12827
+ const viaInstaller = await check.migrate(ctx, finding2);
12828
+ return attempt.note ? { ...viaInstaller, details: [attempt.note, ...viaInstaller.details] } : viaInstaller;
12687
12829
  }
12688
12830
  };
12689
12831
  }
12832
+ function restoreScaffoldScripts(ctx, finding2) {
12833
+ const manifest = manifestBmadModules(ctx.repoRoot);
12834
+ if (manifest.status === "invalid") return {};
12835
+ const modules = manifest.status === "valid" ? manifest.modules : configuredBmadModuleSelection(ctx.repoRoot)?.modules ?? [...DEFAULT_BMAD_MODULES];
12836
+ const issues = bmadScaffoldIssues(ctx.repoRoot, modules);
12837
+ const scriptGap = (path) => BMAD_RUNTIME_SCRIPT_SENTINELS.some((script) => path === join12("scripts", script));
12838
+ if (!issues.gaps.length || issues.nameIssues.details.length || !issues.gaps.every((gap) => scriptGap(gap.path))) return {};
12839
+ const state = bmadSetupScriptsState(ctx.repoRoot);
12840
+ if (state.kind === "present") return {};
12841
+ if (state.kind !== "missing") {
12842
+ return { note: `targeted _bmad/scripts restore not possible, so this takes the installer path: ${state.kind === "decline" ? state.summary : state.error}` };
12843
+ }
12844
+ const source = `bmad-method@${BMAD_INSTALLER_VERSION} (${state.pkg.root})`;
12845
+ if (ctx.dryRun) {
12846
+ return { result: {
12847
+ id: finding2.id,
12848
+ title: finding2.title,
12849
+ status: "applied",
12850
+ summary: "Would restore the missing _bmad/scripts from the verified pinned bmad-method package (no reinstall)",
12851
+ changedFiles: state.files,
12852
+ details: [source]
12853
+ } };
12854
+ }
12855
+ const applied = applyBmadSetupScripts(ctx.repoRoot, state.pkg, BMAD_INSTALLER_VERSION);
12856
+ if (applied.error) return { note: `targeted _bmad/scripts restore failed, so this takes the installer path: ${applied.error}` };
12857
+ return { result: {
12858
+ id: finding2.id,
12859
+ title: finding2.title,
12860
+ status: applied.files.length ? "applied" : "noop",
12861
+ summary: applied.files.length ? "Restored the missing _bmad/scripts from the verified pinned bmad-method package (no reinstall)" : "No changes required",
12862
+ changedFiles: applied.files,
12863
+ details: [source]
12864
+ } };
12865
+ }
12690
12866
  function createBmadChecks() {
12691
12867
  return [
12692
12868
  {
@@ -12963,6 +13139,22 @@ function createBmadChecks() {
12963
13139
  };
12964
13140
  }
12965
13141
  const behindStable = !pinned && stable ? compareBmadVersions(installed, stable) < 0 : false;
13142
+ if (behindStable && compareBmadVersions(installed, BMAD_INSTALLER_VERSION) === 0) {
13143
+ return {
13144
+ id: "bmad.version",
13145
+ title: "BMAD version currency",
13146
+ status: "pass",
13147
+ summary: `BMAD ${installed} matches the verified installer pin; stable ${stable} is newer (advisory)${staleNote}`,
13148
+ details: [
13149
+ `installed: ${installed}`,
13150
+ `verified installer pin: ${BMAD_INSTALLER_VERSION}`,
13151
+ `stable latest: ${stable}`,
13152
+ "pjangler advances its pin only through the verified-installer path (`mise run bmad:verify-surface <version>`)",
13153
+ "run `pj migrate bmad.version` to take the newer release anyway"
13154
+ ],
13155
+ fixable: false
13156
+ };
13157
+ }
12966
13158
  if (!pinned && !behindStable) {
12967
13159
  return {
12968
13160
  id: "bmad.version",
@@ -13198,7 +13390,7 @@ function createBmadChecks() {
13198
13390
  };
13199
13391
  }
13200
13392
  }
13201
- ].map(withInstallerSkillHandoff).map(withScaffoldPostcondition);
13393
+ ].map(withInstallerSkillHandoff).map(withScaffoldScriptRestore);
13202
13394
  }
13203
13395
  function createProjectMomoChecks() {
13204
13396
  return [
@@ -13821,6 +14013,7 @@ function ticketScope(ticket) {
13821
14013
  }
13822
14014
 
13823
14015
  // src/recipes/ProjectRecipe.ts
14016
+ init_manifestIndex();
13824
14017
  init_tree_diff();
13825
14018
 
13826
14019
  // src/recipes/NotebookRecipe.ts
@@ -15650,6 +15843,17 @@ function unsafeToRemove(targetDir) {
15650
15843
  }
15651
15844
  return null;
15652
15845
  }
15846
+ function manifestRefusal(manifestPath) {
15847
+ if (!existsSync18(manifestPath)) return void 0;
15848
+ try {
15849
+ const parsed = JSON.parse(readFileSync21(manifestPath, "utf8"));
15850
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return `Expected a JSON object: ${manifestPath}`;
15851
+ validateManifestShape(parsed, manifestPath);
15852
+ return void 0;
15853
+ } catch (error) {
15854
+ return error instanceof SyntaxError ? `Malformed JSON: ${manifestPath}` : error instanceof Error ? error.message : String(error);
15855
+ }
15856
+ }
15653
15857
  function publicAudit(report) {
15654
15858
  return {
15655
15859
  ...report,
@@ -15775,6 +15979,8 @@ var ProjectRecipe = class extends Recipe {
15775
15979
  });
15776
15980
  };
15777
15981
  let migrationReport;
15982
+ let unfinishedMigrations = [];
15983
+ const warnings = [];
15778
15984
  let audit;
15779
15985
  let notebookPlan;
15780
15986
  let notebookRecipeForCommit;
@@ -15874,20 +16080,37 @@ var ProjectRecipe = class extends Recipe {
15874
16080
  normalized.selectedRuleIds
15875
16081
  ));
15876
16082
  changedFiles.push(...migrationReport.changedFiles);
15877
- phases.push(...migrationReport.results.map((result2) => ({
16083
+ const migrationPhases = migrationReport.results.map((result2) => ({
15878
16084
  id: result2.id,
15879
16085
  status: result2.status === "applied" ? "changed" : result2.status === "noop" ? "unchanged" : result2.status === "skipped" ? "skipped" : "failed",
15880
16086
  // A `partial` failed, but its writes really happened, so they stay
15881
16087
  // accounted for rather than vanishing from the transaction record.
15882
16088
  changedFiles: result2.status === "applied" || result2.status === "partial" ? result2.changedFiles : [],
15883
16089
  message: result2.summary
15884
- })));
15885
- errors.push(...migrationReport.results.filter((result2) => result2.status === "blocked" || result2.status === "partial").map((result2) => describeFinding(result2)));
16090
+ }));
16091
+ phases.push(...migrationPhases);
16092
+ unfinishedMigrations = migrationReport.results.map((result2, index) => ({ result: result2, phase: migrationPhases[index] })).filter(({ result: result2 }) => result2.status === "blocked" || result2.status === "partial");
15886
16093
  }
15887
16094
  const eligibilityAudit = errors.length === 0 ? publicAudit(await this.registry.auditRecipes({ ...transactionContext, dryRun: true })) : void 0;
15888
16095
  audit = eligibilityAudit;
16096
+ const gating = new Set((eligibilityAudit?.rules ?? []).filter((finding2) => gatesProject(finding2)).map((finding2) => finding2.id));
16097
+ const reportedByMigration = /* @__PURE__ */ new Set();
16098
+ for (const { result: result2, phase } of unfinishedMigrations) {
16099
+ if (!eligibilityAudit || gating.has(result2.id)) {
16100
+ errors.push(describeFinding(result2));
16101
+ reportedByMigration.add(result2.id);
16102
+ } else {
16103
+ result2.advisory = true;
16104
+ phase.status = "skipped";
16105
+ phase.message = `advisory (does not block init): ${result2.summary}`;
16106
+ warnings.push(`${result2.id}: ${result2.summary}`);
16107
+ }
16108
+ }
16109
+ if (migrationReport && unfinishedMigrations.length && !unfinishedMigrations.some(({ result: result2 }) => !result2.advisory)) {
16110
+ migrationReport.ok = true;
16111
+ }
15889
16112
  if (eligibilityAudit && !eligibilityAudit.ok) {
15890
- errors.push(...eligibilityAudit.rules.filter((finding2) => gatesProject(finding2)).map((finding2) => describeFinding(finding2)));
16113
+ errors.push(...eligibilityAudit.rules.filter((finding2) => gatesProject(finding2) && !reportedByMigration.has(finding2.id)).map((finding2) => describeFinding(finding2)));
15891
16114
  }
15892
16115
  phases.push({
15893
16116
  id: "project.audit:eligibility",
@@ -15931,6 +16154,13 @@ var ProjectRecipe = class extends Recipe {
15931
16154
  }
15932
16155
  }
15933
16156
  }
16157
+ if (errors.length === 0) {
16158
+ const refusal = manifestRefusal(join24(targetDir, ".project.json"));
16159
+ if (refusal) {
16160
+ errors.push(`manifest refused before external effects: ${refusal}`);
16161
+ phases.push({ id: "project.manifest:validate", status: "failed", changedFiles: [], message: refusal });
16162
+ }
16163
+ }
15934
16164
  registryFinalizerEligible = errors.length === 0;
15935
16165
  if (errors.length === 0 && externalPlanActions.length) {
15936
16166
  const dispatching = externalPlanActions.some((action) => action.kind === "ticket-provider.create-or-link" && action.enabled);
@@ -16102,6 +16332,7 @@ var ProjectRecipe = class extends Recipe {
16102
16332
  selectedOperations: normalized.selectedOperations ?? [],
16103
16333
  selectedParityRules: normalized.selectedRuleIds ?? [],
16104
16334
  migrationReport,
16335
+ warnings,
16105
16336
  ...bmadCompanion ? { bmadCompanion } : {}
16106
16337
  };
16107
16338
  }
@@ -17902,6 +18133,7 @@ function projectPreflightFailure(plan, errors, audit) {
17902
18133
  plan,
17903
18134
  mode: plan.actions.some((action) => action.kind === "copier.copy.commonproject") ? "create" : "sync",
17904
18135
  audit,
18136
+ warnings: [],
17905
18137
  selectedOperations: plan.actions.map((action) => action.kind),
17906
18138
  selectedParityRules: []
17907
18139
  };
@@ -210,7 +210,7 @@ function mergeMissing(current, fallback) {
210
210
  function hash(data) {
211
211
  return createHash("sha256").update(data).digest("hex");
212
212
  }
213
- function validateManifestFields(manifest, path) {
213
+ function validateManifestShape(manifest, path) {
214
214
  validateExecutionBinding(manifest);
215
215
  const fail = (field, shape) => {
216
216
  throw new RegistryError(`Manifest ${path}: ${field} must be ${shape}`);
@@ -269,7 +269,7 @@ async function readManifest(path) {
269
269
  throw new RegistryError(`Malformed JSON: ${path}`);
270
270
  }
271
271
  if (!object(manifest)) throw new RegistryError(`Expected a JSON object: ${path}`);
272
- validateManifestFields(manifest, path);
272
+ validateManifestShape(manifest, path);
273
273
  const id = normalizeProjectId2(manifest.project_id ?? manifest.project_slug);
274
274
  if (manifest.project_id && manifest.project_slug && normalizeProjectId2(manifest.project_slug) !== id) {
275
275
  throw new RegistryError(`Conflicting project_id and project_slug: ${path}`, 409, "identity_conflict");
@@ -298,7 +298,7 @@ function recordFromManifest(manifest, path) {
298
298
  };
299
299
  }
300
300
  async function writeManifest(original, manifest) {
301
- validateManifestFields(manifest, original.path);
301
+ validateManifestShape(manifest, original.path);
302
302
  const serialized = serializedManifest(manifest);
303
303
  const temp = join(dirname(original.path), `.project.json.${randomUUID()}.tmp`);
304
304
  let committed = false;
@@ -585,7 +585,7 @@ var ManifestIndex = class {
585
585
  else throw new RegistryError(`Missing concurrency baseline for ${id}; reload the registry and retry`, 409, "stale_manifest");
586
586
  if (next.repo_path !== current.repo_path) throw new RegistryError(`Cannot move ${id} through a registry snapshot; register its canonical manifest path`, 409, "identity_conflict");
587
587
  const manifest = applyRecord(read.manifest, current, next);
588
- validateManifestFields(manifest, path);
588
+ validateManifestShape(manifest, path);
589
589
  serializedManifest(manifest);
590
590
  writes.push({ read, manifest, oldId: row?.project_id });
591
591
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@delorenj/pjangler",
3
- "version": "1.5.52",
3
+ "version": "1.5.54",
4
4
  "description": "Project subsystem bootstrapper CLI",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",