@delorenj/pjangler 1.5.53 → 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 +261 -42
- package/dist/mcp-server.js +254 -38
- package/dist/project-registry-service.js +4 -4
- package/package.json +1 -1
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
6996
|
-
const objectPath = join5(open(), "objects",
|
|
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
|
-
|
|
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)
|
|
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
|
-
|
|
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))
|
|
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
|
-
...
|
|
13286
|
-
status: wiring.length || retired.length || registryBlocked.length ? "fail" :
|
|
13287
|
-
fixable: unsafe.length ? false :
|
|
13288
|
-
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
|
|
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
|
|
13867
|
-
if (
|
|
13868
|
-
const
|
|
13869
|
-
|
|
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
|
{
|
|
@@ -14403,7 +14579,7 @@ function createBmadChecks() {
|
|
|
14403
14579
|
};
|
|
14404
14580
|
}
|
|
14405
14581
|
}
|
|
14406
|
-
].map(withInstallerSkillHandoff).map(
|
|
14582
|
+
].map(withInstallerSkillHandoff).map(withScaffoldScriptRestore);
|
|
14407
14583
|
}
|
|
14408
14584
|
function createProjectMomoChecks() {
|
|
14409
14585
|
return [
|
|
@@ -14614,16 +14790,17 @@ function formatAuditReport(report) {
|
|
|
14614
14790
|
}
|
|
14615
14791
|
function formatMigrationReport(report) {
|
|
14616
14792
|
const idWidth = report.results.reduce((width, result2) => Math.max(width, result2.id.length), 0);
|
|
14617
|
-
const
|
|
14618
|
-
const
|
|
14619
|
-
const
|
|
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`)}`;
|
|
14620
14797
|
const lines = [""];
|
|
14621
14798
|
lines.push(` ${overall}${report.dryRun ? ` ${dim(glyph.dot)} ${yellow("dry run")}` : ""}`);
|
|
14622
14799
|
lines.push(` ${dim(report.repo)}`);
|
|
14623
14800
|
if (report.selectedRules.length) lines.push(` ${dim(`rules: ${report.selectedRules.join(", ")}`)}`);
|
|
14624
14801
|
lines.push("");
|
|
14625
14802
|
for (const result2 of report.results) {
|
|
14626
|
-
const style = statusStyle(result2.status);
|
|
14803
|
+
const style = result2.advisory ? { ...statusStyle("warn"), label: `warning, ${result2.status}` } : statusStyle(result2.status);
|
|
14627
14804
|
lines.push(` ${style.color(style.glyph)} ${style.color(result2.id.padEnd(idWidth))} ${result2.summary} ${dim(`[${style.label}]`)}`);
|
|
14628
14805
|
for (const detail of result2.details) lines.push(` ${dim(glyph.arrow)} ${dim(detail)}`);
|
|
14629
14806
|
for (const file of result2.changedFiles) lines.push(` ${green(glyph.add)} ${file}`);
|
|
@@ -14637,6 +14814,9 @@ function formatMigrationReport(report) {
|
|
|
14637
14814
|
if (unresolved) {
|
|
14638
14815
|
lines.push("");
|
|
14639
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.`)}`);
|
|
14640
14820
|
}
|
|
14641
14821
|
lines.push("");
|
|
14642
14822
|
return lines.join("\n");
|
|
@@ -15106,6 +15286,7 @@ function ticketScope(ticket) {
|
|
|
15106
15286
|
}
|
|
15107
15287
|
|
|
15108
15288
|
// src/recipes/ProjectRecipe.ts
|
|
15289
|
+
init_manifestIndex();
|
|
15109
15290
|
init_tree_diff();
|
|
15110
15291
|
|
|
15111
15292
|
// src/recipes/NotebookRecipe.ts
|
|
@@ -17172,6 +17353,17 @@ function unsafeToRemove(targetDir) {
|
|
|
17172
17353
|
}
|
|
17173
17354
|
return null;
|
|
17174
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
|
+
}
|
|
17175
17367
|
function publicAudit(report) {
|
|
17176
17368
|
return {
|
|
17177
17369
|
...report,
|
|
@@ -17297,6 +17489,8 @@ var ProjectRecipe = class extends Recipe {
|
|
|
17297
17489
|
});
|
|
17298
17490
|
};
|
|
17299
17491
|
let migrationReport;
|
|
17492
|
+
let unfinishedMigrations = [];
|
|
17493
|
+
const warnings = [];
|
|
17300
17494
|
let audit;
|
|
17301
17495
|
let notebookPlan;
|
|
17302
17496
|
let notebookRecipeForCommit;
|
|
@@ -17396,20 +17590,37 @@ var ProjectRecipe = class extends Recipe {
|
|
|
17396
17590
|
normalized.selectedRuleIds
|
|
17397
17591
|
));
|
|
17398
17592
|
changedFiles.push(...migrationReport.changedFiles);
|
|
17399
|
-
|
|
17593
|
+
const migrationPhases = migrationReport.results.map((result2) => ({
|
|
17400
17594
|
id: result2.id,
|
|
17401
17595
|
status: result2.status === "applied" ? "changed" : result2.status === "noop" ? "unchanged" : result2.status === "skipped" ? "skipped" : "failed",
|
|
17402
17596
|
// A `partial` failed, but its writes really happened, so they stay
|
|
17403
17597
|
// accounted for rather than vanishing from the transaction record.
|
|
17404
17598
|
changedFiles: result2.status === "applied" || result2.status === "partial" ? result2.changedFiles : [],
|
|
17405
17599
|
message: result2.summary
|
|
17406
|
-
}))
|
|
17407
|
-
|
|
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");
|
|
17408
17603
|
}
|
|
17409
17604
|
const eligibilityAudit = errors.length === 0 ? publicAudit(await this.registry.auditRecipes({ ...transactionContext, dryRun: true })) : void 0;
|
|
17410
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
|
+
}
|
|
17411
17622
|
if (eligibilityAudit && !eligibilityAudit.ok) {
|
|
17412
|
-
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)));
|
|
17413
17624
|
}
|
|
17414
17625
|
phases.push({
|
|
17415
17626
|
id: "project.audit:eligibility",
|
|
@@ -17453,6 +17664,13 @@ var ProjectRecipe = class extends Recipe {
|
|
|
17453
17664
|
}
|
|
17454
17665
|
}
|
|
17455
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
|
+
}
|
|
17456
17674
|
registryFinalizerEligible = errors.length === 0;
|
|
17457
17675
|
if (errors.length === 0 && externalPlanActions.length) {
|
|
17458
17676
|
const dispatching = externalPlanActions.some((action) => action.kind === "ticket-provider.create-or-link" && action.enabled);
|
|
@@ -17624,6 +17842,7 @@ var ProjectRecipe = class extends Recipe {
|
|
|
17624
17842
|
selectedOperations: normalized.selectedOperations ?? [],
|
|
17625
17843
|
selectedParityRules: normalized.selectedRuleIds ?? [],
|
|
17626
17844
|
migrationReport,
|
|
17845
|
+
warnings,
|
|
17627
17846
|
...bmadCompanion ? { bmadCompanion } : {}
|
|
17628
17847
|
};
|
|
17629
17848
|
}
|
package/dist/mcp-server.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
6228
|
-
const objectPath = join5(open(), "objects",
|
|
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
|
-
|
|
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)
|
|
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
|
-
|
|
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))
|
|
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
|
-
...
|
|
12097
|
-
status: wiring.length || retired.length || registryBlocked.length ? "fail" :
|
|
12098
|
-
fixable: unsafe.length ? false :
|
|
12099
|
-
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
|
|
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
|
|
12678
|
-
if (
|
|
12679
|
-
const
|
|
12680
|
-
|
|
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
|
{
|
|
@@ -13214,7 +13390,7 @@ function createBmadChecks() {
|
|
|
13214
13390
|
};
|
|
13215
13391
|
}
|
|
13216
13392
|
}
|
|
13217
|
-
].map(withInstallerSkillHandoff).map(
|
|
13393
|
+
].map(withInstallerSkillHandoff).map(withScaffoldScriptRestore);
|
|
13218
13394
|
}
|
|
13219
13395
|
function createProjectMomoChecks() {
|
|
13220
13396
|
return [
|
|
@@ -13837,6 +14013,7 @@ function ticketScope(ticket) {
|
|
|
13837
14013
|
}
|
|
13838
14014
|
|
|
13839
14015
|
// src/recipes/ProjectRecipe.ts
|
|
14016
|
+
init_manifestIndex();
|
|
13840
14017
|
init_tree_diff();
|
|
13841
14018
|
|
|
13842
14019
|
// src/recipes/NotebookRecipe.ts
|
|
@@ -15666,6 +15843,17 @@ function unsafeToRemove(targetDir) {
|
|
|
15666
15843
|
}
|
|
15667
15844
|
return null;
|
|
15668
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
|
+
}
|
|
15669
15857
|
function publicAudit(report) {
|
|
15670
15858
|
return {
|
|
15671
15859
|
...report,
|
|
@@ -15791,6 +15979,8 @@ var ProjectRecipe = class extends Recipe {
|
|
|
15791
15979
|
});
|
|
15792
15980
|
};
|
|
15793
15981
|
let migrationReport;
|
|
15982
|
+
let unfinishedMigrations = [];
|
|
15983
|
+
const warnings = [];
|
|
15794
15984
|
let audit;
|
|
15795
15985
|
let notebookPlan;
|
|
15796
15986
|
let notebookRecipeForCommit;
|
|
@@ -15890,20 +16080,37 @@ var ProjectRecipe = class extends Recipe {
|
|
|
15890
16080
|
normalized.selectedRuleIds
|
|
15891
16081
|
));
|
|
15892
16082
|
changedFiles.push(...migrationReport.changedFiles);
|
|
15893
|
-
|
|
16083
|
+
const migrationPhases = migrationReport.results.map((result2) => ({
|
|
15894
16084
|
id: result2.id,
|
|
15895
16085
|
status: result2.status === "applied" ? "changed" : result2.status === "noop" ? "unchanged" : result2.status === "skipped" ? "skipped" : "failed",
|
|
15896
16086
|
// A `partial` failed, but its writes really happened, so they stay
|
|
15897
16087
|
// accounted for rather than vanishing from the transaction record.
|
|
15898
16088
|
changedFiles: result2.status === "applied" || result2.status === "partial" ? result2.changedFiles : [],
|
|
15899
16089
|
message: result2.summary
|
|
15900
|
-
}))
|
|
15901
|
-
|
|
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");
|
|
15902
16093
|
}
|
|
15903
16094
|
const eligibilityAudit = errors.length === 0 ? publicAudit(await this.registry.auditRecipes({ ...transactionContext, dryRun: true })) : void 0;
|
|
15904
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
|
+
}
|
|
15905
16112
|
if (eligibilityAudit && !eligibilityAudit.ok) {
|
|
15906
|
-
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)));
|
|
15907
16114
|
}
|
|
15908
16115
|
phases.push({
|
|
15909
16116
|
id: "project.audit:eligibility",
|
|
@@ -15947,6 +16154,13 @@ var ProjectRecipe = class extends Recipe {
|
|
|
15947
16154
|
}
|
|
15948
16155
|
}
|
|
15949
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
|
+
}
|
|
15950
16164
|
registryFinalizerEligible = errors.length === 0;
|
|
15951
16165
|
if (errors.length === 0 && externalPlanActions.length) {
|
|
15952
16166
|
const dispatching = externalPlanActions.some((action) => action.kind === "ticket-provider.create-or-link" && action.enabled);
|
|
@@ -16118,6 +16332,7 @@ var ProjectRecipe = class extends Recipe {
|
|
|
16118
16332
|
selectedOperations: normalized.selectedOperations ?? [],
|
|
16119
16333
|
selectedParityRules: normalized.selectedRuleIds ?? [],
|
|
16120
16334
|
migrationReport,
|
|
16335
|
+
warnings,
|
|
16121
16336
|
...bmadCompanion ? { bmadCompanion } : {}
|
|
16122
16337
|
};
|
|
16123
16338
|
}
|
|
@@ -17918,6 +18133,7 @@ function projectPreflightFailure(plan, errors, audit) {
|
|
|
17918
18133
|
plan,
|
|
17919
18134
|
mode: plan.actions.some((action) => action.kind === "copier.copy.commonproject") ? "create" : "sync",
|
|
17920
18135
|
audit,
|
|
18136
|
+
warnings: [],
|
|
17921
18137
|
selectedOperations: plan.actions.map((action) => action.kind),
|
|
17922
18138
|
selectedParityRules: []
|
|
17923
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
588
|
+
validateManifestShape(manifest, path);
|
|
589
589
|
serializedManifest(manifest);
|
|
590
590
|
writes.push({ read, manifest, oldId: row?.project_id });
|
|
591
591
|
}
|