@delorenj/pjangler 1.5.13 → 1.5.15
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/README.md +5 -0
- package/dist/index.js +1173 -891
- package/dist/mcp-server.js +1056 -777
- package/package.json +2 -2
- package/templates/commonproject/copier.yml +5 -3
- package/templates/commonproject/template/mise.toml.jinja +0 -5
package/dist/mcp-server.js
CHANGED
|
@@ -177,6 +177,41 @@ try {
|
|
|
177
177
|
}
|
|
178
178
|
});
|
|
179
179
|
|
|
180
|
+
// src/utils/tree-diff.ts
|
|
181
|
+
import { createHash } from "node:crypto";
|
|
182
|
+
import { lstatSync, readFileSync, readdirSync, readlinkSync } from "node:fs";
|
|
183
|
+
import { join as join2, relative } from "node:path";
|
|
184
|
+
function snapshotTree(root, current = root, snapshot = /* @__PURE__ */ new Map()) {
|
|
185
|
+
let stat;
|
|
186
|
+
try {
|
|
187
|
+
stat = lstatSync(current);
|
|
188
|
+
} catch (error) {
|
|
189
|
+
if (error.code === "ENOENT") return snapshot;
|
|
190
|
+
throw error;
|
|
191
|
+
}
|
|
192
|
+
const rel = relative(root, current) || ".";
|
|
193
|
+
if (rel === ".git" || rel.startsWith(`.git${process.platform === "win32" ? "\\" : "/"}`)) return snapshot;
|
|
194
|
+
if (stat.isSymbolicLink()) {
|
|
195
|
+
snapshot.set(rel, `link:${readlinkSync(current)}`);
|
|
196
|
+
} else if (stat.isFile()) {
|
|
197
|
+
snapshot.set(rel, `file:${createHash("sha256").update(readFileSync(current)).digest("hex")}:${stat.mode & 4095}`);
|
|
198
|
+
} else if (stat.isDirectory()) {
|
|
199
|
+
snapshot.set(rel, `dir:${stat.mode & 4095}`);
|
|
200
|
+
for (const name of readdirSync(current)) snapshotTree(root, join2(current, name), snapshot);
|
|
201
|
+
} else {
|
|
202
|
+
snapshot.set(rel, `other:${stat.mode}`);
|
|
203
|
+
}
|
|
204
|
+
return snapshot;
|
|
205
|
+
}
|
|
206
|
+
function changedTreePaths(root, before, after) {
|
|
207
|
+
return [.../* @__PURE__ */ new Set([...before.keys(), ...after.keys()])].filter((path) => before.get(path) !== after.get(path)).map((path) => join2(root, path)).sort();
|
|
208
|
+
}
|
|
209
|
+
var init_tree_diff = __esm({
|
|
210
|
+
"src/utils/tree-diff.ts"() {
|
|
211
|
+
"use strict";
|
|
212
|
+
}
|
|
213
|
+
});
|
|
214
|
+
|
|
180
215
|
// src/notebook/types.ts
|
|
181
216
|
function notebookCredentialMaterialPath(value) {
|
|
182
217
|
const seen = /* @__PURE__ */ new WeakSet();
|
|
@@ -258,7 +293,7 @@ var init_types = __esm({
|
|
|
258
293
|
|
|
259
294
|
// src/project/notebookSettingsValidation.ts
|
|
260
295
|
import { isIP } from "node:net";
|
|
261
|
-
import { isAbsolute as
|
|
296
|
+
import { isAbsolute as isAbsolute3 } from "node:path";
|
|
262
297
|
function isRecord(value) {
|
|
263
298
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
264
299
|
}
|
|
@@ -315,7 +350,7 @@ function validateGlobalNotebookConfig(value) {
|
|
|
315
350
|
if (limits.lease_seconds * 1e3 <= limits.overall_timeout_ms) throw new Error("Project registry notebook lease_seconds must exceed one request timeout");
|
|
316
351
|
if (isRecord(value.defaults) && value.defaults.overview_max_chars !== void 0 && Number(value.defaults.overview_max_chars) > limits.note_max_bytes) throw new Error("Project registry notebook overview_max_chars exceeds note_max_bytes");
|
|
317
352
|
if (value.summarizer !== void 0) {
|
|
318
|
-
if (!isRecord(value.summarizer) || typeof value.summarizer.executable !== "string" || !
|
|
353
|
+
if (!isRecord(value.summarizer) || typeof value.summarizer.executable !== "string" || !isAbsolute3(value.summarizer.executable) || value.summarizer.executable.includes("\0") || Buffer.byteLength(value.summarizer.executable, "utf8") > 1024) throw new Error("Project registry notebook.summarizer executable must be a bounded absolute path");
|
|
319
354
|
if (value.summarizer.args !== void 0 && (!Array.isArray(value.summarizer.args) || value.summarizer.args.length > 32 || value.summarizer.args.some((entry) => typeof entry !== "string" || entry.includes("\0") || Buffer.byteLength(entry, "utf8") > 1024))) throw new Error("Project registry notebook.summarizer args must be bounded strings");
|
|
320
355
|
}
|
|
321
356
|
}
|
|
@@ -334,7 +369,7 @@ var init_executionBinding = __esm({
|
|
|
334
369
|
});
|
|
335
370
|
|
|
336
371
|
// src/project/manifestIndex.ts
|
|
337
|
-
import { basename as basename4, dirname as dirname6, join as
|
|
372
|
+
import { basename as basename4, dirname as dirname6, join as join8, resolve as resolve7 } from "node:path";
|
|
338
373
|
function normalizeProjectId2(value) {
|
|
339
374
|
try {
|
|
340
375
|
return normalizeProjectId(value);
|
|
@@ -380,53 +415,23 @@ var init_manifestIndex = __esm({
|
|
|
380
415
|
}
|
|
381
416
|
});
|
|
382
417
|
|
|
383
|
-
// src/utils/tree-diff.ts
|
|
384
|
-
import { createHash as createHash3 } from "node:crypto";
|
|
385
|
-
import { existsSync as existsSync4, lstatSync as lstatSync5, readFileSync as readFileSync4, readdirSync as readdirSync4, readlinkSync as readlinkSync4 } from "node:fs";
|
|
386
|
-
import { join as join7, relative as relative5 } from "node:path";
|
|
387
|
-
function snapshotTree(root, current = root, snapshot = /* @__PURE__ */ new Map()) {
|
|
388
|
-
if (!existsSync4(current)) return snapshot;
|
|
389
|
-
const rel = relative5(root, current) || ".";
|
|
390
|
-
if (rel === ".git" || rel.startsWith(`.git${process.platform === "win32" ? "\\" : "/"}`)) return snapshot;
|
|
391
|
-
const stat = lstatSync5(current);
|
|
392
|
-
if (stat.isSymbolicLink()) {
|
|
393
|
-
snapshot.set(rel, `link:${readlinkSync4(current)}`);
|
|
394
|
-
} else if (stat.isFile()) {
|
|
395
|
-
snapshot.set(rel, `file:${createHash3("sha256").update(readFileSync4(current)).digest("hex")}:${stat.mode & 511}`);
|
|
396
|
-
} else if (stat.isDirectory()) {
|
|
397
|
-
snapshot.set(rel, `dir:${stat.mode & 511}`);
|
|
398
|
-
for (const name of readdirSync4(current)) snapshotTree(root, join7(current, name), snapshot);
|
|
399
|
-
} else {
|
|
400
|
-
snapshot.set(rel, `other:${stat.mode}`);
|
|
401
|
-
}
|
|
402
|
-
return snapshot;
|
|
403
|
-
}
|
|
404
|
-
function changedTreePaths(root, before, after) {
|
|
405
|
-
return [.../* @__PURE__ */ new Set([...before.keys(), ...after.keys()])].filter((path) => path !== "." && before.get(path) !== after.get(path)).map((path) => join7(root, path)).sort();
|
|
406
|
-
}
|
|
407
|
-
var init_tree_diff = __esm({
|
|
408
|
-
"src/utils/tree-diff.ts"() {
|
|
409
|
-
"use strict";
|
|
410
|
-
}
|
|
411
|
-
});
|
|
412
|
-
|
|
413
418
|
// src/lifecycle/preflight.ts
|
|
414
419
|
import { createHash as createHash4 } from "node:crypto";
|
|
415
|
-
import { accessSync, constants, existsSync as existsSync5, lstatSync as
|
|
420
|
+
import { accessSync, constants, existsSync as existsSync5, lstatSync as lstatSync7, readFileSync as readFileSync6, readdirSync as readdirSync5, realpathSync as realpathSync4, statSync as statSync2 } from "node:fs";
|
|
416
421
|
import { tmpdir, userInfo } from "node:os";
|
|
417
|
-
import { basename as basename5, delimiter, dirname as dirname7, isAbsolute as
|
|
422
|
+
import { basename as basename5, delimiter, dirname as dirname7, isAbsolute as isAbsolute4, join as join9, relative as relative6, resolve as resolve8 } from "node:path";
|
|
418
423
|
import YAML3 from "yaml";
|
|
419
424
|
function containedBy(parent, candidate) {
|
|
420
|
-
const rel = relative6(
|
|
421
|
-
return rel === "" || !rel.startsWith("..") && !
|
|
425
|
+
const rel = relative6(resolve8(parent), resolve8(candidate));
|
|
426
|
+
return rel === "" || !rel.startsWith("..") && !isAbsolute4(rel);
|
|
422
427
|
}
|
|
423
428
|
function accountMiseShimsDir(home) {
|
|
424
|
-
return
|
|
429
|
+
return resolve8(join9(home, ".local", "share", "mise", "shims"));
|
|
425
430
|
}
|
|
426
431
|
function inactiveMiseShim(candidate, shimsDir) {
|
|
427
432
|
if (dirname7(candidate) !== shimsDir) return void 0;
|
|
428
433
|
try {
|
|
429
|
-
if (!
|
|
434
|
+
if (!lstatSync7(candidate).isSymbolicLink()) return void 0;
|
|
430
435
|
const realPath = realpathSync4(candidate);
|
|
431
436
|
if (basename5(realPath) !== "mise") return void 0;
|
|
432
437
|
return { path: candidate, realPath, reason: "inactive-mise-shim" };
|
|
@@ -438,10 +443,10 @@ function resolveCopierOnPath(env2, shimsDir) {
|
|
|
438
443
|
const skipped = [];
|
|
439
444
|
for (const rawEntry of (env2.PATH ?? "").split(delimiter)) {
|
|
440
445
|
const entry = rawEntry || process.cwd();
|
|
441
|
-
const candidate =
|
|
446
|
+
const candidate = resolve8(entry, process.platform === "win32" ? "copier.exe" : "copier");
|
|
442
447
|
try {
|
|
443
448
|
accessSync(candidate, constants.X_OK);
|
|
444
|
-
const stat =
|
|
449
|
+
const stat = lstatSync7(candidate);
|
|
445
450
|
if (!stat.isFile() && !stat.isSymbolicLink()) continue;
|
|
446
451
|
} catch {
|
|
447
452
|
continue;
|
|
@@ -459,10 +464,10 @@ function describeSkipped(skipped) {
|
|
|
459
464
|
return skipped.map((entry) => `${entry.path} -> ${entry.realPath}`).join(", ");
|
|
460
465
|
}
|
|
461
466
|
function sha256(path) {
|
|
462
|
-
return createHash4("sha256").update(
|
|
467
|
+
return createHash4("sha256").update(readFileSync6(path)).digest("base64url");
|
|
463
468
|
}
|
|
464
469
|
function fingerprint(path) {
|
|
465
|
-
const absolute =
|
|
470
|
+
const absolute = resolve8(path);
|
|
466
471
|
const realPath = realpathSync4(absolute);
|
|
467
472
|
const stat = statSync2(realPath);
|
|
468
473
|
if (!stat.isFile()) throw new Error(`${absolute} is not a regular file`);
|
|
@@ -497,17 +502,17 @@ function sameFingerprint(expected) {
|
|
|
497
502
|
function consoleScriptContract(path) {
|
|
498
503
|
let text;
|
|
499
504
|
try {
|
|
500
|
-
text =
|
|
505
|
+
text = readFileSync6(path, "utf8").slice(0, 32 * 1024);
|
|
501
506
|
} catch (error) {
|
|
502
507
|
return { ok: false, error: `cannot read Copier launcher: ${error instanceof Error ? error.message : String(error)}` };
|
|
503
508
|
}
|
|
504
509
|
const firstLine = text.split(/\r?\n/, 1)[0] ?? "";
|
|
505
510
|
if (!firstLine.startsWith("#!")) return { ok: false, error: "Copier launcher has no executable shebang" };
|
|
506
511
|
const shebang = firstLine.slice(2).trim().split(/\s+/);
|
|
507
|
-
if (shebang.length !== 1 || !
|
|
512
|
+
if (shebang.length !== 1 || !isAbsolute4(shebang[0] ?? "")) {
|
|
508
513
|
return { ok: false, error: "Copier launcher must use one absolute Python interpreter" };
|
|
509
514
|
}
|
|
510
|
-
const interpreterPath =
|
|
515
|
+
const interpreterPath = resolve8(shebang[0]);
|
|
511
516
|
const interpreter = basename5(interpreterPath);
|
|
512
517
|
if (!/^python(?:\d+(?:\.\d+)*)?$/.test(interpreter)) {
|
|
513
518
|
return { ok: false, error: "Copier launcher is not an absolute Python console script" };
|
|
@@ -519,16 +524,16 @@ function consoleScriptContract(path) {
|
|
|
519
524
|
}
|
|
520
525
|
function defaultUvToolRoots(home) {
|
|
521
526
|
return [
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
].map((path) =>
|
|
527
|
+
join9(home, ".local", "share", "uv", "tools", "copier"),
|
|
528
|
+
join9(home, "Library", "Application Support", "uv", "tools", "copier")
|
|
529
|
+
].map((path) => resolve8(path));
|
|
525
530
|
}
|
|
526
531
|
function locateUvSitePackages(toolRoot) {
|
|
527
|
-
const lib =
|
|
532
|
+
const lib = join9(toolRoot, "lib");
|
|
528
533
|
const candidates = [];
|
|
529
534
|
for (const entry of readdirSync5(lib, { withFileTypes: true })) {
|
|
530
535
|
if (!entry.isDirectory() || !/^python\d+(?:\.\d+)*$/.test(entry.name)) continue;
|
|
531
|
-
const sitePackages =
|
|
536
|
+
const sitePackages = join9(lib, entry.name, "site-packages");
|
|
532
537
|
if (existsSync5(sitePackages)) candidates.push(sitePackages);
|
|
533
538
|
}
|
|
534
539
|
if (candidates.length !== 1) throw new Error(`expected one UV Copier site-packages directory, found ${candidates.length}`);
|
|
@@ -545,15 +550,15 @@ function parseRecordLine(line) {
|
|
|
545
550
|
};
|
|
546
551
|
}
|
|
547
552
|
function attestUvCopier(candidate, realCandidate, home) {
|
|
548
|
-
const toolRoot = defaultUvToolRoots(home).find((root) => realCandidate ===
|
|
553
|
+
const toolRoot = defaultUvToolRoots(home).find((root) => realCandidate === join9(root, "bin", "copier"));
|
|
549
554
|
if (!toolRoot) {
|
|
550
555
|
return { ok: false, error: `refusing untrusted PATH-shadowed Copier executable: ${candidate}` };
|
|
551
556
|
}
|
|
552
557
|
const supportedEntries = new Set([
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
].map((path) =>
|
|
556
|
-
if (!supportedEntries.has(
|
|
558
|
+
join9(home, ".local", "bin", "copier"),
|
|
559
|
+
join9(toolRoot, "bin", "copier")
|
|
560
|
+
].map((path) => resolve8(path)));
|
|
561
|
+
if (!supportedEntries.has(resolve8(candidate))) {
|
|
557
562
|
return { ok: false, error: `refusing non-canonical UV Copier entry point: ${candidate}` };
|
|
558
563
|
}
|
|
559
564
|
try {
|
|
@@ -561,43 +566,43 @@ function attestUvCopier(candidate, realCandidate, home) {
|
|
|
561
566
|
if (!launcher.ok || !launcher.interpreter) {
|
|
562
567
|
return { ...launcher, executable: realCandidate, realExecutable: realCandidate };
|
|
563
568
|
}
|
|
564
|
-
const expectedInterpreter =
|
|
565
|
-
if (
|
|
569
|
+
const expectedInterpreter = join9(toolRoot, "bin", basename5(launcher.interpreter));
|
|
570
|
+
if (resolve8(launcher.interpreter) !== resolve8(expectedInterpreter)) {
|
|
566
571
|
return { ok: false, error: "UV Copier launcher interpreter is outside the attested tool environment" };
|
|
567
572
|
}
|
|
568
573
|
const interpreterReal = realpathSync4(launcher.interpreter);
|
|
569
574
|
const uvPythonRoots = [
|
|
570
|
-
|
|
571
|
-
|
|
575
|
+
join9(home, ".local", "share", "uv", "python"),
|
|
576
|
+
join9(home, "Library", "Application Support", "uv", "python")
|
|
572
577
|
];
|
|
573
578
|
if (!uvPythonRoots.some((root) => containedBy(root, interpreterReal))) {
|
|
574
579
|
return { ok: false, error: "UV Copier interpreter is not managed by the canonical UV Python installation" };
|
|
575
580
|
}
|
|
576
|
-
const receiptPath =
|
|
577
|
-
const receipt =
|
|
581
|
+
const receiptPath = join9(toolRoot, "uv-receipt.toml");
|
|
582
|
+
const receipt = readFileSync6(receiptPath, "utf8");
|
|
578
583
|
if (!/requirements\s*=\s*\[[\s\S]*?name\s*=\s*["']copier["']/.test(receipt) || !/entrypoints\s*=\s*\[[\s\S]*?name\s*=\s*["']copier["'][\s\S]*?from\s*=\s*["']copier["']/.test(receipt)) {
|
|
579
584
|
return { ok: false, error: "UV tool receipt does not bind the copier entry point to the Copier package" };
|
|
580
585
|
}
|
|
581
586
|
const sitePackages = locateUvSitePackages(toolRoot);
|
|
582
|
-
const distInfos = readdirSync5(sitePackages, { withFileTypes: true }).filter((entry) => entry.isDirectory() && /^copier-[^-]+\.dist-info$/i.test(entry.name)).map((entry) =>
|
|
587
|
+
const distInfos = readdirSync5(sitePackages, { withFileTypes: true }).filter((entry) => entry.isDirectory() && /^copier-[^-]+\.dist-info$/i.test(entry.name)).map((entry) => join9(sitePackages, entry.name));
|
|
583
588
|
if (distInfos.length !== 1) {
|
|
584
589
|
return { ok: false, error: `expected one installed Copier distribution, found ${distInfos.length}` };
|
|
585
590
|
}
|
|
586
591
|
const distInfo = realpathSync4(distInfos[0]);
|
|
587
|
-
const metadataPath =
|
|
588
|
-
const entryPointsPath =
|
|
589
|
-
const recordPath =
|
|
590
|
-
const metadata =
|
|
592
|
+
const metadataPath = join9(distInfo, "METADATA");
|
|
593
|
+
const entryPointsPath = join9(distInfo, "entry_points.txt");
|
|
594
|
+
const recordPath = join9(distInfo, "RECORD");
|
|
595
|
+
const metadata = readFileSync6(metadataPath, "utf8");
|
|
591
596
|
const name = metadata.match(/^Name:\s*(.+)$/mi)?.[1]?.trim();
|
|
592
597
|
const version = metadata.match(/^Version:\s*(.+)$/mi)?.[1]?.trim();
|
|
593
598
|
if (name?.toLowerCase() !== "copier" || !version || !/^9(?:\.|$)/.test(version)) {
|
|
594
599
|
return { ok: false, error: "installed distribution is not a Copier 9 package" };
|
|
595
600
|
}
|
|
596
|
-
const entryPoints =
|
|
601
|
+
const entryPoints = readFileSync6(entryPointsPath, "utf8");
|
|
597
602
|
if (!/^copier\s*=\s*copier\.__main__:CopierApp\.run\s*$/m.test(entryPoints)) {
|
|
598
603
|
return { ok: false, error: "installed Copier distribution has an unexpected console entry point" };
|
|
599
604
|
}
|
|
600
|
-
const recordEntries =
|
|
605
|
+
const recordEntries = readFileSync6(recordPath, "utf8").split(/\r?\n/).map(parseRecordLine).filter((entry) => Boolean(entry));
|
|
601
606
|
const selected = recordEntries.filter((entry) => {
|
|
602
607
|
const normalized = entry.relativePath.replaceAll("\\", "/");
|
|
603
608
|
return normalized.startsWith("copier/") || normalized === `${basename5(distInfo)}/METADATA` || normalized === `${basename5(distInfo)}/entry_points.txt` || normalized === "../../../bin/copier";
|
|
@@ -607,7 +612,7 @@ function attestUvCopier(candidate, realCandidate, home) {
|
|
|
607
612
|
}
|
|
608
613
|
const attestedFiles = /* @__PURE__ */ new Map();
|
|
609
614
|
for (const entry of selected) {
|
|
610
|
-
const path =
|
|
615
|
+
const path = resolve8(sitePackages, entry.relativePath);
|
|
611
616
|
const allowed = path === realCandidate || containedBy(sitePackages, path);
|
|
612
617
|
if (!allowed) return { ok: false, error: `Copier RECORD path escapes the tool environment: ${entry.relativePath}` };
|
|
613
618
|
const actualDigest = sha256(path);
|
|
@@ -615,14 +620,14 @@ function attestUvCopier(candidate, realCandidate, home) {
|
|
|
615
620
|
if (actualDigest !== entry.digest || actualSize !== entry.size) {
|
|
616
621
|
return { ok: false, error: `Copier RECORD integrity mismatch: ${entry.relativePath}` };
|
|
617
622
|
}
|
|
618
|
-
attestedFiles.set(
|
|
623
|
+
attestedFiles.set(resolve8(path), fingerprint(path));
|
|
619
624
|
}
|
|
620
|
-
for (const path of [realCandidate,
|
|
621
|
-
attestedFiles.set(
|
|
625
|
+
for (const path of [realCandidate, join9(toolRoot, "pyvenv.cfg"), receiptPath, metadataPath, entryPointsPath, recordPath]) {
|
|
626
|
+
attestedFiles.set(resolve8(path), fingerprint(path));
|
|
622
627
|
}
|
|
623
628
|
const identity = {
|
|
624
629
|
executable: realCandidate,
|
|
625
|
-
resolvedFrom:
|
|
630
|
+
resolvedFrom: resolve8(candidate),
|
|
626
631
|
layout: "uv-tool",
|
|
627
632
|
version,
|
|
628
633
|
toolRoot,
|
|
@@ -641,7 +646,7 @@ function attestUvCopier(candidate, realCandidate, home) {
|
|
|
641
646
|
}
|
|
642
647
|
}
|
|
643
648
|
function verifyTrustedCopierIdentity(identity) {
|
|
644
|
-
if (!
|
|
649
|
+
if (!isAbsolute4(identity.executable) || resolve8(identity.executable) !== resolve8(identity.files.find((file) => file.path === identity.executable)?.path ?? "")) {
|
|
645
650
|
return { ok: false, error: "trusted Copier identity has no canonical absolute launcher" };
|
|
646
651
|
}
|
|
647
652
|
const interpreter = sameFingerprint(identity.interpreter);
|
|
@@ -654,9 +659,9 @@ function verifyTrustedCopierIdentity(identity) {
|
|
|
654
659
|
}
|
|
655
660
|
function preflightTrustedCopier(options) {
|
|
656
661
|
const env2 = options.env ?? process.env;
|
|
657
|
-
const home =
|
|
658
|
-
const temporary =
|
|
659
|
-
const target =
|
|
662
|
+
const home = resolve8(options.homeDir ?? userInfo().homedir);
|
|
663
|
+
const temporary = resolve8(options.temporaryDir ?? tmpdir());
|
|
664
|
+
const target = resolve8(options.targetDir);
|
|
660
665
|
const { candidate, skipped } = resolveCopierOnPath(env2, accountMiseShimsDir(home));
|
|
661
666
|
for (const shim of skipped) {
|
|
662
667
|
for (const [label, root] of [["target", target], ["temporary", temporary]]) {
|
|
@@ -692,18 +697,18 @@ function regularContainedFile(root, path, label) {
|
|
|
692
697
|
const rootReal = realpathSync4(root);
|
|
693
698
|
const fileReal = realpathSync4(path);
|
|
694
699
|
if (!containedBy(rootReal, fileReal)) return { ok: false, error: `${label} escapes its vendored template root` };
|
|
695
|
-
if (!
|
|
700
|
+
if (!lstatSync7(path).isFile()) return { ok: false, error: `${label} is not a regular file` };
|
|
696
701
|
return { ok: true };
|
|
697
702
|
} catch (error) {
|
|
698
703
|
return { ok: false, error: `${label} is unavailable: ${error instanceof Error ? error.message : String(error)}` };
|
|
699
704
|
}
|
|
700
705
|
}
|
|
701
706
|
function parseCopierConfig(templateRoot, label) {
|
|
702
|
-
const configPath =
|
|
707
|
+
const configPath = join9(templateRoot, "copier.yml");
|
|
703
708
|
const file = regularContainedFile(templateRoot, configPath, `${label} copier.yml`);
|
|
704
709
|
if (!file.ok) return { result: file };
|
|
705
710
|
try {
|
|
706
|
-
const parsed = YAML3.parse(
|
|
711
|
+
const parsed = YAML3.parse(readFileSync6(configPath, "utf8"));
|
|
707
712
|
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
708
713
|
return { result: { ok: false, error: `${label} copier.yml must contain a mapping` } };
|
|
709
714
|
}
|
|
@@ -721,13 +726,13 @@ function parseCopierConfig(templateRoot, label) {
|
|
|
721
726
|
}
|
|
722
727
|
function requireFiles(templateRoot, files, label) {
|
|
723
728
|
for (const rel of files) {
|
|
724
|
-
const result2 = regularContainedFile(templateRoot,
|
|
729
|
+
const result2 = regularContainedFile(templateRoot, join9(templateRoot, rel), `${label} ${rel}`);
|
|
725
730
|
if (!result2.ok) return result2;
|
|
726
731
|
}
|
|
727
732
|
return { ok: true };
|
|
728
733
|
}
|
|
729
734
|
function preflightCommonProjectTemplate(pjanglerRoot) {
|
|
730
|
-
const templateRoot =
|
|
735
|
+
const templateRoot = join9(resolve8(pjanglerRoot), "templates", "commonproject");
|
|
731
736
|
const parsed = parseCopierConfig(templateRoot, "CommonProject template");
|
|
732
737
|
if (!parsed.result.ok) return parsed.result;
|
|
733
738
|
const files = requireFiles(templateRoot, [
|
|
@@ -738,7 +743,7 @@ function preflightCommonProjectTemplate(pjanglerRoot) {
|
|
|
738
743
|
"template/mise.toml.jinja"
|
|
739
744
|
], "CommonProject template");
|
|
740
745
|
if (!files.ok) return files;
|
|
741
|
-
const projectJson =
|
|
746
|
+
const projectJson = readFileSync6(join9(templateRoot, "template", ".project.json.jinja"), "utf8");
|
|
742
747
|
for (const key of ["project_name", "repo_path", "ticket_provider", "agents"]) {
|
|
743
748
|
if (!projectJson.includes(`"${key}"`)) return { ok: false, error: `CommonProject projection is missing ${key}` };
|
|
744
749
|
}
|
|
@@ -763,12 +768,12 @@ var init_preflight = __esm({
|
|
|
763
768
|
});
|
|
764
769
|
|
|
765
770
|
// src/project/boardUrl.ts
|
|
766
|
-
import { existsSync as existsSync6, readFileSync as
|
|
767
|
-
import { dirname as dirname8, isAbsolute as
|
|
771
|
+
import { existsSync as existsSync6, readFileSync as readFileSync7, statSync as statSync3 } from "node:fs";
|
|
772
|
+
import { dirname as dirname8, isAbsolute as isAbsolute5, join as join10, resolve as resolve9 } from "node:path";
|
|
768
773
|
function findProjectRoot(from) {
|
|
769
|
-
let dir =
|
|
774
|
+
let dir = resolve9(from);
|
|
770
775
|
for (; ; ) {
|
|
771
|
-
if (existsSync6(
|
|
776
|
+
if (existsSync6(join10(dir, ".project.json"))) return dir;
|
|
772
777
|
const parent = dirname8(dir);
|
|
773
778
|
if (parent === dir) return void 0;
|
|
774
779
|
dir = parent;
|
|
@@ -781,8 +786,8 @@ var init_boardUrl = __esm({
|
|
|
781
786
|
});
|
|
782
787
|
|
|
783
788
|
// src/utils/version.ts
|
|
784
|
-
import { readFileSync as
|
|
785
|
-
import { dirname as dirname9, join as
|
|
789
|
+
import { readFileSync as readFileSync8 } from "node:fs";
|
|
790
|
+
import { dirname as dirname9, join as join11 } from "node:path";
|
|
786
791
|
import { fileURLToPath } from "node:url";
|
|
787
792
|
var PJANGLER_VERSION;
|
|
788
793
|
var init_version = __esm({
|
|
@@ -793,7 +798,7 @@ var init_version = __esm({
|
|
|
793
798
|
let dir = dirname9(fileURLToPath(import.meta.url));
|
|
794
799
|
for (let i = 0; i < 4; i++) {
|
|
795
800
|
try {
|
|
796
|
-
const raw =
|
|
801
|
+
const raw = readFileSync8(join11(dir, "package.json"), "utf8");
|
|
797
802
|
return JSON.parse(raw).version ?? "0.0.0";
|
|
798
803
|
} catch {
|
|
799
804
|
const parent = dirname9(dir);
|
|
@@ -822,10 +827,10 @@ var init_boardQuery = __esm({
|
|
|
822
827
|
});
|
|
823
828
|
|
|
824
829
|
// src/project/index.ts
|
|
825
|
-
import { spawnSync as
|
|
826
|
-
import { chmodSync as chmodSync2, closeSync, copyFileSync as copyFileSync2, existsSync as existsSync7, fchmodSync, fsyncSync, lstatSync as
|
|
830
|
+
import { spawnSync as spawnSync5 } from "node:child_process";
|
|
831
|
+
import { chmodSync as chmodSync2, closeSync, copyFileSync as copyFileSync2, existsSync as existsSync7, fchmodSync, fsyncSync, lstatSync as lstatSync8, mkdirSync as mkdirSync4, mkdtempSync, openSync, readFileSync as readFileSync9, realpathSync as realpathSync5, renameSync as renameSync3, rmSync as rmSync3, statSync as statSync4, unlinkSync as unlinkSync4, writeFileSync as writeFileSync4 } from "node:fs";
|
|
827
832
|
import { homedir as homedir3, tmpdir as tmpdir2 } from "node:os";
|
|
828
|
-
import { basename as basename6, delimiter as delimiter2, dirname as dirname10, isAbsolute as
|
|
833
|
+
import { basename as basename6, delimiter as delimiter2, dirname as dirname10, isAbsolute as isAbsolute6, join as join12, relative as relative7, resolve as resolve10, sep as sep2, win32 } from "node:path";
|
|
829
834
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
830
835
|
import YAML4 from "yaml";
|
|
831
836
|
function normalizeProjectType(value, label = "Project type") {
|
|
@@ -835,7 +840,7 @@ function normalizeProjectType(value, label = "Project type") {
|
|
|
835
840
|
}
|
|
836
841
|
function looksLikeObsidianPlugin(dir) {
|
|
837
842
|
try {
|
|
838
|
-
const manifest = JSON.parse(
|
|
843
|
+
const manifest = JSON.parse(readFileSync9(join12(dir, "manifest.json"), "utf8"));
|
|
839
844
|
return isRecord2(manifest) && typeof manifest.id === "string" && typeof manifest.minAppVersion === "string";
|
|
840
845
|
} catch {
|
|
841
846
|
return false;
|
|
@@ -843,10 +848,10 @@ function looksLikeObsidianPlugin(dir) {
|
|
|
843
848
|
}
|
|
844
849
|
function assertTemplateOffersProjectType(pjanglerRoot, projectType) {
|
|
845
850
|
if (projectType === "base") return;
|
|
846
|
-
const copierYml =
|
|
851
|
+
const copierYml = join12(pjanglerRoot, "templates", "commonproject", "copier.yml");
|
|
847
852
|
let choices;
|
|
848
853
|
try {
|
|
849
|
-
const parsed = YAML4.parse(
|
|
854
|
+
const parsed = YAML4.parse(readFileSync9(copierYml, "utf8"));
|
|
850
855
|
choices = isRecord2(parsed) && isRecord2(parsed.project_type) ? parsed.project_type.choices : void 0;
|
|
851
856
|
} catch {
|
|
852
857
|
choices = void 0;
|
|
@@ -857,7 +862,7 @@ function assertTemplateOffersProjectType(pjanglerRoot, projectType) {
|
|
|
857
862
|
}
|
|
858
863
|
}
|
|
859
864
|
function operatorDisplayName(cwd) {
|
|
860
|
-
const result2 =
|
|
865
|
+
const result2 = spawnSync5("git", ["config", "--get", "user.name"], { cwd: existsSync7(cwd) ? cwd : void 0, encoding: "utf8", timeout: 5e3 });
|
|
861
866
|
return result2.status === 0 ? result2.stdout.trim() : "";
|
|
862
867
|
}
|
|
863
868
|
function providerAssignsIdentifiers(type) {
|
|
@@ -869,9 +874,9 @@ function carriedAutomation(automation) {
|
|
|
869
874
|
return Object.keys(rest).length ? { automation: rest } : {};
|
|
870
875
|
}
|
|
871
876
|
function synchronizeCopierIdentity(manifestPath, manifest) {
|
|
872
|
-
const answersPath =
|
|
877
|
+
const answersPath = join12(dirname10(manifestPath), ".copier-answers.yml");
|
|
873
878
|
if (!existsSync7(answersPath)) return [];
|
|
874
|
-
const current =
|
|
879
|
+
const current = readFileSync9(answersPath, "utf8");
|
|
875
880
|
const document = YAML4.parseDocument(current);
|
|
876
881
|
if (document.errors.length) return [];
|
|
877
882
|
const name = String(document.get("project_name") ?? "");
|
|
@@ -888,12 +893,12 @@ function projectRegistryPath(env2 = process.env) {
|
|
|
888
893
|
return resolveRegistryLocation(void 0, env2);
|
|
889
894
|
}
|
|
890
895
|
function createSafeRecord(entries = []) {
|
|
891
|
-
const
|
|
892
|
-
for (const [key, value] of entries)
|
|
893
|
-
return
|
|
896
|
+
const record3 = /* @__PURE__ */ Object.create(null);
|
|
897
|
+
for (const [key, value] of entries) record3[key] = value;
|
|
898
|
+
return record3;
|
|
894
899
|
}
|
|
895
|
-
function getOwnRecordValue(
|
|
896
|
-
return Object.hasOwn(
|
|
900
|
+
function getOwnRecordValue(record3, key) {
|
|
901
|
+
return Object.hasOwn(record3, key) ? record3[key] : void 0;
|
|
897
902
|
}
|
|
898
903
|
function emptyProjectRegistry() {
|
|
899
904
|
return { schema_version: PROJECT_REGISTRY_SCHEMA_VERSION, projects: createSafeRecord() };
|
|
@@ -905,7 +910,7 @@ function loadProjectRegistry(path = projectRegistryPath()) {
|
|
|
905
910
|
return registry2;
|
|
906
911
|
}
|
|
907
912
|
if (!existsSync7(path)) return emptyProjectRegistry();
|
|
908
|
-
const raw = YAML4.parse(
|
|
913
|
+
const raw = YAML4.parse(readFileSync9(path, "utf8"));
|
|
909
914
|
if (raw == null) return emptyProjectRegistry();
|
|
910
915
|
if (!isRecord2(raw)) throw new Error(`Project registry must be a mapping: ${path}`);
|
|
911
916
|
const registry = raw;
|
|
@@ -995,10 +1000,10 @@ function saveProjectRegistry(registry, path = projectRegistryPath()) {
|
|
|
995
1000
|
let text;
|
|
996
1001
|
let mode = 420;
|
|
997
1002
|
if (existsSync7(path)) {
|
|
998
|
-
const stat =
|
|
1003
|
+
const stat = lstatSync8(path);
|
|
999
1004
|
if (!stat.isFile() || stat.isSymbolicLink()) throw new Error(`Project registry must be a regular file: ${path}`);
|
|
1000
1005
|
mode = stat.mode & 511;
|
|
1001
|
-
const current =
|
|
1006
|
+
const current = readFileSync9(path, "utf8");
|
|
1002
1007
|
const document = YAML4.parseDocument(current);
|
|
1003
1008
|
if (document.errors.length) throw new Error(`Project registry YAML is invalid: ${path}`);
|
|
1004
1009
|
setYamlLeaf(document, ["schema_version"], registry.schema_version);
|
|
@@ -1065,7 +1070,7 @@ function validateProjectRegistry(registry) {
|
|
|
1065
1070
|
validateProjectRecord(project, slug);
|
|
1066
1071
|
if (slugs.has(project.slug)) throw new Error(`Duplicate project slug: ${project.slug}`);
|
|
1067
1072
|
slugs.add(project.slug);
|
|
1068
|
-
const repoKey =
|
|
1073
|
+
const repoKey = resolve10(project.repo_path);
|
|
1069
1074
|
const existingRepoSlug = repoPaths.get(repoKey);
|
|
1070
1075
|
if (existingRepoSlug && existingRepoSlug !== slug) {
|
|
1071
1076
|
throw new Error(`Duplicate project repo_path: ${project.repo_path} used by ${existingRepoSlug} and ${slug}`);
|
|
@@ -1142,18 +1147,18 @@ function ticketProviderKeyVars(provider, workspace) {
|
|
|
1142
1147
|
return ticketProviderKeyGroups(provider, workspace).flat();
|
|
1143
1148
|
}
|
|
1144
1149
|
function ticketProviderSecretsPath(env2 = process.env) {
|
|
1145
|
-
const base = env2.XDG_CONFIG_HOME ||
|
|
1146
|
-
return
|
|
1150
|
+
const base = env2.XDG_CONFIG_HOME || join12(env2.HOME || homedir3(), ".config");
|
|
1151
|
+
return join12(base, "zshyzsh", "secrets.zsh");
|
|
1147
1152
|
}
|
|
1148
1153
|
function ticketProviderFleetEnvPath(env2 = process.env) {
|
|
1149
|
-
return env2.HERMES_FLEET_ENV?.trim() ||
|
|
1154
|
+
return env2.HERMES_FLEET_ENV?.trim() || join12(env2.HOME || homedir3(), ".hermes", "fleet.env");
|
|
1150
1155
|
}
|
|
1151
1156
|
function readShellAssignments(path, keys) {
|
|
1152
1157
|
const found = {};
|
|
1153
1158
|
if (!existsSync7(path)) return found;
|
|
1154
1159
|
let text;
|
|
1155
1160
|
try {
|
|
1156
|
-
text =
|
|
1161
|
+
text = readFileSync9(path, "utf8");
|
|
1157
1162
|
} catch {
|
|
1158
1163
|
return found;
|
|
1159
1164
|
}
|
|
@@ -1189,7 +1194,7 @@ function resolveTicketProviderCredentials(input) {
|
|
|
1189
1194
|
}
|
|
1190
1195
|
}
|
|
1191
1196
|
const candidates = [];
|
|
1192
|
-
if (input.repoPath) candidates.push(
|
|
1197
|
+
if (input.repoPath) candidates.push(join12(input.repoPath, ".env"));
|
|
1193
1198
|
candidates.push(ticketProviderFleetEnvPath(env2));
|
|
1194
1199
|
candidates.push(ticketProviderSecretsPath(env2));
|
|
1195
1200
|
for (const candidate of candidates) {
|
|
@@ -1207,19 +1212,19 @@ function resolveTicketProviderAdapter(provider, env2 = process.env) {
|
|
|
1207
1212
|
const file = `${provider}.sh`;
|
|
1208
1213
|
const candidates = [];
|
|
1209
1214
|
const override = env2[TICKET_PROVIDER_ADAPTERS_ENV];
|
|
1210
|
-
if (override) candidates.push(
|
|
1211
|
-
const relativeRoot =
|
|
1215
|
+
if (override) candidates.push(join12(override, file));
|
|
1216
|
+
const relativeRoot = join12("krebs", "adapters", "tp");
|
|
1212
1217
|
try {
|
|
1213
1218
|
let dir = dirname10(fileURLToPath2(import.meta.url));
|
|
1214
1219
|
for (let depth = 0; depth < 8; depth++) {
|
|
1215
|
-
candidates.push(
|
|
1220
|
+
candidates.push(join12(dir, relativeRoot, file));
|
|
1216
1221
|
const parent = dirname10(dir);
|
|
1217
1222
|
if (parent === dir) break;
|
|
1218
1223
|
dir = parent;
|
|
1219
1224
|
}
|
|
1220
1225
|
} catch {
|
|
1221
1226
|
}
|
|
1222
|
-
candidates.push(
|
|
1227
|
+
candidates.push(join12(homedir3(), "code", "33GOD", relativeRoot, file));
|
|
1223
1228
|
return candidates.find((candidate) => existsSync7(candidate));
|
|
1224
1229
|
}
|
|
1225
1230
|
function provisionTicketProviderBoard(action, env2 = process.env) {
|
|
@@ -1237,7 +1242,7 @@ function provisionTicketProviderBoard(action, env2 = process.env) {
|
|
|
1237
1242
|
ok: true,
|
|
1238
1243
|
skipped: true,
|
|
1239
1244
|
logs: [
|
|
1240
|
-
`ticket-provider: ${names} not set; skipping ${provider} board creation (state stays "planned"). Set it in the environment, ${
|
|
1245
|
+
`ticket-provider: ${names} not set; skipping ${provider} board creation (state stays "planned"). Set it in the environment, ${join12(action.repoPath, ".env")}, ${ticketProviderFleetEnvPath(env2)}, or ${ticketProviderSecretsPath(env2)} \u2014 or pass --board-id to link an existing board.`
|
|
1241
1246
|
]
|
|
1242
1247
|
};
|
|
1243
1248
|
}
|
|
@@ -1251,12 +1256,12 @@ function provisionTicketProviderBoard(action, env2 = process.env) {
|
|
|
1251
1256
|
};
|
|
1252
1257
|
}
|
|
1253
1258
|
const redact = (text) => Object.values(values).reduce((acc, secret) => secret ? acc.split(secret).join("***") : acc, text);
|
|
1254
|
-
const staging = mkdtempSync(
|
|
1259
|
+
const staging = mkdtempSync(join12(tmpdir2(), "pjangler-tp-"));
|
|
1255
1260
|
try {
|
|
1256
|
-
const providersDir =
|
|
1261
|
+
const providersDir = join12(staging, ".tp", "adapters");
|
|
1257
1262
|
mkdirSync4(providersDir, { recursive: true });
|
|
1258
1263
|
writeFileSync4(
|
|
1259
|
-
|
|
1264
|
+
join12(staging, ".project.json"),
|
|
1260
1265
|
`${JSON.stringify(
|
|
1261
1266
|
{
|
|
1262
1267
|
project_name: action.boardName,
|
|
@@ -1275,11 +1280,11 @@ function provisionTicketProviderBoard(action, env2 = process.env) {
|
|
|
1275
1280
|
`,
|
|
1276
1281
|
"utf8"
|
|
1277
1282
|
);
|
|
1278
|
-
const staged =
|
|
1283
|
+
const staged = join12(providersDir, `${provider}.sh`);
|
|
1279
1284
|
copyFileSync2(adapter, staged);
|
|
1280
1285
|
const childEnv = { ...env2, ...values, TICKET_PROVIDER: provider };
|
|
1281
1286
|
if (provider === "plane" && action.workspace) childEnv.PLANE_WORKSPACE = action.workspace;
|
|
1282
|
-
const result2 =
|
|
1287
|
+
const result2 = spawnSync5("sh", [staged, "create_board", action.boardName, action.identifier, action.description], {
|
|
1283
1288
|
cwd: existsSync7(action.repoPath) ? action.repoPath : staging,
|
|
1284
1289
|
encoding: "utf8",
|
|
1285
1290
|
env: childEnv
|
|
@@ -1350,7 +1355,7 @@ function slugifyProjectName(value) {
|
|
|
1350
1355
|
}
|
|
1351
1356
|
function validateSafePathSegment(value, label) {
|
|
1352
1357
|
const normalized = value.trim();
|
|
1353
|
-
const unsafe = !normalized || normalized !== value || normalized === "." || normalized === ".." ||
|
|
1358
|
+
const unsafe = !normalized || normalized !== value || normalized === "." || normalized === ".." || isAbsolute6(normalized) || win32.isAbsolute(normalized) || normalized.includes("/") || normalized.includes("\\") || !SAFE_PATH_SEGMENT.test(normalized);
|
|
1354
1359
|
if (unsafe) {
|
|
1355
1360
|
throw new Error(
|
|
1356
1361
|
`${label} must be a non-empty safe single path segment using letters, numbers, dots, underscores, or hyphens (no dot segments, absolute paths, separators, or traversal)`
|
|
@@ -1359,24 +1364,24 @@ function validateSafePathSegment(value, label) {
|
|
|
1359
1364
|
return normalized;
|
|
1360
1365
|
}
|
|
1361
1366
|
function prospectiveRealPath(path) {
|
|
1362
|
-
let cursor =
|
|
1367
|
+
let cursor = resolve10(path);
|
|
1363
1368
|
const suffix = [];
|
|
1364
1369
|
while (!existsSync7(cursor)) {
|
|
1365
1370
|
const parent = dirname10(cursor);
|
|
1366
|
-
if (parent === cursor) return
|
|
1371
|
+
if (parent === cursor) return resolve10(path);
|
|
1367
1372
|
suffix.unshift(basename6(cursor));
|
|
1368
1373
|
cursor = parent;
|
|
1369
1374
|
}
|
|
1370
|
-
return
|
|
1375
|
+
return resolve10(realpathSync5(cursor), ...suffix);
|
|
1371
1376
|
}
|
|
1372
1377
|
function resolveContainedPath(parentDir, candidate, label) {
|
|
1373
1378
|
const physicalParent = prospectiveRealPath(parentDir);
|
|
1374
1379
|
const physicalCandidate = prospectiveRealPath(candidate);
|
|
1375
1380
|
const fromParent = relative7(physicalParent, physicalCandidate);
|
|
1376
|
-
if (!fromParent || fromParent === ".." || fromParent.startsWith(`..${sep2}`) ||
|
|
1377
|
-
throw new Error(`${label} must remain contained beneath parent directory ${
|
|
1381
|
+
if (!fromParent || fromParent === ".." || fromParent.startsWith(`..${sep2}`) || isAbsolute6(fromParent)) {
|
|
1382
|
+
throw new Error(`${label} must remain contained beneath parent directory ${resolve10(parentDir)}`);
|
|
1378
1383
|
}
|
|
1379
|
-
return
|
|
1384
|
+
return resolve10(candidate);
|
|
1380
1385
|
}
|
|
1381
1386
|
function proposeProjectIdentifier(value) {
|
|
1382
1387
|
const compact = value.replace(/[^A-Za-z0-9]/g, "").toUpperCase();
|
|
@@ -1391,7 +1396,7 @@ function resolveAgentHooksLayer2(input, env2 = process.env) {
|
|
|
1391
1396
|
const override = env2.PJ_AGENT_HOOKS_LAYER;
|
|
1392
1397
|
if (override === "0" || override === "false") return false;
|
|
1393
1398
|
if (override === "1" || override === "true") return true;
|
|
1394
|
-
return !existsSync7(
|
|
1399
|
+
return !existsSync7(join12(homedir3(), ".agents", "hooks"));
|
|
1395
1400
|
}
|
|
1396
1401
|
function jsonStable(value) {
|
|
1397
1402
|
return JSON.stringify(value, (_key, item) => isRecord2(item) ? Object.fromEntries(Object.keys(item).sort().map((key) => [key, item[key]])) : item);
|
|
@@ -1405,14 +1410,14 @@ function projectRecordEquivalent(a, b) {
|
|
|
1405
1410
|
function defaultProjectTargetDir(name, cwd = process.cwd()) {
|
|
1406
1411
|
const compactName = name.replace(/[^A-Za-z0-9._-]/g, "");
|
|
1407
1412
|
const safeName = SAFE_PATH_SEGMENT.test(compactName) ? compactName : slugifyProjectName(name);
|
|
1408
|
-
return
|
|
1413
|
+
return resolve10(dirname10(resolve10(cwd)), validateSafePathSegment(safeName, "Generated project directory"));
|
|
1409
1414
|
}
|
|
1410
1415
|
function sourceSkillRoots(env2 = process.env) {
|
|
1411
1416
|
const configuredRoots = (env2[PROJECT_SOURCE_SKILL_ROOTS_ENV] || "").split(delimiter2).map((root) => root.trim()).filter(Boolean);
|
|
1412
1417
|
const seen = /* @__PURE__ */ new Set();
|
|
1413
1418
|
const roots = [];
|
|
1414
1419
|
for (const root of [...DEFAULT_SOURCE_SKILL_ROOTS, ...configuredRoots]) {
|
|
1415
|
-
const normalized =
|
|
1420
|
+
const normalized = resolve10(expandHome(root));
|
|
1416
1421
|
if (seen.has(normalized)) continue;
|
|
1417
1422
|
seen.add(normalized);
|
|
1418
1423
|
roots.push(normalized);
|
|
@@ -1422,12 +1427,12 @@ function sourceSkillRoots(env2 = process.env) {
|
|
|
1422
1427
|
function resolveSourceSkillPath(sourceSkill, env2 = process.env) {
|
|
1423
1428
|
if (!sourceSkill) return void 0;
|
|
1424
1429
|
const expanded = expandHome(sourceSkill);
|
|
1425
|
-
const direct =
|
|
1430
|
+
const direct = resolve10(expanded);
|
|
1426
1431
|
if (existsSync7(direct)) return direct;
|
|
1427
1432
|
const name = basename6(sourceSkill);
|
|
1428
1433
|
const roots = sourceSkillRoots(env2);
|
|
1429
1434
|
for (const root of roots) {
|
|
1430
|
-
const candidate =
|
|
1435
|
+
const candidate = join12(root, name);
|
|
1431
1436
|
if (existsSync7(candidate)) return candidate;
|
|
1432
1437
|
}
|
|
1433
1438
|
const searched = roots.length ? ` Searched roots: ${roots.join(", ")}.` : "";
|
|
@@ -1436,9 +1441,9 @@ function resolveSourceSkillPath(sourceSkill, env2 = process.env) {
|
|
|
1436
1441
|
}
|
|
1437
1442
|
function planProjectInit(input) {
|
|
1438
1443
|
if (!input.name.trim()) throw new Error("Project name is required");
|
|
1439
|
-
const targetDir =
|
|
1440
|
-
const localManifestPath =
|
|
1441
|
-
const localManifestContent = existsSync7(localManifestPath) ?
|
|
1444
|
+
const targetDir = resolve10(input.targetDir ?? defaultProjectTargetDir(input.name, input.cwd));
|
|
1445
|
+
const localManifestPath = join12(targetDir, ".project.json");
|
|
1446
|
+
const localManifestContent = existsSync7(localManifestPath) ? readFileSync9(localManifestPath, "utf8") : void 0;
|
|
1442
1447
|
const localManifest = localManifestContent === void 0 ? void 0 : JSON.parse(localManifestContent);
|
|
1443
1448
|
const localId = localManifest?.project_id ?? localManifest?.project_slug;
|
|
1444
1449
|
const slug = normalizeProjectId(input.projectSlug ?? localId ?? slugifyProjectName(input.name));
|
|
@@ -1466,7 +1471,7 @@ function planProjectInit(input) {
|
|
|
1466
1471
|
const recordedTypeValue = existing?.template?.commonproject?.project_type;
|
|
1467
1472
|
const recordedType = recordedTypeValue === void 0 ? void 0 : normalizeProjectType(recordedTypeValue, `Recorded project type of ${slug}`);
|
|
1468
1473
|
const requestedType = input.projectType === void 0 ? void 0 : normalizeProjectType(input.projectType);
|
|
1469
|
-
const freshRender = scaffold && localManifest === void 0 && !existsSync7(
|
|
1474
|
+
const freshRender = scaffold && localManifest === void 0 && !existsSync7(join12(targetDir, ".copier-answers.yml"));
|
|
1470
1475
|
if (requestedType && !freshRender && requestedType !== (recordedType ?? "base")) {
|
|
1471
1476
|
const adoptingPlugin = !scaffold && recordedType === void 0 && requestedType === "obsidian-plugin" && looksLikeObsidianPlugin(targetDir);
|
|
1472
1477
|
if (!adoptingPlugin) {
|
|
@@ -1532,7 +1537,7 @@ function planProjectInit(input) {
|
|
|
1532
1537
|
updated_at: projectRecordEquivalent(existing, candidateProject) ? existing.updated_at : now
|
|
1533
1538
|
};
|
|
1534
1539
|
validateNoDuplicateProject(registry, project, overwrite);
|
|
1535
|
-
const pjanglerRoot =
|
|
1540
|
+
const pjanglerRoot = resolve10(input.pjanglerRoot ?? resolvePjanglerRoot());
|
|
1536
1541
|
const manifest = projectManifestFromRegistryProject(project);
|
|
1537
1542
|
const apply = input.apply ?? false;
|
|
1538
1543
|
const live = input.live ?? false;
|
|
@@ -1564,7 +1569,7 @@ function planProjectInit(input) {
|
|
|
1564
1569
|
}));
|
|
1565
1570
|
}
|
|
1566
1571
|
actions.push(
|
|
1567
|
-
{ kind: "project.write-manifest", path:
|
|
1572
|
+
{ kind: "project.write-manifest", path: join12(targetDir, ".project.json"), manifest, expectedContent: localManifestContent },
|
|
1568
1573
|
{
|
|
1569
1574
|
kind: "ticket-provider.create-or-link",
|
|
1570
1575
|
enabled: boardEnabled,
|
|
@@ -1619,12 +1624,12 @@ function linkTicketProviderBoard(plan, action, boardId, identifier, now = /* @__
|
|
|
1619
1624
|
plan.manifest.ticket_provider = manifestProvider;
|
|
1620
1625
|
action.boardId = boardId;
|
|
1621
1626
|
action.state = manifestProvider.state;
|
|
1622
|
-
const manifestPath =
|
|
1627
|
+
const manifestPath = join12(action.repoPath, ".project.json");
|
|
1623
1628
|
let next;
|
|
1624
1629
|
if (existsSync7(manifestPath)) {
|
|
1625
1630
|
let existing = {};
|
|
1626
1631
|
try {
|
|
1627
|
-
const parsed = JSON.parse(
|
|
1632
|
+
const parsed = JSON.parse(readFileSync9(manifestPath, "utf8"));
|
|
1628
1633
|
if (isRecord2(parsed)) existing = parsed;
|
|
1629
1634
|
} catch {
|
|
1630
1635
|
existing = {};
|
|
@@ -1637,7 +1642,7 @@ function linkTicketProviderBoard(plan, action, boardId, identifier, now = /* @__
|
|
|
1637
1642
|
}
|
|
1638
1643
|
const text = `${JSON.stringify(next, null, 2)}
|
|
1639
1644
|
`;
|
|
1640
|
-
if (!existsSync7(manifestPath) ||
|
|
1645
|
+
if (!existsSync7(manifestPath) || readFileSync9(manifestPath, "utf8") !== text) {
|
|
1641
1646
|
writeFileSync4(manifestPath, text, "utf8");
|
|
1642
1647
|
return [manifestPath];
|
|
1643
1648
|
}
|
|
@@ -1677,7 +1682,7 @@ async function executeProjectInitPlan(plan, options = {}) {
|
|
|
1677
1682
|
copierEnv.PYTHONSAFEPATH = "1";
|
|
1678
1683
|
}
|
|
1679
1684
|
copierEnv.MISE_YES = "1";
|
|
1680
|
-
const result2 =
|
|
1685
|
+
const result2 = spawnSync5(copierExecutable, action.command.slice(1), {
|
|
1681
1686
|
encoding: "utf8",
|
|
1682
1687
|
cwd: action.cwd,
|
|
1683
1688
|
env: copierEnv
|
|
@@ -1698,14 +1703,14 @@ async function executeProjectInitPlan(plan, options = {}) {
|
|
|
1698
1703
|
break;
|
|
1699
1704
|
}
|
|
1700
1705
|
const projectType = action.data.project_type ?? "base";
|
|
1701
|
-
const missing = (PROJECT_TYPE_SENTINELS[projectType] ?? []).filter((rel) => !existsSync7(
|
|
1706
|
+
const missing = (PROJECT_TYPE_SENTINELS[projectType] ?? []).filter((rel) => !existsSync7(join12(action.targetDir, rel)));
|
|
1702
1707
|
if (missing.length) {
|
|
1703
1708
|
errors.push(`copier rendered no ${projectType} project: ${missing.join(", ")} missing from ${action.targetDir}`);
|
|
1704
1709
|
break;
|
|
1705
1710
|
}
|
|
1706
1711
|
if (projectType !== "base") logs.push(`commonproject: rendered as ${projectType}`);
|
|
1707
1712
|
} else if (action.kind === "project.write-manifest") {
|
|
1708
|
-
if (action.expectedContent !== void 0 && (!existsSync7(action.path) ||
|
|
1713
|
+
if (action.expectedContent !== void 0 && (!existsSync7(action.path) || readFileSync9(action.path, "utf8") !== action.expectedContent)) {
|
|
1709
1714
|
errors.push(`Manifest changed after planning: ${action.path}; re-plan before applying`);
|
|
1710
1715
|
break;
|
|
1711
1716
|
}
|
|
@@ -1713,7 +1718,7 @@ async function executeProjectInitPlan(plan, options = {}) {
|
|
|
1713
1718
|
let value = action.manifest;
|
|
1714
1719
|
if (existsSync7(action.path)) {
|
|
1715
1720
|
try {
|
|
1716
|
-
const currentValue = JSON.parse(
|
|
1721
|
+
const currentValue = JSON.parse(readFileSync9(action.path, "utf8"));
|
|
1717
1722
|
if (isRecord2(currentValue)) {
|
|
1718
1723
|
const currentNotebook = isRecord2(currentValue.notebook) ? currentValue.notebook : {};
|
|
1719
1724
|
const desiredNotebook = isRecord2(value.notebook) ? value.notebook : {};
|
|
@@ -1741,7 +1746,7 @@ async function executeProjectInitPlan(plan, options = {}) {
|
|
|
1741
1746
|
value.project_id = normalizeProjectId(value.project_id ?? action.manifest.project_id);
|
|
1742
1747
|
const next = `${JSON.stringify(value, null, 2)}
|
|
1743
1748
|
`;
|
|
1744
|
-
const current = existsSync7(action.path) ?
|
|
1749
|
+
const current = existsSync7(action.path) ? readFileSync9(action.path, "utf8") : void 0;
|
|
1745
1750
|
if (current !== next) {
|
|
1746
1751
|
writeFileSync4(action.path, next, "utf8");
|
|
1747
1752
|
changedFiles.push(action.path);
|
|
@@ -1824,7 +1829,7 @@ function getProject(registry, slug) {
|
|
|
1824
1829
|
return project;
|
|
1825
1830
|
}
|
|
1826
1831
|
function buildCommonProjectCopierAction(input) {
|
|
1827
|
-
const templateDir =
|
|
1832
|
+
const templateDir = join12(input.pjanglerRoot, "templates", "commonproject");
|
|
1828
1833
|
const data = {
|
|
1829
1834
|
project_name: input.projectName,
|
|
1830
1835
|
project_description: input.projectDescription ?? "",
|
|
@@ -1855,19 +1860,19 @@ function buildCommonProjectCopierAction(input) {
|
|
|
1855
1860
|
function resolvePjanglerRoot() {
|
|
1856
1861
|
let dir = dirname10(fileURLToPath2(import.meta.url));
|
|
1857
1862
|
while (dir !== dirname10(dir)) {
|
|
1858
|
-
if (existsSync7(
|
|
1863
|
+
if (existsSync7(join12(dir, "package.json")) && existsSync7(join12(dir, "templates", "commonproject", "copier.yml"))) return dir;
|
|
1859
1864
|
dir = dirname10(dir);
|
|
1860
1865
|
}
|
|
1861
|
-
return
|
|
1866
|
+
return resolve10(process.cwd());
|
|
1862
1867
|
}
|
|
1863
1868
|
function validateNoDuplicateProject(registry, project, overwrite) {
|
|
1864
1869
|
const existingSameSlug = getOwnRecordValue(registry.projects, project.slug);
|
|
1865
|
-
if (existingSameSlug && !overwrite &&
|
|
1870
|
+
if (existingSameSlug && !overwrite && resolve10(existingSameSlug.repo_path) !== resolve10(project.repo_path)) {
|
|
1866
1871
|
throw new Error(`Project slug already exists in registry: ${project.slug}`);
|
|
1867
1872
|
}
|
|
1868
1873
|
for (const [slug, existing] of Object.entries(registry.projects)) {
|
|
1869
1874
|
if (slug === project.slug) continue;
|
|
1870
|
-
if (
|
|
1875
|
+
if (resolve10(existing.repo_path) === resolve10(project.repo_path)) {
|
|
1871
1876
|
throw new Error(`Project repo_path already registered by ${slug}: ${project.repo_path}`);
|
|
1872
1877
|
}
|
|
1873
1878
|
if (ticketProviderScope(existing.ticket_provider) !== ticketProviderScope(project.ticket_provider)) continue;
|
|
@@ -1932,7 +1937,7 @@ function validateProjectRecord(project, key) {
|
|
|
1932
1937
|
}
|
|
1933
1938
|
function expandHome(path) {
|
|
1934
1939
|
if (path === "~") return homedir3();
|
|
1935
|
-
if (path.startsWith("~/")) return
|
|
1940
|
+
if (path.startsWith("~/")) return join12(homedir3(), path.slice(2));
|
|
1936
1941
|
return path;
|
|
1937
1942
|
}
|
|
1938
1943
|
function isRecord2(value) {
|
|
@@ -1959,7 +1964,7 @@ var init_project = __esm({
|
|
|
1959
1964
|
"obsidian-plugin": ["typescript", "javascript"]
|
|
1960
1965
|
};
|
|
1961
1966
|
PROJECT_TYPE_SENTINELS = {
|
|
1962
|
-
"obsidian-plugin": ["manifest.json",
|
|
1967
|
+
"obsidian-plugin": ["manifest.json", join12("src", "main.ts")]
|
|
1963
1968
|
};
|
|
1964
1969
|
BOARD_URL_DEPRECATION_WARNING = "boardUrl is deprecated and ignored; board URLs are derived at runtime and are never persisted.";
|
|
1965
1970
|
PROJECT_IDENTIFIER_SOURCES = ["provider", "proposed"];
|
|
@@ -1968,8 +1973,8 @@ var init_project = __esm({
|
|
|
1968
1973
|
IDENTIFIER_REPAIR_COMMAND = "pj project identity --all --apply";
|
|
1969
1974
|
DEFAULT_SOURCE_SKILL_ROOTS = [
|
|
1970
1975
|
"/home/delorenj/code/skillex/all-skills",
|
|
1971
|
-
|
|
1972
|
-
|
|
1976
|
+
join12(homedir3(), ".agents", "skills"),
|
|
1977
|
+
join12(homedir3(), ".codex", "skills")
|
|
1973
1978
|
];
|
|
1974
1979
|
PROJECT_REGISTRY_OWNED_KEYS = [
|
|
1975
1980
|
"name",
|
|
@@ -2001,21 +2006,21 @@ var init_project = __esm({
|
|
|
2001
2006
|
import { randomUUID } from "node:crypto";
|
|
2002
2007
|
import { isDeepStrictEqual } from "node:util";
|
|
2003
2008
|
import { isIP as isIP2 } from "node:net";
|
|
2004
|
-
import { closeSync as closeSync2, existsSync as existsSync8, fchmodSync as fchmodSync2, fsyncSync as fsyncSync2, lstatSync as
|
|
2005
|
-
import { dirname as dirname11, resolve as
|
|
2009
|
+
import { closeSync as closeSync2, existsSync as existsSync8, fchmodSync as fchmodSync2, fsyncSync as fsyncSync2, lstatSync as lstatSync9, openSync as openSync2, readFileSync as readFileSync10, realpathSync as realpathSync6, renameSync as renameSync4, writeFileSync as writeFileSync5 } from "node:fs";
|
|
2010
|
+
import { dirname as dirname11, resolve as resolve11 } from "node:path";
|
|
2006
2011
|
function isRecord3(value) {
|
|
2007
2012
|
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
2008
2013
|
}
|
|
2009
2014
|
function realOrResolved(path) {
|
|
2010
|
-
const absolute =
|
|
2015
|
+
const absolute = resolve11(path);
|
|
2011
2016
|
return existsSync8(absolute) ? realpathSync6(absolute) : absolute;
|
|
2012
2017
|
}
|
|
2013
2018
|
function readManifest(repoPath) {
|
|
2014
|
-
const path =
|
|
2019
|
+
const path = resolve11(repoPath, ".project.json");
|
|
2015
2020
|
if (!existsSync8(path)) return null;
|
|
2016
2021
|
let parsed;
|
|
2017
2022
|
try {
|
|
2018
|
-
parsed = JSON.parse(
|
|
2023
|
+
parsed = JSON.parse(readFileSync10(path, "utf8"));
|
|
2019
2024
|
} catch {
|
|
2020
2025
|
throw new NotebookError("INVALID_INPUT", `${path} is not valid JSON`);
|
|
2021
2026
|
}
|
|
@@ -2255,9 +2260,9 @@ function registryProjectBySlug(registry, slug) {
|
|
|
2255
2260
|
}
|
|
2256
2261
|
function persistProjectNotebookBinding(resolved, binding, policy) {
|
|
2257
2262
|
const changed = [];
|
|
2258
|
-
const manifestPath =
|
|
2263
|
+
const manifestPath = resolve11(resolved.project.repo_path, ".project.json");
|
|
2259
2264
|
if (isRegistryServiceLocation(resolved.registry_path)) {
|
|
2260
|
-
const before = existsSync8(manifestPath) ?
|
|
2265
|
+
const before = existsSync8(manifestPath) ? readFileSync10(manifestPath, "utf8") : null;
|
|
2261
2266
|
const declaredPolicy = resolved.manifest?.notebook?.policy ?? {};
|
|
2262
2267
|
const nextProject = {
|
|
2263
2268
|
...resolved.project,
|
|
@@ -2269,10 +2274,10 @@ function persistProjectNotebookBinding(resolved, binding, policy) {
|
|
|
2269
2274
|
resolved.registry = loadProjectRegistry(resolved.registry_path);
|
|
2270
2275
|
resolved.project = registryProjectBySlug(resolved.registry, nextProject.slug);
|
|
2271
2276
|
resolved.manifest = readManifest(nextProject.repo_path);
|
|
2272
|
-
const after = existsSync8(manifestPath) ?
|
|
2277
|
+
const after = existsSync8(manifestPath) ? readFileSync10(manifestPath, "utf8") : null;
|
|
2273
2278
|
return before === after ? [] : [manifestPath];
|
|
2274
2279
|
}
|
|
2275
|
-
const manifestRaw = existsSync8(manifestPath) ? JSON.parse(
|
|
2280
|
+
const manifestRaw = existsSync8(manifestPath) ? JSON.parse(readFileSync10(manifestPath, "utf8")) : {};
|
|
2276
2281
|
if (!isRecord3(manifestRaw)) throw new NotebookError("INVALID_INPUT", `${manifestPath} must contain a JSON object`);
|
|
2277
2282
|
validateManifestNotebookSurface(manifestRaw.notebook);
|
|
2278
2283
|
const manifestNotebook2 = isRecord3(manifestRaw.notebook) ? manifestRaw.notebook : {};
|
|
@@ -2305,14 +2310,14 @@ function persistProjectNotebookBinding(resolved, binding, policy) {
|
|
|
2305
2310
|
};
|
|
2306
2311
|
const manifestText = `${JSON.stringify(manifestNext, null, 2)}
|
|
2307
2312
|
`;
|
|
2308
|
-
if (!existsSync8(manifestPath) ||
|
|
2313
|
+
if (!existsSync8(manifestPath) || readFileSync10(manifestPath, "utf8") !== manifestText) {
|
|
2309
2314
|
let mode = 420;
|
|
2310
2315
|
if (existsSync8(manifestPath)) {
|
|
2311
|
-
const current2 =
|
|
2316
|
+
const current2 = lstatSync9(manifestPath);
|
|
2312
2317
|
if (!current2.isFile() || current2.isSymbolicLink()) throw new NotebookError("CONFLICT", `${manifestPath} must be a regular non-symlink file`);
|
|
2313
2318
|
mode = current2.mode & 511;
|
|
2314
2319
|
}
|
|
2315
|
-
const temp =
|
|
2320
|
+
const temp = resolve11(dirname11(manifestPath), `.${process.pid}.${randomUUID()}.project.json.tmp`);
|
|
2316
2321
|
const fd = openSync2(temp, "wx", mode);
|
|
2317
2322
|
try {
|
|
2318
2323
|
writeFileSync5(fd, manifestText, "utf8");
|
|
@@ -2336,9 +2341,9 @@ function persistProjectNotebookBinding(resolved, binding, policy) {
|
|
|
2336
2341
|
const current = resolved.project.notebook;
|
|
2337
2342
|
resolved.project.notebook = { ...isRecord3(current) ? current : { state: "planned" }, ...binding };
|
|
2338
2343
|
const fileRegistry = !isRegistryServiceLocation(resolved.registry_path);
|
|
2339
|
-
const registryBefore = fileRegistry && existsSync8(resolved.registry_path) ?
|
|
2344
|
+
const registryBefore = fileRegistry && existsSync8(resolved.registry_path) ? readFileSync10(resolved.registry_path, "utf8") : null;
|
|
2340
2345
|
saveProjectRegistry(resolved.registry, resolved.registry_path);
|
|
2341
|
-
const registryAfter = fileRegistry && existsSync8(resolved.registry_path) ?
|
|
2346
|
+
const registryAfter = fileRegistry && existsSync8(resolved.registry_path) ? readFileSync10(resolved.registry_path, "utf8") : null;
|
|
2342
2347
|
if (registryBefore !== registryAfter) changed.push(resolved.registry_path);
|
|
2343
2348
|
resolved.manifest = manifestNext;
|
|
2344
2349
|
return changed;
|
|
@@ -2380,8 +2385,8 @@ function canonicalJson2(value) {
|
|
|
2380
2385
|
return encoded;
|
|
2381
2386
|
}
|
|
2382
2387
|
if (Array.isArray(value)) return `[${value.map((item) => item === void 0 ? "null" : canonicalJson2(item)).join(",")}]`;
|
|
2383
|
-
const
|
|
2384
|
-
return `{${Object.keys(
|
|
2388
|
+
const record3 = value;
|
|
2389
|
+
return `{${Object.keys(record3).filter((key) => record3[key] !== void 0).sort().map((key) => `${JSON.stringify(key)}:${canonicalJson2(record3[key])}`).join(",")}}`;
|
|
2385
2390
|
}
|
|
2386
2391
|
function sha256Hex(value) {
|
|
2387
2392
|
return createHash5("sha256").update(value).digest("hex");
|
|
@@ -2649,7 +2654,7 @@ import {
|
|
|
2649
2654
|
fchmodSync as fchmodSync3,
|
|
2650
2655
|
fstatSync,
|
|
2651
2656
|
fsyncSync as fsyncSync3,
|
|
2652
|
-
lstatSync as
|
|
2657
|
+
lstatSync as lstatSync10,
|
|
2653
2658
|
mkdirSync as mkdirSync5,
|
|
2654
2659
|
openSync as openSync3,
|
|
2655
2660
|
readSync,
|
|
@@ -2659,26 +2664,26 @@ import {
|
|
|
2659
2664
|
writeFileSync as writeFileSync6
|
|
2660
2665
|
} from "node:fs";
|
|
2661
2666
|
import { homedir as homedir4 } from "node:os";
|
|
2662
|
-
import { basename as basename7, dirname as dirname12, join as
|
|
2667
|
+
import { basename as basename7, dirname as dirname12, join as join13, parse, relative as relative8, resolve as resolve12, sep as sep3 } from "node:path";
|
|
2663
2668
|
function notebookStateRoot(env2 = process.env) {
|
|
2664
|
-
const base = env2.XDG_STATE_HOME ||
|
|
2665
|
-
return
|
|
2669
|
+
const base = env2.XDG_STATE_HOME || join13(env2.HOME || homedir4(), ".local", "state");
|
|
2670
|
+
return resolve12(base, "pjangler", "notebook", NOTEBOOK_STATE_VERSION);
|
|
2666
2671
|
}
|
|
2667
2672
|
function assertDigest(value, label) {
|
|
2668
2673
|
if (!SESSION_KEY_RE.test(value)) throw new NotebookError("INVALID_INPUT", `Invalid ${label}`);
|
|
2669
2674
|
}
|
|
2670
2675
|
function projectStateDir(root, projectSlug) {
|
|
2671
2676
|
if (!/^[a-z0-9][a-z0-9._-]{0,127}$/iu.test(projectSlug)) throw new NotebookError("INVALID_INPUT", "Invalid project slug for Notebook state");
|
|
2672
|
-
return
|
|
2677
|
+
return join13(resolve12(root), "projects", sha256Hex(`pjangler-project-state-v1\0${projectSlug}`));
|
|
2673
2678
|
}
|
|
2674
2679
|
function assertContained(root, candidate) {
|
|
2675
|
-
const rel = relative8(
|
|
2680
|
+
const rel = relative8(resolve12(root), resolve12(candidate));
|
|
2676
2681
|
if (!rel || !rel.startsWith(`..${sep3}`) && rel !== ".." && !rel.startsWith(sep3)) return;
|
|
2677
2682
|
throw new NotebookError("INTERNAL_ERROR", "Notebook state path escaped its root");
|
|
2678
2683
|
}
|
|
2679
2684
|
function openPinnedDirectory(path, root, create) {
|
|
2680
|
-
const absolute =
|
|
2681
|
-
const absoluteRoot =
|
|
2685
|
+
const absolute = resolve12(path);
|
|
2686
|
+
const absoluteRoot = resolve12(root);
|
|
2682
2687
|
assertContained(absoluteRoot, absolute);
|
|
2683
2688
|
if (!existsSync9("/proc/self/fd")) throw new NotebookError("INTERNAL_ERROR", "Descriptor-pinned Notebook state requires procfs");
|
|
2684
2689
|
const parsed = parse(absolute);
|
|
@@ -2687,7 +2692,7 @@ function openPinnedDirectory(path, root, create) {
|
|
|
2687
2692
|
try {
|
|
2688
2693
|
for (const part of absolute.slice(parsed.root.length).split(sep3).filter(Boolean)) {
|
|
2689
2694
|
const child = `/proc/self/fd/${fd}/${part}`;
|
|
2690
|
-
cursor =
|
|
2695
|
+
cursor = join13(cursor, part);
|
|
2691
2696
|
let childFd;
|
|
2692
2697
|
try {
|
|
2693
2698
|
childFd = openSync3(child, DIRECTORY_OPEN_FLAGS);
|
|
@@ -2724,8 +2729,8 @@ function assertOwned(stat) {
|
|
|
2724
2729
|
}
|
|
2725
2730
|
}
|
|
2726
2731
|
function ensureDirectory(path, root) {
|
|
2727
|
-
const absoluteRoot =
|
|
2728
|
-
const absolute =
|
|
2732
|
+
const absoluteRoot = resolve12(root);
|
|
2733
|
+
const absolute = resolve12(path);
|
|
2729
2734
|
assertContained(absoluteRoot, absolute);
|
|
2730
2735
|
const fd = openPinnedDirectory(absolute, absoluteRoot, true);
|
|
2731
2736
|
try {
|
|
@@ -2739,23 +2744,23 @@ function ensureDirectory(path, root) {
|
|
|
2739
2744
|
}
|
|
2740
2745
|
}
|
|
2741
2746
|
function notebookStatePaths(root, projectSlug) {
|
|
2742
|
-
const absoluteRoot =
|
|
2747
|
+
const absoluteRoot = resolve12(root);
|
|
2743
2748
|
const project = projectStateDir(absoluteRoot, projectSlug);
|
|
2744
2749
|
return {
|
|
2745
2750
|
root: absoluteRoot,
|
|
2746
2751
|
project,
|
|
2747
|
-
baselines:
|
|
2748
|
-
claims:
|
|
2749
|
-
receipts:
|
|
2750
|
-
refusals:
|
|
2751
|
-
journals:
|
|
2752
|
-
locks:
|
|
2752
|
+
baselines: join13(project, "baselines"),
|
|
2753
|
+
claims: join13(project, "claims"),
|
|
2754
|
+
receipts: join13(project, "receipts"),
|
|
2755
|
+
refusals: join13(project, "refusals"),
|
|
2756
|
+
journals: join13(project, "journals"),
|
|
2757
|
+
locks: join13(project, "locks")
|
|
2753
2758
|
};
|
|
2754
2759
|
}
|
|
2755
2760
|
function ensureNotebookState(root, projectSlug) {
|
|
2756
2761
|
const paths = notebookStatePaths(root, projectSlug);
|
|
2757
2762
|
const { root: absoluteRoot, project } = paths;
|
|
2758
|
-
for (const path of [absoluteRoot,
|
|
2763
|
+
for (const path of [absoluteRoot, join13(absoluteRoot, "projects"), project, paths.baselines, paths.claims, paths.receipts, paths.refusals, paths.journals, paths.locks]) {
|
|
2759
2764
|
ensureDirectory(path, absoluteRoot);
|
|
2760
2765
|
}
|
|
2761
2766
|
return paths;
|
|
@@ -2823,7 +2828,7 @@ function renameStateFile(source, target, root) {
|
|
|
2823
2828
|
if (!stat.isFile() || (stat.mode & 511) !== 384) throw new NotebookError("CONFLICT", "Notebook state rename source has an integrity finding");
|
|
2824
2829
|
assertOwned(stat);
|
|
2825
2830
|
try {
|
|
2826
|
-
|
|
2831
|
+
lstatSync10(targetPath);
|
|
2827
2832
|
throw new NotebookError("CONFLICT", "Notebook state rename target already exists");
|
|
2828
2833
|
} catch (error) {
|
|
2829
2834
|
if (error.code !== "ENOENT") throw error;
|
|
@@ -2868,7 +2873,7 @@ function atomicWriteJson(path, value, root, afterParentPinned) {
|
|
|
2868
2873
|
closeSync3(fd);
|
|
2869
2874
|
}
|
|
2870
2875
|
try {
|
|
2871
|
-
const current =
|
|
2876
|
+
const current = lstatSync10(target);
|
|
2872
2877
|
if (!existingIdentity || !current.isFile() || current.isSymbolicLink() || current.dev !== existingIdentity.dev || current.ino !== existingIdentity.ino) {
|
|
2873
2878
|
throw new NotebookError("CONFLICT", "Notebook state target changed during atomic update; preserving both entries for audit");
|
|
2874
2879
|
}
|
|
@@ -3103,13 +3108,13 @@ function baselineReceiptByteCeiling(limits) {
|
|
|
3103
3108
|
function readSessionBaseline(root, projectSlug, sessionKey, limits) {
|
|
3104
3109
|
assertDigest(sessionKey, "session key");
|
|
3105
3110
|
const paths = ensureNotebookState(root, projectSlug);
|
|
3106
|
-
return readBaseline(
|
|
3111
|
+
return readBaseline(join13(paths.baselines, `${sessionKey}.json`), baselineReceiptByteCeiling(limits));
|
|
3107
3112
|
}
|
|
3108
3113
|
function acquireLock(paths, maxWaitMs) {
|
|
3109
|
-
const lock =
|
|
3114
|
+
const lock = join13(paths.locks, "admission.lock");
|
|
3110
3115
|
const deadline = Date.now() + Math.max(1, maxWaitMs);
|
|
3111
3116
|
const token = randomUUID3();
|
|
3112
|
-
const
|
|
3117
|
+
const record3 = () => ({
|
|
3113
3118
|
schema_version: NOTEBOOK_SCHEMA_VERSION,
|
|
3114
3119
|
token,
|
|
3115
3120
|
pid: process.pid,
|
|
@@ -3117,7 +3122,7 @@ function acquireLock(paths, maxWaitMs) {
|
|
|
3117
3122
|
expires_at: new Date(Date.now() + Math.max(5e3, maxWaitMs * 20)).toISOString()
|
|
3118
3123
|
});
|
|
3119
3124
|
while (true) {
|
|
3120
|
-
if (exclusiveWrite(lock, jsonLine(
|
|
3125
|
+
if (exclusiveWrite(lock, jsonLine(record3()), paths.root)) {
|
|
3121
3126
|
return () => {
|
|
3122
3127
|
const current = safeReadJson(lock, 8192);
|
|
3123
3128
|
if (current.value && typeof current.value === "object" && !Array.isArray(current.value) && current.value.token === token) {
|
|
@@ -3136,7 +3141,7 @@ function acquireLock(paths, maxWaitMs) {
|
|
|
3136
3141
|
throw new NotebookError("CONFLICT", "Notebook state lock has an integrity finding; preserve it and run pj notebook audit --json");
|
|
3137
3142
|
}
|
|
3138
3143
|
if (Date.now() >= expiresAt) {
|
|
3139
|
-
const recovery =
|
|
3144
|
+
const recovery = join13(paths.locks, `recovery-${heldToken}.lock`);
|
|
3140
3145
|
const recoveryRecord = jsonLine({ schema_version: NOTEBOOK_SCHEMA_VERSION, stale_token: heldToken, recovery_token: token, expires_at: new Date(Date.now() + 5e3).toISOString() });
|
|
3141
3146
|
if (exclusiveWrite(recovery, recoveryRecord, paths.root)) {
|
|
3142
3147
|
try {
|
|
@@ -3144,7 +3149,7 @@ function acquireLock(paths, maxWaitMs) {
|
|
|
3144
3149
|
const currentToken = verify.value && typeof verify.value === "object" && !Array.isArray(verify.value) ? verify.value.token : null;
|
|
3145
3150
|
const currentExpiry = verify.value && typeof verify.value === "object" && !Array.isArray(verify.value) ? Date.parse(String(verify.value.expires_at ?? "")) : Number.NaN;
|
|
3146
3151
|
if (currentToken === heldToken && Number.isFinite(currentExpiry) && Date.now() >= currentExpiry) {
|
|
3147
|
-
const recovered =
|
|
3152
|
+
const recovered = join13(paths.locks, `.recovered-${heldToken}-${randomUUID3()}.json`);
|
|
3148
3153
|
renameStateFile(lock, recovered, paths.root);
|
|
3149
3154
|
unlinkStateFile(recovered, paths.root);
|
|
3150
3155
|
}
|
|
@@ -3224,7 +3229,7 @@ function scanAuxiliaryState(paths, limits) {
|
|
|
3224
3229
|
addBoundedIntegrity(scan, limits, { entry_id: entryId, reason: "non-regular" });
|
|
3225
3230
|
continue;
|
|
3226
3231
|
}
|
|
3227
|
-
const read = safeReadJson(
|
|
3232
|
+
const read = safeReadJson(join13(specification.dir, entry.name), specification.maxBytes);
|
|
3228
3233
|
if (read.reason || read.value === void 0) {
|
|
3229
3234
|
addBoundedIntegrity(scan, limits, { entry_id: entryId, reason: read.reason ?? "invalid-json" }, read.bytes);
|
|
3230
3235
|
continue;
|
|
@@ -3249,7 +3254,7 @@ function scanAuxiliaryState(paths, limits) {
|
|
|
3249
3254
|
addBoundedIntegrity(scan, limits, { entry_id: entryId, reason: "non-regular" });
|
|
3250
3255
|
continue;
|
|
3251
3256
|
}
|
|
3252
|
-
const read = safeReadJson(
|
|
3257
|
+
const read = safeReadJson(join13(paths.journals, entry.name), limits.receipt_max_bytes);
|
|
3253
3258
|
if (read.reason || read.value === void 0) {
|
|
3254
3259
|
addBoundedIntegrity(scan, limits, { entry_id: entryId, reason: read.reason ?? "invalid-json" }, read.bytes);
|
|
3255
3260
|
continue;
|
|
@@ -3278,7 +3283,7 @@ function scanAuxiliaryState(paths, limits) {
|
|
|
3278
3283
|
addBoundedIntegrity(scan, limits, { entry_id: entryId, reason: "non-regular" });
|
|
3279
3284
|
continue;
|
|
3280
3285
|
}
|
|
3281
|
-
const read = safeReadJson(
|
|
3286
|
+
const read = safeReadJson(join13(paths.locks, entry.name), 8192);
|
|
3282
3287
|
if (read.reason || read.value === void 0) {
|
|
3283
3288
|
addBoundedIntegrity(scan, limits, { entry_id: entryId, reason: read.reason ?? "invalid-json" }, read.bytes);
|
|
3284
3289
|
continue;
|
|
@@ -3317,7 +3322,7 @@ function scanReceipts(paths, limits) {
|
|
|
3317
3322
|
}
|
|
3318
3323
|
for (const entry of entries) {
|
|
3319
3324
|
const entryId = safeEntryId("receipts", entry.name);
|
|
3320
|
-
const path =
|
|
3325
|
+
const path = join13(paths.receipts, entry.name);
|
|
3321
3326
|
if (!entry.isFile() || entry.isSymbolicLink() || !/^[a-f0-9]{64}\.json$/u.test(entry.name)) {
|
|
3322
3327
|
addIntegrity({ entry_id: entryId, reason: "non-regular" });
|
|
3323
3328
|
continue;
|
|
@@ -3347,7 +3352,7 @@ function scanBaselines(paths, nowMs, limits, referenced) {
|
|
|
3347
3352
|
if (!existsSync9(paths.baselines)) return { current, stale };
|
|
3348
3353
|
for (const entry of readStateDirectory(paths.baselines, paths.root)) {
|
|
3349
3354
|
if (!entry.isFile() || !entry.name.endsWith(".json")) continue;
|
|
3350
|
-
const baseline = readBaseline(
|
|
3355
|
+
const baseline = readBaseline(join13(paths.baselines, entry.name), baselineReceiptByteCeiling(limits));
|
|
3351
3356
|
if (!baseline || referenced.has(baseline.session_key)) continue;
|
|
3352
3357
|
const expires = Date.parse(baseline.created_at) + limits.receiptless_session_retention_seconds * 1e3;
|
|
3353
3358
|
if (nowMs >= expires) stale += 1;
|
|
@@ -3361,7 +3366,7 @@ function scanRefusals(paths, nowMs, limits, scan) {
|
|
|
3361
3366
|
const entries = readStateDirectory(paths.refusals, paths.root).sort((a, b) => a.name.localeCompare(b.name, "en")).slice(0, limits.refusal_max_entries);
|
|
3362
3367
|
for (const entry of entries) {
|
|
3363
3368
|
if (!entry.isFile() || !entry.name.endsWith(".json")) continue;
|
|
3364
|
-
const read = safeReadJson(
|
|
3369
|
+
const read = safeReadJson(join13(paths.refusals, entry.name), limits.receipt_max_bytes);
|
|
3365
3370
|
const marker = read.value === void 0 ? null : parseRefusal(read.value);
|
|
3366
3371
|
if (!marker || `${marker.session_key}.json` !== entry.name) continue;
|
|
3367
3372
|
if (nowMs >= Date.parse(marker.baseline_created_at) + limits.receiptless_session_retention_seconds * 1e3) continue;
|
|
@@ -3444,7 +3449,7 @@ function listCaptureReceipts(root, projectSlug, limits, state) {
|
|
|
3444
3449
|
function readCaptureReceipt(root, projectSlug, receiptId, limits) {
|
|
3445
3450
|
if (!RECEIPT_ID_RE.test(receiptId)) throw new NotebookError("INVALID_INPUT", "Invalid receipt ID");
|
|
3446
3451
|
const paths = notebookStatePaths(root, projectSlug);
|
|
3447
|
-
const path =
|
|
3452
|
+
const path = join13(paths.receipts, `${receiptId}.json`);
|
|
3448
3453
|
if (!existsSync9(path)) throw new NotebookError("NOT_FOUND", `Capture receipt not found: ${receiptId}`);
|
|
3449
3454
|
const read = safeReadJson(path, limits.receipt_max_bytes);
|
|
3450
3455
|
const receipt = read.value === void 0 ? null : parseReceipt(read.value);
|
|
@@ -3457,7 +3462,7 @@ function writeReceipt(paths, receipt, limit) {
|
|
|
3457
3462
|
const bytes = Buffer.byteLength(text, "utf8");
|
|
3458
3463
|
if (bytes > limit) throw new NotebookError("CONFLICT", "Receipt transition exceeds its per-receipt ceiling");
|
|
3459
3464
|
if (receipt.serialized_bytes === bytes) {
|
|
3460
|
-
atomicWriteJson(
|
|
3465
|
+
atomicWriteJson(join13(paths.receipts, `${receipt.receipt_id}.json`), receipt, paths.root);
|
|
3461
3466
|
return receipt;
|
|
3462
3467
|
}
|
|
3463
3468
|
receipt.serialized_bytes = bytes;
|
|
@@ -3596,7 +3601,7 @@ function pruneNotebookState(root, projectSlug, limits, now = /* @__PURE__ */ new
|
|
|
3596
3601
|
const successCutoff = now.getTime() - limits.receipt_succeeded_retention_days * 864e5;
|
|
3597
3602
|
for (const receipt of scan.receipts) {
|
|
3598
3603
|
if (receipt.state !== "succeeded" || Date.parse(receipt.updated_at) > successCutoff) continue;
|
|
3599
|
-
const path =
|
|
3604
|
+
const path = join13(paths.receipts, `${receipt.receipt_id}.json`);
|
|
3600
3605
|
unlinkStateFile(path, paths.root);
|
|
3601
3606
|
removed.push(safeEntryId("receipts", basename7(path)));
|
|
3602
3607
|
}
|
|
@@ -3605,15 +3610,15 @@ function pruneNotebookState(root, projectSlug, limits, now = /* @__PURE__ */ new
|
|
|
3605
3610
|
const referenced = /* @__PURE__ */ new Set([...remaining.referencedSessions, ...remainingAuxiliary.referencedSessions]);
|
|
3606
3611
|
for (const entry of readStateDirectory(paths.baselines, paths.root)) {
|
|
3607
3612
|
if (!entry.isFile() || !entry.name.endsWith(".json")) continue;
|
|
3608
|
-
const path =
|
|
3613
|
+
const path = join13(paths.baselines, entry.name);
|
|
3609
3614
|
const baseline = readBaseline(path, baselineReceiptByteCeiling(limits));
|
|
3610
3615
|
if (!baseline || referenced.has(baseline.session_key)) continue;
|
|
3611
3616
|
if (now.getTime() < Date.parse(baseline.created_at) + limits.receiptless_session_retention_seconds * 1e3) continue;
|
|
3612
3617
|
unlinkStateFile(path, paths.root);
|
|
3613
3618
|
removed.push(safeEntryId("baselines", entry.name));
|
|
3614
|
-
const claim =
|
|
3619
|
+
const claim = join13(paths.claims, `${baseline.session_key}.overview`);
|
|
3615
3620
|
if (unlinkStateFile(claim, paths.root, true)) removed.push(safeEntryId("claims", basename7(claim)));
|
|
3616
|
-
const refusal =
|
|
3621
|
+
const refusal = join13(paths.refusals, `${baseline.session_key}.json`);
|
|
3617
3622
|
if (unlinkStateFile(refusal, paths.root, true)) removed.push(safeEntryId("refusals", basename7(refusal)));
|
|
3618
3623
|
}
|
|
3619
3624
|
fsyncDirectory2(paths.project);
|
|
@@ -3998,11 +4003,11 @@ var init_open_notebook_client = __esm({
|
|
|
3998
4003
|
});
|
|
3999
4004
|
|
|
4000
4005
|
// src/notebook/git-evidence.ts
|
|
4001
|
-
import { spawnSync as
|
|
4002
|
-
import { closeSync as closeSync4, constants as constants3, fstatSync as fstatSync2, lstatSync as
|
|
4003
|
-
import { extname, join as
|
|
4006
|
+
import { spawnSync as spawnSync7 } from "node:child_process";
|
|
4007
|
+
import { closeSync as closeSync4, constants as constants3, fstatSync as fstatSync2, lstatSync as lstatSync11, openSync as openSync4, readSync as readSync2, realpathSync as realpathSync7 } from "node:fs";
|
|
4008
|
+
import { extname, join as join15, relative as relative9, resolve as resolve13, sep as sep4 } from "node:path";
|
|
4004
4009
|
function git(repo, args, maxBuffer = 4 * 1024 * 1024, timeout = 5e3) {
|
|
4005
|
-
const result2 =
|
|
4010
|
+
const result2 = spawnSync7("git", args, { cwd: repo, encoding: "utf8", maxBuffer, timeout, shell: false });
|
|
4006
4011
|
return { ok: result2.status === 0, stdout: result2.stdout ?? "" };
|
|
4007
4012
|
}
|
|
4008
4013
|
function statusPaths(value) {
|
|
@@ -4069,7 +4074,7 @@ function validateCommittedGitRef(repoPath, gitRef) {
|
|
|
4069
4074
|
function safeRelative(repoPath, relativePath2) {
|
|
4070
4075
|
if (!relativePath2 || relativePath2.includes("\0") || relativePath2.startsWith("/") || relativePath2.split(/[\\/]/u).includes("..")) return null;
|
|
4071
4076
|
const root = realpathSync7(repoPath);
|
|
4072
|
-
const candidate =
|
|
4077
|
+
const candidate = resolve13(root, relativePath2);
|
|
4073
4078
|
const rel = relative9(root, candidate);
|
|
4074
4079
|
if (rel === ".." || rel.startsWith(`..${sep4}`) || rel.startsWith(sep4)) return null;
|
|
4075
4080
|
return { root, candidate };
|
|
@@ -4078,9 +4083,9 @@ function hasSymlinkComponent(root, candidate) {
|
|
|
4078
4083
|
const rel = relative9(root, candidate);
|
|
4079
4084
|
let cursor = root;
|
|
4080
4085
|
for (const part of rel.split(sep4).filter(Boolean)) {
|
|
4081
|
-
cursor =
|
|
4086
|
+
cursor = join15(cursor, part);
|
|
4082
4087
|
try {
|
|
4083
|
-
if (
|
|
4088
|
+
if (lstatSync11(cursor).isSymbolicLink()) return true;
|
|
4084
4089
|
} catch {
|
|
4085
4090
|
return true;
|
|
4086
4091
|
}
|
|
@@ -4147,8 +4152,8 @@ function matchesSimpleGlob(path, glob) {
|
|
|
4147
4152
|
if (glob.startsWith("**/*.")) return extname(path).toLowerCase() === `.${glob.slice(5).toLowerCase()}`;
|
|
4148
4153
|
return path === glob;
|
|
4149
4154
|
}
|
|
4150
|
-
function addExclusion(
|
|
4151
|
-
|
|
4155
|
+
function addExclusion(record3, reason) {
|
|
4156
|
+
record3[reason] = (record3[reason] ?? 0) + 1;
|
|
4152
4157
|
}
|
|
4153
4158
|
function selectEligibleDocuments(config, baseline, manualBaselineRef) {
|
|
4154
4159
|
const repo = config.repo_path;
|
|
@@ -4221,10 +4226,10 @@ var init_git_evidence = __esm({
|
|
|
4221
4226
|
|
|
4222
4227
|
// src/notebook/remote-mutation-journal.ts
|
|
4223
4228
|
import { randomUUID as randomUUID4 } from "node:crypto";
|
|
4224
|
-
import { join as
|
|
4229
|
+
import { join as join16 } from "node:path";
|
|
4225
4230
|
function journalPath(root, projectSlug, operationId) {
|
|
4226
4231
|
if (!/^[a-f0-9-]{16,64}$/iu.test(operationId)) throw new NotebookError("INVALID_INPUT", "Invalid remote mutation operation ID");
|
|
4227
|
-
return
|
|
4232
|
+
return join16(ensureNotebookState(root, projectSlug).journals, `${operationId}.json`);
|
|
4228
4233
|
}
|
|
4229
4234
|
function remoteMutationJournalPath(root, projectSlug, operationId) {
|
|
4230
4235
|
return journalPath(root, projectSlug, operationId);
|
|
@@ -4244,7 +4249,7 @@ function listRemoteMutationJournals(root, projectSlug) {
|
|
|
4244
4249
|
if (!entry.isFile() || entry.isSymbolicLink() || !/^[a-f0-9-]{16,64}\.json$/iu.test(entry.name)) {
|
|
4245
4250
|
throw new NotebookError("CONFLICT", `Remote mutation journal state-integrity finding: journals/${entry.name}`);
|
|
4246
4251
|
}
|
|
4247
|
-
const read = readNotebookStateJson(
|
|
4252
|
+
const read = readNotebookStateJson(join16(paths.journals, entry.name), paths.root, 65536);
|
|
4248
4253
|
const journal = read.value === void 0 ? null : parseRemoteMutationJournal(read.value);
|
|
4249
4254
|
if (!journal || `${journal.operation_id}.json` !== entry.name) {
|
|
4250
4255
|
throw new NotebookError("CONFLICT", `Remote mutation journal state-integrity finding: journals/${entry.name}`);
|
|
@@ -4599,15 +4604,15 @@ var init_reconcile = __esm({
|
|
|
4599
4604
|
// src/notebook/entropy.ts
|
|
4600
4605
|
import { existsSync as existsSync12 } from "node:fs";
|
|
4601
4606
|
import { createHash as createHash9 } from "node:crypto";
|
|
4602
|
-
import { resolve as
|
|
4603
|
-
import { spawnSync as
|
|
4607
|
+
import { resolve as resolve16 } from "node:path";
|
|
4608
|
+
import { spawnSync as spawnSync10 } from "node:child_process";
|
|
4604
4609
|
function listAllTrackedDocuments(config) {
|
|
4605
4610
|
const repo = config.repo_path;
|
|
4606
4611
|
const timeout = config.limits.overall_timeout_ms;
|
|
4607
|
-
const trackedResult =
|
|
4612
|
+
const trackedResult = spawnSync10("git", ["ls-files", "-z"], { cwd: repo, encoding: "utf8", maxBuffer: 4 * 1024 * 1024, timeout, shell: false });
|
|
4608
4613
|
if (trackedResult.status !== 0) return [];
|
|
4609
4614
|
const tracked = trackedResult.stdout.split("\0").filter(Boolean);
|
|
4610
|
-
const headRev =
|
|
4615
|
+
const headRev = spawnSync10("git", ["rev-parse", "HEAD"], { cwd: repo, encoding: "utf8", timeout, shell: false }).stdout?.trim() ?? "HEAD";
|
|
4611
4616
|
const documents = [];
|
|
4612
4617
|
for (const path of tracked) {
|
|
4613
4618
|
if (!config.policy.documentation_globs.some((glob) => matchesSimpleGlob(path, glob))) continue;
|
|
@@ -4634,7 +4639,7 @@ async function pruneZombieNotes(input) {
|
|
|
4634
4639
|
const parsed = parseNoteEnvelope(note.content);
|
|
4635
4640
|
if (!parsed || parsed.envelope.kind !== "document" || parsed.envelope.project_slug !== input.projectSlug) continue;
|
|
4636
4641
|
const sourcePath = parsed.envelope.source_path;
|
|
4637
|
-
if (!sourcePath || !existsSync12(
|
|
4642
|
+
if (!sourcePath || !existsSync12(resolve16(input.repoPath, sourcePath))) {
|
|
4638
4643
|
if (typeof input.client.deleteOwnedNote === "function") {
|
|
4639
4644
|
await input.client.deleteOwnedNote(input.notebookId, note.id);
|
|
4640
4645
|
} else if (typeof input.client.deleteNote === "function") {
|
|
@@ -4794,8 +4799,8 @@ var init_entropy = __esm({
|
|
|
4794
4799
|
});
|
|
4795
4800
|
|
|
4796
4801
|
// src/notebook/summarizer.ts
|
|
4797
|
-
import { spawnSync as
|
|
4798
|
-
import { isAbsolute as
|
|
4802
|
+
import { spawnSync as spawnSync11 } from "node:child_process";
|
|
4803
|
+
import { isAbsolute as isAbsolute8 } from "node:path";
|
|
4799
4804
|
function evidenceItems(evidence) {
|
|
4800
4805
|
const items = [];
|
|
4801
4806
|
const eligible = new Set(evidence.documents.map((item) => item.path));
|
|
@@ -4868,11 +4873,11 @@ function words(value) {
|
|
|
4868
4873
|
}
|
|
4869
4874
|
function validateClaims(value, items, noteMaxBytes) {
|
|
4870
4875
|
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
4871
|
-
const
|
|
4872
|
-
if (
|
|
4876
|
+
const record3 = value;
|
|
4877
|
+
if (record3.schema_version !== 1 || !Array.isArray(record3.claims) || record3.claims.length < 1 || record3.claims.length > 50 || Object.keys(record3).some((key) => key !== "schema_version" && key !== "claims")) return null;
|
|
4873
4878
|
const byId = new Map(items.map((item) => [item.evidence_id, item]));
|
|
4874
4879
|
const rendered = ["## Session outcome"];
|
|
4875
|
-
for (const claimValue of
|
|
4880
|
+
for (const claimValue of record3.claims) {
|
|
4876
4881
|
if (!claimValue || typeof claimValue !== "object" || Array.isArray(claimValue)) return null;
|
|
4877
4882
|
const claim = claimValue;
|
|
4878
4883
|
if (Object.keys(claim).some((key) => key !== "text" && key !== "evidence_ids") || typeof claim.text !== "string" || !claim.text.trim() || Buffer.byteLength(claim.text, "utf8") > 1024 || !Array.isArray(claim.evidence_ids) || claim.evidence_ids.length < 1 || claim.evidence_ids.length > 10 || claim.evidence_ids.some((id) => typeof id !== "string" || !byId.has(id))) return null;
|
|
@@ -4893,7 +4898,7 @@ function summarizeCapture(config, evidence) {
|
|
|
4893
4898
|
const items = evidenceItems(evidence);
|
|
4894
4899
|
const payload = JSON.stringify({ schema_version: 1, evidence: items });
|
|
4895
4900
|
if (Buffer.byteLength(payload, "utf8") > config.limits.request_max_bytes) return { schema_version: 1, mode: "deterministic-fallback", summary: fallbackSummary };
|
|
4896
|
-
const result2 =
|
|
4901
|
+
const result2 = spawnSync11(config.summarizer.executable, config.summarizer.args, {
|
|
4897
4902
|
cwd: config.repo_path,
|
|
4898
4903
|
input: Buffer.from(payload, "utf8"),
|
|
4899
4904
|
timeout: config.limits.overall_timeout_ms,
|
|
@@ -4911,7 +4916,7 @@ function summarizeCapture(config, evidence) {
|
|
|
4911
4916
|
}
|
|
4912
4917
|
}
|
|
4913
4918
|
function assertSummarizerConfig(config) {
|
|
4914
|
-
if (config.summarizer && (!
|
|
4919
|
+
if (config.summarizer && (!isAbsolute8(config.summarizer.executable) || config.summarizer.executable.includes("\0") || config.summarizer.args.length > 32 || config.summarizer.args.some((arg) => arg.includes("\0") || Buffer.byteLength(arg, "utf8") > 1024))) {
|
|
4915
4920
|
throw new NotebookError("NOT_CONFIGURED", "Configured Notebook summarizer command is invalid");
|
|
4916
4921
|
}
|
|
4917
4922
|
}
|
|
@@ -5221,7 +5226,7 @@ var init_capture = __esm({
|
|
|
5221
5226
|
|
|
5222
5227
|
// src/mcp-server.ts
|
|
5223
5228
|
import { existsSync as existsSync18, statSync as statSync7 } from "node:fs";
|
|
5224
|
-
import { dirname as dirname17, join as
|
|
5229
|
+
import { dirname as dirname17, join as join27, resolve as resolve22 } from "node:path";
|
|
5225
5230
|
import { fileURLToPath as fileURLToPath6 } from "node:url";
|
|
5226
5231
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
5227
5232
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
@@ -5332,12 +5337,12 @@ var Recipe = class {
|
|
|
5332
5337
|
};
|
|
5333
5338
|
}
|
|
5334
5339
|
/** Initialize missing/drifted state using only this recipe's owned checks. */
|
|
5335
|
-
async initializeOwnedChecks(ctx) {
|
|
5340
|
+
async initializeOwnedChecks(ctx, checks = this.checks) {
|
|
5336
5341
|
const phases = [];
|
|
5337
5342
|
const logs = [];
|
|
5338
5343
|
const errors = [];
|
|
5339
5344
|
const changedFiles = [];
|
|
5340
|
-
for (const check of
|
|
5345
|
+
for (const check of checks) {
|
|
5341
5346
|
const finding2 = await check.audit(ctx);
|
|
5342
5347
|
if (finding2.status === "pass" || finding2.status === "skip") {
|
|
5343
5348
|
phases.push({ id: check.id, status: finding2.status === "skip" ? "skipped" : "unchanged", changedFiles: [], message: finding2.summary });
|
|
@@ -5419,10 +5424,233 @@ var Recipe = class {
|
|
|
5419
5424
|
|
|
5420
5425
|
// src/parity/rules.ts
|
|
5421
5426
|
init_registryClient();
|
|
5427
|
+
|
|
5428
|
+
// src/bmad/companion.ts
|
|
5429
|
+
init_tree_diff();
|
|
5430
|
+
import { existsSync as existsSync2, lstatSync as lstatSync2, readFileSync as readFileSync2 } from "node:fs";
|
|
5431
|
+
import { spawnSync as spawnSync2 } from "node:child_process";
|
|
5432
|
+
import { isAbsolute, join as join3, resolve as resolve3 } from "node:path";
|
|
5433
|
+
import { pathToFileURL } from "node:url";
|
|
5434
|
+
var record = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
5435
|
+
function optionalObject(parent, key, label) {
|
|
5436
|
+
if (!Object.hasOwn(parent, key)) return {};
|
|
5437
|
+
const value = parent[key];
|
|
5438
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error(`${label} must be an object when present`);
|
|
5439
|
+
return value;
|
|
5440
|
+
}
|
|
5441
|
+
function select(projectRoot) {
|
|
5442
|
+
const path = join3(projectRoot, ".project.json");
|
|
5443
|
+
const manifest = existsSync2(path) ? record(JSON.parse(readFileSync2(path, "utf8"))) : {};
|
|
5444
|
+
const bmad = optionalObject(manifest, "bmad", "bmad");
|
|
5445
|
+
const config = optionalObject(bmad, "companion", "bmad.companion");
|
|
5446
|
+
const adapter = optionalObject(config, "adapter", "bmad.companion.adapter");
|
|
5447
|
+
const options = optionalObject(adapter, "options", "bmad.companion.adapter.options");
|
|
5448
|
+
if (config.enabled === false) return { enabled: false, reason: "g33 explicitly disabled in .project.json", options: {} };
|
|
5449
|
+
if (config.enabled !== void 0 && config.enabled !== true) throw new Error("bmad.companion.enabled must be boolean");
|
|
5450
|
+
const binding = record(manifest.ticket_provider);
|
|
5451
|
+
const registered = typeof manifest.project_id === "string" && /^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/i.test(manifest.project_id) && manifest.project_id.length <= 128;
|
|
5452
|
+
const eligible = registered && binding.type === "plane" && binding.state === "linked" && typeof binding.workspace === "string" && binding.workspace.trim().toLowerCase() === "33god" && typeof binding.board_id === "string" && binding.board_id.trim() !== "";
|
|
5453
|
+
if (!eligible && config.enabled !== true) return { enabled: false, reason: "g33 defaults off without a registered linked 33god project or explicit opt-in", options: {} };
|
|
5454
|
+
const module = adapter.module ?? (process.env.PJ_G33_ADAPTER_MODULE?.trim() || void 0);
|
|
5455
|
+
const exportName = adapter.exportName ?? (process.env.PJ_G33_ADAPTER_EXPORT?.trim() || void 0);
|
|
5456
|
+
if (module !== void 0 && (typeof module !== "string" || !module.trim())) throw new Error("g33 adapter.module must be a non-empty package specifier or source path");
|
|
5457
|
+
if (exportName !== void 0 && (typeof exportName !== "string" || !exportName.trim())) throw new Error("g33 adapter.exportName must name the supported callable export");
|
|
5458
|
+
return {
|
|
5459
|
+
enabled: true,
|
|
5460
|
+
reason: eligible ? "Registered linked 33god project" : "Explicit g33 opt-in",
|
|
5461
|
+
module,
|
|
5462
|
+
exportName,
|
|
5463
|
+
options
|
|
5464
|
+
};
|
|
5465
|
+
}
|
|
5466
|
+
function g33CompanionEnabled(projectRoot) {
|
|
5467
|
+
return select(projectRoot).enabled;
|
|
5468
|
+
}
|
|
5469
|
+
function resolveAdapter(projectRoot, module) {
|
|
5470
|
+
if (module.startsWith(".") || isAbsolute(module)) {
|
|
5471
|
+
const path = resolve3(projectRoot, module);
|
|
5472
|
+
if (!existsSync2(path)) throw new Error(`Configured g33 source is unavailable: ${module}`);
|
|
5473
|
+
return pathToFileURL(path).href;
|
|
5474
|
+
}
|
|
5475
|
+
if (module.includes(":") || module.startsWith("/")) throw new Error("Use a local source path or installed package specifier for g33");
|
|
5476
|
+
const resolved = spawnSync2(process.execPath, [
|
|
5477
|
+
"--experimental-import-meta-resolve",
|
|
5478
|
+
"--input-type=module",
|
|
5479
|
+
"--eval",
|
|
5480
|
+
"process.stdout.write(import.meta.resolve(process.argv[1], process.argv[2]))",
|
|
5481
|
+
"--",
|
|
5482
|
+
module,
|
|
5483
|
+
pathToFileURL(join3(projectRoot, ".project.json")).href
|
|
5484
|
+
], { encoding: "utf8", timeout: 1e4 });
|
|
5485
|
+
if (resolved.status === 0 && resolved.stdout.trim()) return resolved.stdout.trim();
|
|
5486
|
+
const projectError = resolved.stderr.trim() || resolved.error?.message || `resolver exit ${resolved.status}`;
|
|
5487
|
+
try {
|
|
5488
|
+
return import.meta.resolve(module);
|
|
5489
|
+
} catch (error) {
|
|
5490
|
+
throw new Error(`Project ESM resolution failed: ${projectError}
|
|
5491
|
+
PJangler ESM resolution failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
5492
|
+
}
|
|
5493
|
+
}
|
|
5494
|
+
function reply(value, operation) {
|
|
5495
|
+
const data = record(value);
|
|
5496
|
+
const statuses = operation === "observe" ? ["installed", "missing", "unavailable", "conflict", "error"] : operation === "plan" ? ["planned", "unchanged", "unavailable", "conflict", "error"] : ["changed", "unchanged", "unavailable", "conflict", "error"];
|
|
5497
|
+
if (data.schemaVersion !== 1 || !statuses.includes(String(data.status)) || typeof data.summary !== "string" || ![data.details, data.evidence].every((field2) => Array.isArray(field2) && field2.every((item) => typeof item === "string"))) {
|
|
5498
|
+
throw new Error(`Configured g33 adapter returned an invalid v1 ${operation} result`);
|
|
5499
|
+
}
|
|
5500
|
+
if (data.status === "installed" && !data.evidence.some((item) => item.trim())) {
|
|
5501
|
+
throw new Error("g33 installed observation requires owner content/mapping evidence; metadata alone is insufficient");
|
|
5502
|
+
}
|
|
5503
|
+
return {
|
|
5504
|
+
schemaVersion: 1,
|
|
5505
|
+
status: data.status,
|
|
5506
|
+
summary: data.summary,
|
|
5507
|
+
details: [...data.details],
|
|
5508
|
+
evidence: [...data.evidence]
|
|
5509
|
+
};
|
|
5510
|
+
}
|
|
5511
|
+
async function runG33Companion(projectRoot, dryRun, reason = "recipe") {
|
|
5512
|
+
projectRoot = resolve3(projectRoot);
|
|
5513
|
+
const result2 = { schemaVersion: 1, moduleId: "g33", optional: true, status: "unavailable", summary: "g33 companion unavailable", details: [], evidence: [], changedFiles: [] };
|
|
5514
|
+
let before;
|
|
5515
|
+
try {
|
|
5516
|
+
const selection = select(projectRoot);
|
|
5517
|
+
if (!selection.enabled) return { ...result2, status: "disabled", summary: selection.reason };
|
|
5518
|
+
result2.details.push(selection.reason);
|
|
5519
|
+
if (!selection.module || !selection.exportName) return {
|
|
5520
|
+
...result2,
|
|
5521
|
+
summary: "Optional g33 installer is not configured",
|
|
5522
|
+
details: [...result2.details, "Configure bmad.companion.adapter.module and exportName (or PJ_G33_ADAPTER_MODULE/PJ_G33_ADAPTER_EXPORT) to the owner's supported callable adapter. No g33 installation was attempted."]
|
|
5523
|
+
};
|
|
5524
|
+
result2.adapter = { module: selection.module, exportName: selection.exportName };
|
|
5525
|
+
let specifier;
|
|
5526
|
+
try {
|
|
5527
|
+
specifier = resolveAdapter(projectRoot, selection.module);
|
|
5528
|
+
} catch (error) {
|
|
5529
|
+
return { ...result2, status: "unavailable", summary: "Configured g33 adapter is unavailable", details: [...result2.details, error instanceof Error ? error.message : String(error)] };
|
|
5530
|
+
}
|
|
5531
|
+
before = snapshotTree(projectRoot);
|
|
5532
|
+
const exported = await import(specifier);
|
|
5533
|
+
if (changedTreePaths(projectRoot, before, snapshotTree(projectRoot)).length) {
|
|
5534
|
+
throw new Error("g33 adapter violated the no-write import contract");
|
|
5535
|
+
}
|
|
5536
|
+
const callable = exported[selection.exportName];
|
|
5537
|
+
if (typeof callable !== "function") throw new Error(`Configured g33 export ${selection.exportName} is not callable`);
|
|
5538
|
+
const invoke = async (operation) => {
|
|
5539
|
+
const value = reply(await callable({ schemaVersion: 1, moduleId: "g33", operation, projectRoot, reason, options: selection.options }), operation);
|
|
5540
|
+
if (operation !== "apply" && changedTreePaths(projectRoot, before, snapshotTree(projectRoot)).length) {
|
|
5541
|
+
throw new Error(`g33 adapter violated the no-write ${operation} contract`);
|
|
5542
|
+
}
|
|
5543
|
+
return value;
|
|
5544
|
+
};
|
|
5545
|
+
const observed = await invoke("observe");
|
|
5546
|
+
if (["unavailable", "conflict", "error"].includes(observed.status)) return { ...result2, ...observed };
|
|
5547
|
+
const plan = await invoke("plan");
|
|
5548
|
+
if (["unavailable", "conflict", "error"].includes(plan.status)) return { ...result2, ...plan };
|
|
5549
|
+
if (plan.status === "unchanged" && observed.status !== "installed") throw new Error("g33 plan claims convergence but owner observation does not prove installation");
|
|
5550
|
+
if (dryRun) return { ...result2, ...plan, evidence: observed.evidence };
|
|
5551
|
+
if (plan.status === "unchanged" && observed.status === "installed") return { ...result2, ...observed, status: "unchanged" };
|
|
5552
|
+
const applied = await invoke("apply");
|
|
5553
|
+
result2.changedFiles = changedTreePaths(projectRoot, before, snapshotTree(projectRoot));
|
|
5554
|
+
if (["unavailable", "conflict", "error"].includes(applied.status)) return { ...result2, ...applied };
|
|
5555
|
+
before = snapshotTree(projectRoot);
|
|
5556
|
+
const verified = await invoke("observe");
|
|
5557
|
+
if (verified.status !== "installed") return {
|
|
5558
|
+
...result2,
|
|
5559
|
+
status: "error",
|
|
5560
|
+
summary: "g33 apply did not establish an installed owner observation",
|
|
5561
|
+
details: [verified.summary, ...verified.details],
|
|
5562
|
+
evidence: verified.evidence
|
|
5563
|
+
};
|
|
5564
|
+
return { ...result2, ...verified, status: result2.changedFiles.length ? "changed" : "unchanged" };
|
|
5565
|
+
} catch (error) {
|
|
5566
|
+
if (before) {
|
|
5567
|
+
try {
|
|
5568
|
+
result2.changedFiles = [.../* @__PURE__ */ new Set([...result2.changedFiles, ...changedTreePaths(projectRoot, before, snapshotTree(projectRoot))])].sort();
|
|
5569
|
+
} catch (snapshotError) {
|
|
5570
|
+
result2.details.push(`Could not inventory partial g33 writes: ${String(snapshotError)}`);
|
|
5571
|
+
}
|
|
5572
|
+
}
|
|
5573
|
+
return { ...result2, status: "error", summary: "Optional g33 companion could not be reconciled", details: [...result2.details, error instanceof Error ? error.message : String(error)] };
|
|
5574
|
+
}
|
|
5575
|
+
}
|
|
5576
|
+
function localEntryPresent(path) {
|
|
5577
|
+
try {
|
|
5578
|
+
lstatSync2(path);
|
|
5579
|
+
return true;
|
|
5580
|
+
} catch (error) {
|
|
5581
|
+
if (error.code === "ENOENT") return false;
|
|
5582
|
+
throw error;
|
|
5583
|
+
}
|
|
5584
|
+
}
|
|
5585
|
+
function g33BmadUpdateBoundary(projectRoot, retainedG33Declaration = false) {
|
|
5586
|
+
const result2 = {
|
|
5587
|
+
schemaVersion: 1,
|
|
5588
|
+
moduleId: "g33",
|
|
5589
|
+
optional: true,
|
|
5590
|
+
status: "conflict",
|
|
5591
|
+
summary: "BMAD update withheld pending the g33 owner pre-update preservation contract",
|
|
5592
|
+
details: [
|
|
5593
|
+
"No installer, retired-state eviction, or companion import/apply was attempted. Existing native BMAD bytes, customizations, and module mappings are retained.",
|
|
5594
|
+
"The owner must provide a supported pre-update capture/protection and post-update restore/verification handoff, including failure recovery. The configured v1 callable alone does not authorize a destructive BMAD update."
|
|
5595
|
+
],
|
|
5596
|
+
evidence: [],
|
|
5597
|
+
changedFiles: []
|
|
5598
|
+
};
|
|
5599
|
+
try {
|
|
5600
|
+
const retainedG33 = retainedG33Declaration || localEntryPresent(join3(projectRoot, "_bmad", "g33"));
|
|
5601
|
+
const installedBmad = retainedG33 || localEntryPresent(join3(projectRoot, "_bmad", "_config", "manifest.yaml")) || localEntryPresent(join3(projectRoot, "_bmad", "core"));
|
|
5602
|
+
if (!installedBmad) return void 0;
|
|
5603
|
+
const selection = select(projectRoot);
|
|
5604
|
+
if (!selection.enabled && !retainedG33) return void 0;
|
|
5605
|
+
if (selection.module && selection.exportName) result2.adapter = { module: selection.module, exportName: selection.exportName };
|
|
5606
|
+
result2.details.unshift(retainedG33 ? "Retained g33 tree or module declaration requires preservation, including after opt-out." : selection.reason);
|
|
5607
|
+
return result2;
|
|
5608
|
+
} catch (error) {
|
|
5609
|
+
return {
|
|
5610
|
+
...result2,
|
|
5611
|
+
status: "error",
|
|
5612
|
+
summary: "BMAD update withheld: companion configuration or preservation inspection failed",
|
|
5613
|
+
details: [...result2.details, error instanceof Error ? error.message : String(error)]
|
|
5614
|
+
};
|
|
5615
|
+
}
|
|
5616
|
+
}
|
|
5617
|
+
function companionDetails(result2) {
|
|
5618
|
+
return [`g33 (${result2.status}): ${result2.summary}`, ...result2.details, ...result2.evidence];
|
|
5619
|
+
}
|
|
5620
|
+
function createG33CompanionCheck() {
|
|
5621
|
+
return {
|
|
5622
|
+
id: "bmad.g33-companion",
|
|
5623
|
+
title: "Optional g33 companion",
|
|
5624
|
+
audit: async (ctx) => {
|
|
5625
|
+
const result2 = await runG33Companion(ctx.repoRoot, true, "audit");
|
|
5626
|
+
return {
|
|
5627
|
+
id: "bmad.g33-companion",
|
|
5628
|
+
title: "Optional g33 companion",
|
|
5629
|
+
status: result2.status === "disabled" ? "skip" : result2.status === "unchanged" && result2.evidence.length ? "pass" : "warn",
|
|
5630
|
+
summary: result2.summary,
|
|
5631
|
+
details: companionDetails(result2),
|
|
5632
|
+
fixable: result2.status === "planned"
|
|
5633
|
+
};
|
|
5634
|
+
},
|
|
5635
|
+
migrate: async (ctx, finding2) => {
|
|
5636
|
+
const result2 = await runG33Companion(ctx.repoRoot, ctx.dryRun, "repair");
|
|
5637
|
+
return {
|
|
5638
|
+
id: finding2.id,
|
|
5639
|
+
title: finding2.title,
|
|
5640
|
+
status: result2.status === "changed" || result2.status === "planned" ? "applied" : result2.status === "unchanged" ? "noop" : result2.status === "disabled" ? "skipped" : "partial",
|
|
5641
|
+
summary: result2.summary,
|
|
5642
|
+
details: companionDetails(result2),
|
|
5643
|
+
changedFiles: result2.changedFiles
|
|
5644
|
+
};
|
|
5645
|
+
}
|
|
5646
|
+
};
|
|
5647
|
+
}
|
|
5648
|
+
|
|
5649
|
+
// src/parity/rules.ts
|
|
5422
5650
|
init_style();
|
|
5423
|
-
import { existsSync as
|
|
5424
|
-
import { basename as basename3, dirname as dirname5, join as
|
|
5425
|
-
import { spawnSync as
|
|
5651
|
+
import { existsSync as existsSync4, lstatSync as lstatSync6, mkdirSync as mkdirSync3, readFileSync as readFileSync5, readlinkSync as readlinkSync4, readdirSync as readdirSync4, realpathSync as realpathSync3, renameSync as renameSync2, symlinkSync as symlinkSync2, unlinkSync as unlinkSync3, writeFileSync as writeFileSync3, chmodSync, copyFileSync, rmSync as rmSync2 } from "node:fs";
|
|
5652
|
+
import { basename as basename3, dirname as dirname5, join as join7, relative as relative5, resolve as resolve6 } from "node:path";
|
|
5653
|
+
import { spawnSync as spawnSync4 } from "node:child_process";
|
|
5426
5654
|
import YAML2 from "yaml";
|
|
5427
5655
|
import { parse as parseToml2 } from "smol-toml";
|
|
5428
5656
|
|
|
@@ -5441,8 +5669,8 @@ var SUPPORTED_BMAD_TOOLS = SUPPORTED_CLIS.map((cli) => cli.bmadTool);
|
|
|
5441
5669
|
var SUPPORTED_CLI_ROOTS = SUPPORTED_CLIS.map((cli) => cli.projectRoot);
|
|
5442
5670
|
|
|
5443
5671
|
// src/parity/skills.ts
|
|
5444
|
-
import { lstatSync as
|
|
5445
|
-
import { basename as basename2, dirname as dirname4, join as
|
|
5672
|
+
import { lstatSync as lstatSync5, readlinkSync as readlinkSync3, realpathSync as realpathSync2, unlinkSync as unlinkSync2 } from "node:fs";
|
|
5673
|
+
import { basename as basename2, dirname as dirname4, join as join6, relative as relative4, resolve as resolve5 } from "node:path";
|
|
5446
5674
|
import {
|
|
5447
5675
|
initScope,
|
|
5448
5676
|
inspectStatus,
|
|
@@ -5453,15 +5681,15 @@ import {
|
|
|
5453
5681
|
} from "@delorenj/skillex";
|
|
5454
5682
|
|
|
5455
5683
|
// src/parity/skill-roots.ts
|
|
5456
|
-
import { spawnSync as
|
|
5457
|
-
import { createHash as
|
|
5684
|
+
import { spawnSync as spawnSync3 } from "node:child_process";
|
|
5685
|
+
import { createHash as createHash3 } from "node:crypto";
|
|
5458
5686
|
import {
|
|
5459
5687
|
appendFileSync,
|
|
5460
|
-
lstatSync as
|
|
5688
|
+
lstatSync as lstatSync4,
|
|
5461
5689
|
mkdirSync as mkdirSync2,
|
|
5462
|
-
readFileSync as
|
|
5463
|
-
readdirSync as
|
|
5464
|
-
readlinkSync,
|
|
5690
|
+
readFileSync as readFileSync4,
|
|
5691
|
+
readdirSync as readdirSync3,
|
|
5692
|
+
readlinkSync as readlinkSync2,
|
|
5465
5693
|
realpathSync,
|
|
5466
5694
|
renameSync,
|
|
5467
5695
|
rmSync,
|
|
@@ -5471,15 +5699,15 @@ import {
|
|
|
5471
5699
|
unlinkSync,
|
|
5472
5700
|
writeFileSync as writeFileSync2
|
|
5473
5701
|
} from "node:fs";
|
|
5474
|
-
import { basename, dirname as dirname3, isAbsolute, join as
|
|
5702
|
+
import { basename, dirname as dirname3, isAbsolute as isAbsolute2, join as join5, relative as relative3, resolve as resolve4, sep } from "node:path";
|
|
5475
5703
|
|
|
5476
5704
|
// src/parity/bmad-attestation.ts
|
|
5477
|
-
import { createHash } from "node:crypto";
|
|
5478
|
-
import { existsSync as
|
|
5479
|
-
import { dirname as dirname2, join as
|
|
5705
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
5706
|
+
import { existsSync as existsSync3, lstatSync as lstatSync3, readFileSync as readFileSync3, readdirSync as readdirSync2 } from "node:fs";
|
|
5707
|
+
import { dirname as dirname2, join as join4, relative as relative2 } from "node:path";
|
|
5480
5708
|
import YAML from "yaml";
|
|
5481
5709
|
function safeReadText(path) {
|
|
5482
|
-
return
|
|
5710
|
+
return existsSync3(path) ? readFileSync3(path, "utf8").replace(/\r\n/g, "\n") : null;
|
|
5483
5711
|
}
|
|
5484
5712
|
function parseCsvRows(text) {
|
|
5485
5713
|
const rows = [];
|
|
@@ -5523,7 +5751,7 @@ function csvObjects(text) {
|
|
|
5523
5751
|
return rows.filter((row) => row.some(Boolean)).map((row) => Object.fromEntries(headers.map((header, index) => [header, row[index] ?? ""])));
|
|
5524
5752
|
}
|
|
5525
5753
|
function installedBmadTools(repoRoot) {
|
|
5526
|
-
const raw = safeReadText(
|
|
5754
|
+
const raw = safeReadText(join4(repoRoot, "_bmad", "_config", "manifest.yaml"));
|
|
5527
5755
|
if (!raw) return /* @__PURE__ */ new Set();
|
|
5528
5756
|
try {
|
|
5529
5757
|
const parsed = YAML.parse(raw);
|
|
@@ -5533,8 +5761,8 @@ function installedBmadTools(repoRoot) {
|
|
|
5533
5761
|
}
|
|
5534
5762
|
}
|
|
5535
5763
|
function bmadCliProjectionInventory(repoRoot) {
|
|
5536
|
-
const filesText = safeReadText(
|
|
5537
|
-
const skillsText = safeReadText(
|
|
5764
|
+
const filesText = safeReadText(join4(repoRoot, "_bmad", "_config", "files-manifest.csv"));
|
|
5765
|
+
const skillsText = safeReadText(join4(repoRoot, "_bmad", "_config", "skill-manifest.csv"));
|
|
5538
5766
|
if (!filesText || !skillsText) return { files: /* @__PURE__ */ new Map(), error: "BMAD files/skill manifests are missing" };
|
|
5539
5767
|
const fileHashes = /* @__PURE__ */ new Map();
|
|
5540
5768
|
for (const row of csvObjects(filesText)) {
|
|
@@ -5550,23 +5778,23 @@ function bmadCliProjectionInventory(repoRoot) {
|
|
|
5550
5778
|
const sourceRoot = dirname2(skillPath);
|
|
5551
5779
|
for (const [sourcePath, hash] of fileHashes) {
|
|
5552
5780
|
if (sourcePath !== `${sourceRoot}/SKILL.md` && !sourcePath.startsWith(`${sourceRoot}/`)) continue;
|
|
5553
|
-
const suffix =
|
|
5781
|
+
const suffix = relative2(sourceRoot, sourcePath);
|
|
5554
5782
|
if (!suffix || suffix.startsWith("..")) continue;
|
|
5555
|
-
projected.set(
|
|
5783
|
+
projected.set(join4("skills", canonicalId, suffix), hash);
|
|
5556
5784
|
}
|
|
5557
5785
|
}
|
|
5558
5786
|
return projected.size ? { files: projected } : { files: projected, error: "BMAD manifests contain no projected skill inventory" };
|
|
5559
5787
|
}
|
|
5560
5788
|
function inventoryFilesUnder(root, current = root) {
|
|
5561
|
-
if (!
|
|
5562
|
-
const stat =
|
|
5563
|
-
const rel =
|
|
5789
|
+
if (!existsSync3(current)) return { files: [], unsafe: [] };
|
|
5790
|
+
const stat = lstatSync3(current);
|
|
5791
|
+
const rel = relative2(root, current) || ".";
|
|
5564
5792
|
if (stat.isSymbolicLink()) return { files: [], unsafe: [rel] };
|
|
5565
|
-
if (stat.isFile()) return { files: [
|
|
5793
|
+
if (stat.isFile()) return { files: [relative2(root, current)], unsafe: [] };
|
|
5566
5794
|
if (!stat.isDirectory()) return { files: [], unsafe: [rel] };
|
|
5567
5795
|
const result2 = { files: [], unsafe: [] };
|
|
5568
|
-
for (const name of
|
|
5569
|
-
const child = inventoryFilesUnder(root,
|
|
5796
|
+
for (const name of readdirSync2(current)) {
|
|
5797
|
+
const child = inventoryFilesUnder(root, join4(current, name));
|
|
5570
5798
|
result2.files.push(...child.files);
|
|
5571
5799
|
result2.unsafe.push(...child.unsafe);
|
|
5572
5800
|
}
|
|
@@ -5578,7 +5806,7 @@ function attestBmadInstallerFiles(inventory, base, files, label) {
|
|
|
5578
5806
|
for (const rel of files) {
|
|
5579
5807
|
const expectedHash = inventory.files.get(rel);
|
|
5580
5808
|
if (!expectedHash) return { safe: false, reason: `${label}/${rel} is outside the BMAD generated inventory` };
|
|
5581
|
-
const actualHash =
|
|
5809
|
+
const actualHash = createHash2("sha256").update(readFileSync3(join4(base, rel))).digest("hex");
|
|
5582
5810
|
if (actualHash !== expectedHash) return { safe: false, reason: `${label}/${rel} was locally modified after generation` };
|
|
5583
5811
|
}
|
|
5584
5812
|
return { safe: true, reason: `${files.length} file(s) match BMAD installer inventory and hashes` };
|
|
@@ -5590,7 +5818,7 @@ var SUPPORTED_SKILLS_ALIASES = SUPPORTED_CLIS.map((cli) => cli.skillsRoot);
|
|
|
5590
5818
|
var SKILL_ROOTS_QUARANTINE = ".pjangler-quarantine";
|
|
5591
5819
|
function lstatOrUndefined(path) {
|
|
5592
5820
|
try {
|
|
5593
|
-
return
|
|
5821
|
+
return lstatSync4(path);
|
|
5594
5822
|
} catch (error) {
|
|
5595
5823
|
const code = error.code;
|
|
5596
5824
|
if (code === "ENOENT" || code === "ENOTDIR" || code === "ELOOP" || code === "ENAMETOOLONG") return void 0;
|
|
@@ -5605,17 +5833,17 @@ function realpathOrUndefined(path) {
|
|
|
5605
5833
|
}
|
|
5606
5834
|
}
|
|
5607
5835
|
function within(path, root) {
|
|
5608
|
-
const part =
|
|
5609
|
-
return part === "" || !part.startsWith(`..${sep}`) && part !== ".." && !
|
|
5836
|
+
const part = relative3(root, path);
|
|
5837
|
+
return part === "" || !part.startsWith(`..${sep}`) && part !== ".." && !isAbsolute2(part);
|
|
5610
5838
|
}
|
|
5611
5839
|
function gitToplevel(directory) {
|
|
5612
|
-
const result2 =
|
|
5840
|
+
const result2 = spawnSync3("git", ["-C", directory, "rev-parse", "--show-toplevel"], { encoding: "utf8" });
|
|
5613
5841
|
if (result2.status !== 0) return void 0;
|
|
5614
5842
|
const top = result2.stdout.trim();
|
|
5615
5843
|
return top ? realpathOrUndefined(top) ?? top : void 0;
|
|
5616
5844
|
}
|
|
5617
5845
|
function repoOwnsPath(repoRoot, path) {
|
|
5618
|
-
const repoReal = realpathOrUndefined(repoRoot) ??
|
|
5846
|
+
const repoReal = realpathOrUndefined(repoRoot) ?? resolve4(repoRoot);
|
|
5619
5847
|
const real = realpathOrUndefined(path);
|
|
5620
5848
|
if (!real || !within(real, repoReal)) return false;
|
|
5621
5849
|
let directory = real;
|
|
@@ -5627,7 +5855,7 @@ function repoOwnsPath(repoRoot, path) {
|
|
|
5627
5855
|
return gitToplevel(directory) === gitToplevel(repoReal);
|
|
5628
5856
|
}
|
|
5629
5857
|
function entryOf(path, stat) {
|
|
5630
|
-
if (stat.isSymbolicLink()) return { kind: "link", text:
|
|
5858
|
+
if (stat.isSymbolicLink()) return { kind: "link", text: readlinkSync2(path), stat, phys: path };
|
|
5631
5859
|
if (stat.isDirectory()) return { kind: "dir", stat, phys: path };
|
|
5632
5860
|
if (stat.isFile()) return { kind: "file", stat, phys: path };
|
|
5633
5861
|
return { kind: "special", stat, phys: path };
|
|
@@ -5638,16 +5866,16 @@ var diskLayout = {
|
|
|
5638
5866
|
return stat ? entryOf(path, stat) : void 0;
|
|
5639
5867
|
},
|
|
5640
5868
|
readdir(path) {
|
|
5641
|
-
return
|
|
5869
|
+
return readdirSync3(path);
|
|
5642
5870
|
}
|
|
5643
5871
|
};
|
|
5644
5872
|
function finalLayout(spec) {
|
|
5645
5873
|
const underIncoming = (path) => {
|
|
5646
5874
|
if (!within(path, spec.root) || path === spec.root) return void 0;
|
|
5647
|
-
const [name, ...rest] =
|
|
5875
|
+
const [name, ...rest] = relative3(spec.root, path).split(sep);
|
|
5648
5876
|
const occupant = spec.incoming.get(name);
|
|
5649
5877
|
if (!occupant) return void 0;
|
|
5650
|
-
if (occupant.kind === "move") return { mapped:
|
|
5878
|
+
if (occupant.kind === "move") return { mapped: join5(occupant.from, ...rest) };
|
|
5651
5879
|
return rest.length ? void 0 : { link: occupant.text };
|
|
5652
5880
|
};
|
|
5653
5881
|
return {
|
|
@@ -5662,8 +5890,8 @@ function finalLayout(spec) {
|
|
|
5662
5890
|
},
|
|
5663
5891
|
readdir(path) {
|
|
5664
5892
|
const mapping = underIncoming(path);
|
|
5665
|
-
if (mapping?.mapped) return
|
|
5666
|
-
const names = new Set(lstatOrUndefined(path)?.isDirectory() ?
|
|
5893
|
+
if (mapping?.mapped) return readdirSync3(mapping.mapped);
|
|
5894
|
+
const names = new Set(lstatOrUndefined(path)?.isDirectory() ? readdirSync3(path) : []);
|
|
5667
5895
|
if (path === spec.root) for (const name of spec.incoming.keys()) names.add(name);
|
|
5668
5896
|
for (const created of [...spec.links.keys(), ...spec.dirs]) if (dirname3(created) === path) names.add(basename(created));
|
|
5669
5897
|
return [...names].sort();
|
|
@@ -5703,7 +5931,7 @@ function walkNode(layout, path, root) {
|
|
|
5703
5931
|
if (!entry) return { t: "special" };
|
|
5704
5932
|
if (entry.kind === "link") {
|
|
5705
5933
|
const text = entry.text ?? "";
|
|
5706
|
-
return { t: "link", text, escapes: !
|
|
5934
|
+
return { t: "link", text, escapes: !isAbsolute2(text) && !within(resolve4(dirname3(path), text), root) };
|
|
5707
5935
|
}
|
|
5708
5936
|
if (entry.kind === "file") {
|
|
5709
5937
|
const stat = entry.stat;
|
|
@@ -5711,7 +5939,7 @@ function walkNode(layout, path, root) {
|
|
|
5711
5939
|
}
|
|
5712
5940
|
if (entry.kind === "dir") {
|
|
5713
5941
|
const children = /* @__PURE__ */ new Map();
|
|
5714
|
-
for (const name of layout.readdir(path).sort()) children.set(name, walkNode(layout,
|
|
5942
|
+
for (const name of layout.readdir(path).sort()) children.set(name, walkNode(layout, join5(path, name), root));
|
|
5715
5943
|
return { t: "dir", children };
|
|
5716
5944
|
}
|
|
5717
5945
|
return { t: "special" };
|
|
@@ -5731,7 +5959,7 @@ var Hasher = class {
|
|
|
5731
5959
|
if (this.cache.has(key)) return this.cache.get(key);
|
|
5732
5960
|
const path = before ? this.translate(node.phys) : node.phys;
|
|
5733
5961
|
const stat = lstatOrUndefined(path);
|
|
5734
|
-
const digest = stat && stat.ino === node.ino && stat.dev === node.dev ?
|
|
5962
|
+
const digest = stat && stat.ino === node.ino && stat.dev === node.dev ? createHash3("sha256").update(readFileSync4(path)).digest("hex") : void 0;
|
|
5735
5963
|
this.cache.set(key, digest);
|
|
5736
5964
|
return digest;
|
|
5737
5965
|
}
|
|
@@ -5809,34 +6037,34 @@ function shellQuote(value) {
|
|
|
5809
6037
|
function describeQuarantine(quarantine, label) {
|
|
5810
6038
|
let runs;
|
|
5811
6039
|
try {
|
|
5812
|
-
runs =
|
|
6040
|
+
runs = readdirSync3(quarantine).sort();
|
|
5813
6041
|
} catch {
|
|
5814
6042
|
return [`${label(quarantine)} exists but cannot be read; inspect it before converting any CLI skills root`];
|
|
5815
6043
|
}
|
|
5816
6044
|
if (!runs.length) return [`${label(quarantine)} is an empty leftover of an earlier conversion; remove it: rmdir ${shellQuote(quarantine)}`];
|
|
5817
6045
|
return runs.map((run) => {
|
|
5818
|
-
const path =
|
|
6046
|
+
const path = join5(quarantine, run);
|
|
5819
6047
|
let progress;
|
|
5820
6048
|
try {
|
|
5821
|
-
progress =
|
|
6049
|
+
progress = readFileSync4(join5(path, "progress.log"), "utf8");
|
|
5822
6050
|
} catch {
|
|
5823
6051
|
}
|
|
5824
6052
|
if (progress !== void 0 && /^committed$/m.test(progress)) {
|
|
5825
6053
|
return `an earlier conversion completed and was verified, but its quarantine of proven duplicates remains at ${label(path)}; remove it: rm -rf ${shellQuote(path)}`;
|
|
5826
6054
|
}
|
|
5827
6055
|
if (progress !== void 0 && /^rolled-back$/m.test(progress)) {
|
|
5828
|
-
return `an earlier conversion was rolled back, but ${label(
|
|
6056
|
+
return `an earlier conversion was rolled back, but ${label(join5(path, "items"))} still holds entries nothing planned to put there; inspect them, then remove ${shellQuote(path)}`;
|
|
5829
6057
|
}
|
|
5830
|
-
if (progress === void 0 || !lstatOrUndefined(
|
|
6058
|
+
if (progress === void 0 || !lstatOrUndefined(join5(path, "restore.sh"))) {
|
|
5831
6059
|
return `an interrupted conversion left ${label(path)} before its first step (no journal), so nothing was changed; remove it: rm -rf ${shellQuote(path)}`;
|
|
5832
6060
|
}
|
|
5833
|
-
return `an interrupted CLI skills-root conversion left ${label(path)}; restore the repository exactly with: sh ${shellQuote(
|
|
6061
|
+
return `an interrupted CLI skills-root conversion left ${label(path)}; restore the repository exactly with: sh ${shellQuote(join5(path, "restore.sh"))} (it reverses every completed step listed in journal.json, then removes the quarantine)`;
|
|
5834
6062
|
});
|
|
5835
6063
|
}
|
|
5836
6064
|
function indexRows(repoReal, rel) {
|
|
5837
|
-
const probe =
|
|
6065
|
+
const probe = spawnSync3("git", ["rev-parse", "--is-inside-work-tree"], { cwd: repoReal, encoding: "utf8" });
|
|
5838
6066
|
if (probe.status !== 0 || probe.stdout.trim() !== "true") return { rows: [], worktree: false };
|
|
5839
|
-
const listed =
|
|
6067
|
+
const listed = spawnSync3("git", ["ls-files", "-s", "-z", "--", rel], { cwd: repoReal, encoding: "utf8", maxBuffer: 64 * 1024 * 1024 });
|
|
5840
6068
|
if (listed.status !== 0) return { rows: [], worktree: true, error: `git ls-files failed for ${rel}: ${listed.stderr.trim()}` };
|
|
5841
6069
|
const rows = listed.stdout.split("\0").filter(Boolean).map((line) => {
|
|
5842
6070
|
const [meta, path] = line.split(" ");
|
|
@@ -5846,19 +6074,19 @@ function indexRows(repoReal, rel) {
|
|
|
5846
6074
|
}
|
|
5847
6075
|
function entryHazard(path, label) {
|
|
5848
6076
|
const walk = (current, rel) => {
|
|
5849
|
-
const stat =
|
|
6077
|
+
const stat = lstatSync4(current);
|
|
5850
6078
|
if (rel && basename(current) === ".git") return `${label} contains a nested git repository (${rel}); refusing to move or drop another repository's work tree`;
|
|
5851
6079
|
if (stat.isSymbolicLink()) {
|
|
5852
|
-
const text =
|
|
5853
|
-
if (!
|
|
6080
|
+
const text = readlinkSync2(current);
|
|
6081
|
+
if (!isAbsolute2(text) && !within(resolve4(dirname3(current), text), path)) {
|
|
5854
6082
|
return `${label}/${rel} -> ${text} leaves the entry; moving or dropping it would change what that link resolves to`;
|
|
5855
6083
|
}
|
|
5856
6084
|
return void 0;
|
|
5857
6085
|
}
|
|
5858
6086
|
if (stat.isFile()) return void 0;
|
|
5859
6087
|
if (stat.isDirectory()) {
|
|
5860
|
-
for (const name of
|
|
5861
|
-
const found = walk(
|
|
6088
|
+
for (const name of readdirSync3(current).sort()) {
|
|
6089
|
+
const found = walk(join5(current, name), rel ? `${rel}/${name}` : name);
|
|
5862
6090
|
if (found) return found;
|
|
5863
6091
|
}
|
|
5864
6092
|
return void 0;
|
|
@@ -5869,25 +6097,25 @@ function entryHazard(path, label) {
|
|
|
5869
6097
|
}
|
|
5870
6098
|
function planSkillRoots(repoRoot, options = {}) {
|
|
5871
6099
|
const selected = options.aliases ?? SUPPORTED_SKILLS_ALIASES;
|
|
5872
|
-
const lexicalRoot =
|
|
6100
|
+
const lexicalRoot = join5(repoRoot, ".agents", "skills");
|
|
5873
6101
|
const repoReal = realpathOrUndefined(repoRoot);
|
|
5874
6102
|
const empty = (rootState2, reason) => {
|
|
5875
|
-
const aliases2 = selected.map((alias) => ({ alias, path:
|
|
6103
|
+
const aliases2 = selected.map((alias) => ({ alias, path: join5(repoRoot, alias), state: "unsupported", operations: [], blocks: [reason], summary: `${alias}: blocked (${reason})` }));
|
|
5876
6104
|
return { repoRoot, root: lexicalRoot, rootState: rootState2, aliases: aliases2, operations: [], blocks: [reason], clean: false, incoming: [] };
|
|
5877
6105
|
};
|
|
5878
6106
|
if (!repoReal) return empty("unsafe", `${repoRoot} does not resolve; refusing to plan any CLI skills root`);
|
|
5879
|
-
const lex = (path) => within(path, repoReal) ?
|
|
5880
|
-
const rel = (path) =>
|
|
5881
|
-
const agents =
|
|
5882
|
-
const R =
|
|
6107
|
+
const lex = (path) => within(path, repoReal) ? join5(repoRoot, relative3(repoReal, path)) : path;
|
|
6108
|
+
const rel = (path) => relative3(repoReal, path) || ".";
|
|
6109
|
+
const agents = join5(repoReal, ".agents");
|
|
6110
|
+
const R = join5(agents, "skills");
|
|
5883
6111
|
const agentsStat = lstatOrUndefined(agents);
|
|
5884
6112
|
if (agentsStat && (agentsStat.isSymbolicLink() || !agentsStat.isDirectory())) {
|
|
5885
|
-
const what = agentsStat.isSymbolicLink() ? `a symlink (to ${
|
|
6113
|
+
const what = agentsStat.isSymbolicLink() ? `a symlink (to ${readlinkSync2(agents)})` : "not a directory";
|
|
5886
6114
|
return empty("unsafe", `.agents is ${what}; the activation root must be a real .agents/skills directory inside the repository, so no CLI skills root is planned`);
|
|
5887
6115
|
}
|
|
5888
6116
|
const rootStat = agentsStat ? lstatOrUndefined(R) : void 0;
|
|
5889
6117
|
const rootState = !rootStat ? "absent" : rootStat.isSymbolicLink() ? "symlink" : rootStat.isDirectory() ? "directory" : "other";
|
|
5890
|
-
const quarantine =
|
|
6118
|
+
const quarantine = join5(agents, SKILL_ROOTS_QUARANTINE);
|
|
5891
6119
|
if (lstatOrUndefined(quarantine)) {
|
|
5892
6120
|
const reasons = describeQuarantine(quarantine, (path) => rel(path));
|
|
5893
6121
|
const plan2 = empty(rootState, reasons[0]);
|
|
@@ -5907,15 +6135,15 @@ function planSkillRoots(repoRoot, options = {}) {
|
|
|
5907
6135
|
const repoTop = gitToplevel(repoReal);
|
|
5908
6136
|
let inventory;
|
|
5909
6137
|
const inventoryOf = () => inventory ??= bmadCliProjectionInventory(repoReal);
|
|
5910
|
-
const realAliasDirs = SUPPORTED_SKILLS_ALIASES.map((alias) =>
|
|
6138
|
+
const realAliasDirs = SUPPORTED_SKILLS_ALIASES.map((alias) => join5(repoReal, alias)).filter((path) => {
|
|
5911
6139
|
const stat = lstatOrUndefined(path);
|
|
5912
6140
|
return Boolean(stat?.isDirectory() && !stat.isSymbolicLink() && !lstatOrUndefined(dirname3(path))?.isSymbolicLink());
|
|
5913
6141
|
});
|
|
5914
6142
|
const infos = selected.map((alias) => {
|
|
5915
|
-
const real =
|
|
6143
|
+
const real = join5(repoReal, alias);
|
|
5916
6144
|
const cliRoot = dirname3(real);
|
|
5917
6145
|
const cliName = rel(cliRoot);
|
|
5918
|
-
const info = { alias, path:
|
|
6146
|
+
const info = { alias, path: join5(repoRoot, alias), real, cliRoot, cliName, state: "unsupported", cliRootExists: false, blocks: [], candidates: [] };
|
|
5919
6147
|
if (rootState === "other") {
|
|
5920
6148
|
info.blocks.push(`.agents/skills is not a directory; refusing to plan ${alias}`);
|
|
5921
6149
|
return info;
|
|
@@ -5939,10 +6167,10 @@ function planSkillRoots(repoRoot, options = {}) {
|
|
|
5939
6167
|
return info;
|
|
5940
6168
|
}
|
|
5941
6169
|
if (stat.isSymbolicLink()) {
|
|
5942
|
-
const text =
|
|
6170
|
+
const text = readlinkSync2(real);
|
|
5943
6171
|
info.text = text;
|
|
5944
6172
|
const reached = realpathOrUndefined(real);
|
|
5945
|
-
if (rootReal !== void 0 && reached === rootReal || rootState === "absent" &&
|
|
6173
|
+
if (rootReal !== void 0 && reached === rootReal || rootState === "absent" && resolve4(cliRoot, text) === R) {
|
|
5946
6174
|
info.state = "alias";
|
|
5947
6175
|
return info;
|
|
5948
6176
|
}
|
|
@@ -5989,16 +6217,16 @@ function planSkillRoots(repoRoot, options = {}) {
|
|
|
5989
6217
|
const name = row.path.slice(aliasRel.length + 1).split("/")[0];
|
|
5990
6218
|
tracked.set(name, [...tracked.get(name) ?? [], row]);
|
|
5991
6219
|
}
|
|
5992
|
-
for (const name of
|
|
5993
|
-
const path =
|
|
6220
|
+
for (const name of readdirSync3(real).sort()) {
|
|
6221
|
+
const path = join5(real, name);
|
|
5994
6222
|
const label = `${alias}/${name}`;
|
|
5995
|
-
const entryStat =
|
|
6223
|
+
const entryStat = lstatSync4(path);
|
|
5996
6224
|
const rows = tracked.get(name) ?? [];
|
|
5997
6225
|
const candidate = { alias, name, path, label, kind: entryStat.isSymbolicLink() ? "link" : entryStat.isDirectory() ? "directory" : "file", tracked: rows.map((row) => row.path) };
|
|
5998
6226
|
if (name === ".git") {
|
|
5999
6227
|
candidate.staticBlock = `${label} is git metadata; refusing to relocate another repository's work tree`;
|
|
6000
6228
|
} else if (entryStat.isSymbolicLink()) {
|
|
6001
|
-
candidate.text =
|
|
6229
|
+
candidate.text = readlinkSync2(path);
|
|
6002
6230
|
candidate.resolution = resolveIn(diskLayout, path);
|
|
6003
6231
|
if (rows.length) candidate.staticBlock = `${label} -> ${candidate.text} is tracked in git; a tracked link cannot be attested BMAD installer output, so it is never untracked, moved or dropped`;
|
|
6004
6232
|
} else if (!entryStat.isDirectory() && !entryStat.isFile()) {
|
|
@@ -6012,10 +6240,10 @@ function planSkillRoots(repoRoot, options = {}) {
|
|
|
6012
6240
|
if (!candidate.staticBlock && linkRow) candidate.staticBlock = `${label} holds a tracked symlink (${linkRow.path}); a tracked link cannot be attested BMAD installer output`;
|
|
6013
6241
|
if (!candidate.staticBlock && rows.length) {
|
|
6014
6242
|
const paths = rows.map((row) => row.path);
|
|
6015
|
-
const missing = paths.filter((path2) => !lstatOrUndefined(
|
|
6243
|
+
const missing = paths.filter((path2) => !lstatOrUndefined(join5(repoReal, path2)));
|
|
6016
6244
|
if (missing.length) candidate.staticBlock = `${label} has tracked path(s) missing from the work tree (${missing[0]}); restore or commit the deletion first`;
|
|
6017
6245
|
else {
|
|
6018
|
-
const verdict = attestBmadInstallerFiles(inventoryOf(), cliRoot, paths.map((path2) =>
|
|
6246
|
+
const verdict = attestBmadInstallerFiles(inventoryOf(), cliRoot, paths.map((path2) => relative3(cliRoot, join5(repoReal, path2))), cliName);
|
|
6019
6247
|
if (!verdict.safe) candidate.staticBlock = `${label} is tracked in git and not attested BMAD installer output: ${verdict.reason}`;
|
|
6020
6248
|
}
|
|
6021
6249
|
}
|
|
@@ -6064,15 +6292,15 @@ function planSkillRoots(repoRoot, options = {}) {
|
|
|
6064
6292
|
};
|
|
6065
6293
|
const finalText = (candidate) => {
|
|
6066
6294
|
const text = candidate.text;
|
|
6067
|
-
if (
|
|
6068
|
-
const lexical =
|
|
6295
|
+
if (isAbsolute2(text)) return text;
|
|
6296
|
+
const lexical = resolve4(dirname3(candidate.path), text);
|
|
6069
6297
|
const home = activeDirs.find((dir) => within(lexical, dir));
|
|
6070
|
-
const mapped = home ?
|
|
6071
|
-
return
|
|
6298
|
+
const mapped = home ? join5(R, relative3(home, lexical)) : lexical;
|
|
6299
|
+
return relative3(R, mapped) || ".";
|
|
6072
6300
|
};
|
|
6073
6301
|
for (const name of [...byName.keys()].sort()) {
|
|
6074
6302
|
const candidates = byName.get(name);
|
|
6075
|
-
const counterpart =
|
|
6303
|
+
const counterpart = join5(R, name);
|
|
6076
6304
|
const counterpartLabel = `.agents/skills/${name}`;
|
|
6077
6305
|
const live = [];
|
|
6078
6306
|
for (const candidate of candidates) {
|
|
@@ -6087,7 +6315,7 @@ function planSkillRoots(repoRoot, options = {}) {
|
|
|
6087
6315
|
else decide(candidate, "drop-duplicate-entry", `drop ${candidate.label} (every entry already exists byte-identical in ${where})`, extra);
|
|
6088
6316
|
};
|
|
6089
6317
|
if (rootEntry) {
|
|
6090
|
-
const counterpartText = rootEntry.isSymbolicLink() ?
|
|
6318
|
+
const counterpartText = rootEntry.isSymbolicLink() ? readlinkSync2(counterpart) : void 0;
|
|
6091
6319
|
const rootView = beforeView(counterpart);
|
|
6092
6320
|
if (rootEntry.isSymbolicLink()) {
|
|
6093
6321
|
if (!rootView) {
|
|
@@ -6181,7 +6409,7 @@ function planSkillRoots(repoRoot, options = {}) {
|
|
|
6181
6409
|
const responsible = converting.filter((info) => within(path, info.real) || [...view.hops, view.real].some((hop) => within(hop, info.real)));
|
|
6182
6410
|
for (const hop of [path, ...view.hops, view.real]) {
|
|
6183
6411
|
if (!within(hop, R) || hop === R) continue;
|
|
6184
|
-
const writer = incomingByName.get(
|
|
6412
|
+
const writer = incomingByName.get(relative3(R, hop).split(sep)[0]);
|
|
6185
6413
|
const info = writer && converting.find((item) => item.alias === writer.alias);
|
|
6186
6414
|
if (info && !responsible.includes(info)) responsible.push(info);
|
|
6187
6415
|
}
|
|
@@ -6267,10 +6495,10 @@ function planSkillRoots(repoRoot, options = {}) {
|
|
|
6267
6495
|
const after = finalLayout(spec);
|
|
6268
6496
|
for (const [name, entry] of incomingByName) {
|
|
6269
6497
|
if (blocked.has(entry.alias)) continue;
|
|
6270
|
-
const resolution = resolveIn(after,
|
|
6498
|
+
const resolution = resolveIn(after, join5(R, name));
|
|
6271
6499
|
const phys = resolution ? after.lstat(resolution.real)?.phys ?? resolution.real : void 0;
|
|
6272
|
-
const text = entry.kind !== "link" ? void 0 : entry.incoming.kind === "link" ? entry.incoming.text :
|
|
6273
|
-
incoming.push({ name, path:
|
|
6500
|
+
const text = entry.kind !== "link" ? void 0 : entry.incoming.kind === "link" ? entry.incoming.text : readlinkSync2(entry.from);
|
|
6501
|
+
incoming.push({ name, path: join5(lexicalRoot, name), from: lex(entry.from), kind: entry.kind, ...text === void 0 ? {} : { text }, resolves: phys ? lex(phys) : void 0 });
|
|
6274
6502
|
}
|
|
6275
6503
|
}
|
|
6276
6504
|
const plan = { repoRoot, root: lexicalRoot, rootState, aliases, operations, blocks, clean: blocks.length === 0, incoming };
|
|
@@ -6282,26 +6510,26 @@ function planSkillRoots(repoRoot, options = {}) {
|
|
|
6282
6510
|
agentsExisted: Boolean(agentsStat),
|
|
6283
6511
|
before,
|
|
6284
6512
|
affected,
|
|
6285
|
-
converted: converted.map((entry) =>
|
|
6286
|
-
created: aliases.filter((entry) => entry.operations.some((operation) => operation.kind === "create-alias" || operation.kind === "replace-dangling-alias")).map((entry) =>
|
|
6513
|
+
converted: converted.map((entry) => join5(repoReal, entry.alias)),
|
|
6514
|
+
created: aliases.filter((entry) => entry.operations.some((operation) => operation.kind === "create-alias" || operation.kind === "replace-dangling-alias")).map((entry) => join5(repoReal, entry.alias))
|
|
6287
6515
|
});
|
|
6288
6516
|
return plan;
|
|
6289
6517
|
}
|
|
6290
6518
|
function visibleViews(repoReal, R, view) {
|
|
6291
6519
|
const views = /* @__PURE__ */ new Map();
|
|
6292
6520
|
const seen = /* @__PURE__ */ new Set();
|
|
6293
|
-
for (const location of [R, ...SUPPORTED_SKILLS_ALIASES.map((alias) =>
|
|
6521
|
+
for (const location of [R, ...SUPPORTED_SKILLS_ALIASES.map((alias) => join5(repoReal, alias))]) {
|
|
6294
6522
|
const resolved = realpathOrUndefined(location);
|
|
6295
6523
|
if (!resolved || seen.has(resolved)) continue;
|
|
6296
6524
|
seen.add(resolved);
|
|
6297
6525
|
let names;
|
|
6298
6526
|
try {
|
|
6299
|
-
names =
|
|
6527
|
+
names = readdirSync3(location);
|
|
6300
6528
|
} catch {
|
|
6301
6529
|
continue;
|
|
6302
6530
|
}
|
|
6303
6531
|
for (const name of names.sort()) {
|
|
6304
|
-
const path =
|
|
6532
|
+
const path = join5(location, name);
|
|
6305
6533
|
const found = view(path);
|
|
6306
6534
|
if (found) views.set(path, found);
|
|
6307
6535
|
}
|
|
@@ -6317,12 +6545,12 @@ function changedPaths(operation) {
|
|
|
6317
6545
|
return operation.from ? [operation.from, operation.path] : [operation.path];
|
|
6318
6546
|
}
|
|
6319
6547
|
function compileSteps(plan, facts, items) {
|
|
6320
|
-
const physical = (path) =>
|
|
6548
|
+
const physical = (path) => join5(facts.repoReal, relative3(plan.repoRoot, path));
|
|
6321
6549
|
const steps = [];
|
|
6322
6550
|
const park = (from, kind) => {
|
|
6323
6551
|
const stat = lstatOrUndefined(from);
|
|
6324
|
-
const text = kind === "link" && stat?.isSymbolicLink() ?
|
|
6325
|
-
steps.push({ step: "rename", from, to:
|
|
6552
|
+
const text = kind === "link" && stat?.isSymbolicLink() ? readlinkSync2(from) : void 0;
|
|
6553
|
+
steps.push({ step: "rename", from, to: join5(items, `${steps.length}-${basename(from)}`), park: kind, ...text === void 0 ? {} : { text } });
|
|
6326
6554
|
};
|
|
6327
6555
|
for (const operation of plan.operations) {
|
|
6328
6556
|
const path = physical(operation.path);
|
|
@@ -6425,15 +6653,15 @@ function runStep(step) {
|
|
|
6425
6653
|
return;
|
|
6426
6654
|
}
|
|
6427
6655
|
if (lstatOrUndefined(step.to)) throw new Error(`${step.to} appeared before the move`);
|
|
6428
|
-
if (step.park === "dir" &&
|
|
6429
|
-
if (step.park === "link" && (!lstatOrUndefined(step.from)?.isSymbolicLink() ||
|
|
6656
|
+
if (step.park === "dir" && readdirSync3(step.from).length) throw new Error(`${step.from} is not empty (${readdirSync3(step.from)[0]})`);
|
|
6657
|
+
if (step.park === "link" && (!lstatOrUndefined(step.from)?.isSymbolicLink() || readlinkSync2(step.from) !== step.text)) throw new Error(`${step.from} is no longer the link that was planned`);
|
|
6430
6658
|
renameSync(step.from, step.to);
|
|
6431
6659
|
}
|
|
6432
6660
|
function parkedGap(step, before, affected, hasher) {
|
|
6433
6661
|
const stat = lstatOrUndefined(step.to);
|
|
6434
6662
|
if (!stat) return `${step.to} is missing from the quarantine`;
|
|
6435
|
-
if (step.park === "dir") return
|
|
6436
|
-
if (step.park === "link") return stat.isSymbolicLink() &&
|
|
6663
|
+
if (step.park === "dir") return readdirSync3(step.to).length ? `${step.to} is no longer empty` : void 0;
|
|
6664
|
+
if (step.park === "link") return stat.isSymbolicLink() && readlinkSync2(step.to) === step.text ? void 0 : `${step.to} is no longer the planned link`;
|
|
6437
6665
|
if (!before?.node) return `${step.from} had no recorded content to compare`;
|
|
6438
6666
|
const parked = viewIn(diskLayout, step.to, affected);
|
|
6439
6667
|
if (!parked) return `${step.to} does not resolve`;
|
|
@@ -6447,7 +6675,7 @@ function undoStep(step) {
|
|
|
6447
6675
|
}
|
|
6448
6676
|
if (step.step === "symlink") {
|
|
6449
6677
|
const stat = lstatOrUndefined(step.path);
|
|
6450
|
-
if (!stat?.isSymbolicLink() ||
|
|
6678
|
+
if (!stat?.isSymbolicLink() || readlinkSync2(step.path) !== step.text) throw new Error(`${step.path} is not the link this run created`);
|
|
6451
6679
|
unlinkSync(step.path);
|
|
6452
6680
|
return;
|
|
6453
6681
|
}
|
|
@@ -6459,7 +6687,7 @@ function translator(steps, done) {
|
|
|
6459
6687
|
let current = path;
|
|
6460
6688
|
for (let index = 0; index < done; index++) {
|
|
6461
6689
|
const step = steps[index];
|
|
6462
|
-
if (step.step === "rename" && within(current, step.from)) current =
|
|
6690
|
+
if (step.step === "rename" && within(current, step.from)) current = join5(step.to, relative3(step.from, current));
|
|
6463
6691
|
}
|
|
6464
6692
|
return current;
|
|
6465
6693
|
};
|
|
@@ -6479,10 +6707,10 @@ function applySkillRoots(repoRoot, options) {
|
|
|
6479
6707
|
if (!plan.operations.length) return { ok: plan.clean, changedFiles: [], details: [], blocks: plan.blocks, plan };
|
|
6480
6708
|
const facts = internals.get(plan);
|
|
6481
6709
|
const failed = (reason) => ({ ok: false, changedFiles: [], details: [], blocks: [...plan.blocks, reason], plan });
|
|
6482
|
-
const agents =
|
|
6483
|
-
const quarantine =
|
|
6484
|
-
const run =
|
|
6485
|
-
const items =
|
|
6710
|
+
const agents = join5(facts.repoReal, ".agents");
|
|
6711
|
+
const quarantine = join5(agents, SKILL_ROOTS_QUARANTINE);
|
|
6712
|
+
const run = join5(quarantine, `${Date.now()}-${process.pid}`);
|
|
6713
|
+
const items = join5(run, "items");
|
|
6486
6714
|
const createdAgents = !lstatOrUndefined(agents);
|
|
6487
6715
|
try {
|
|
6488
6716
|
if (createdAgents) mkdirSync2(agents);
|
|
@@ -6492,12 +6720,12 @@ function applySkillRoots(repoRoot, options) {
|
|
|
6492
6720
|
return failed(`could not create the quarantine ${quarantine}: ${error instanceof Error ? error.message : String(error)}`);
|
|
6493
6721
|
}
|
|
6494
6722
|
const steps = compileSteps(plan, facts, items);
|
|
6495
|
-
const log =
|
|
6723
|
+
const log = join5(run, "progress.log");
|
|
6496
6724
|
try {
|
|
6497
|
-
writeFileSync2(
|
|
6498
|
-
writeFileSync2(
|
|
6725
|
+
writeFileSync2(join5(run, ".gitignore"), "*\n");
|
|
6726
|
+
writeFileSync2(join5(run, "journal.json"), `${JSON.stringify({ version: 1, repo: facts.repoReal, createdAgents, steps, operations: plan.operations.map((operation) => operation.detail) }, null, 2)}
|
|
6499
6727
|
`);
|
|
6500
|
-
writeFileSync2(
|
|
6728
|
+
writeFileSync2(join5(run, "restore.sh"), restoreScript(run, quarantine, createdAgents ? agents : void 0, facts.repoReal, steps), { mode: 493 });
|
|
6501
6729
|
writeFileSync2(log, "");
|
|
6502
6730
|
} catch (error) {
|
|
6503
6731
|
rmSync(run, { recursive: true, force: true });
|
|
@@ -6535,11 +6763,11 @@ function applySkillRoots(repoRoot, options) {
|
|
|
6535
6763
|
appendFileSync(log, `undone ${index}
|
|
6536
6764
|
`);
|
|
6537
6765
|
} catch (error) {
|
|
6538
|
-
return failed(`${reason}; rollback stopped at step ${index} (${error instanceof Error ? error.message : String(error)}): finish it exactly with: sh ${shellQuote(
|
|
6766
|
+
return failed(`${reason}; rollback stopped at step ${index} (${error instanceof Error ? error.message : String(error)}): finish it exactly with: sh ${shellQuote(join5(run, "restore.sh"))}`);
|
|
6539
6767
|
}
|
|
6540
6768
|
}
|
|
6541
6769
|
appendFileSync(log, "rolled-back\n");
|
|
6542
|
-
if (
|
|
6770
|
+
if (readdirSync3(items).length) return failed(`${reason}; rolled back, but ${items} is not empty \u2014 inspect it, then remove ${run}`);
|
|
6543
6771
|
cleanup();
|
|
6544
6772
|
return failed(`rolled back: ${reason}`);
|
|
6545
6773
|
};
|
|
@@ -6560,12 +6788,12 @@ function applySkillRoots(repoRoot, options) {
|
|
|
6560
6788
|
const mismatches = [];
|
|
6561
6789
|
for (const [path, view] of facts.before) {
|
|
6562
6790
|
const gap = viewGap(view, viewIn(diskLayout, path, facts.affected), hasher);
|
|
6563
|
-
if (gap) mismatches.push(`${
|
|
6791
|
+
if (gap) mismatches.push(`${relative3(facts.repoReal, path)}: ${gap}`);
|
|
6564
6792
|
}
|
|
6565
6793
|
const rootReal = realpathOrUndefined(facts.R);
|
|
6566
6794
|
for (const alias of [...facts.converted, ...facts.created]) {
|
|
6567
|
-
const reaches = rootReal ? realpathOrUndefined(alias) === rootReal :
|
|
6568
|
-
if (!reaches) mismatches.push(`${
|
|
6795
|
+
const reaches = rootReal ? realpathOrUndefined(alias) === rootReal : resolve4(dirname3(alias), readlinkSync2(alias)) === facts.R;
|
|
6796
|
+
if (!reaches) mismatches.push(`${relative3(facts.repoReal, alias)} does not resolve to .agents/skills`);
|
|
6569
6797
|
}
|
|
6570
6798
|
for (const step of steps) {
|
|
6571
6799
|
if (step.step !== "rename" || !step.park) continue;
|
|
@@ -6575,7 +6803,7 @@ function applySkillRoots(repoRoot, options) {
|
|
|
6575
6803
|
if (mismatches.length) return rollback(mismatches.slice(0, 5).join("; "));
|
|
6576
6804
|
const tracked = plan.operations.flatMap((operation) => operation.kind === "untrack" ? operation.tracked ?? [] : []);
|
|
6577
6805
|
if (tracked.length) {
|
|
6578
|
-
const removed =
|
|
6806
|
+
const removed = spawnSync3(
|
|
6579
6807
|
"git",
|
|
6580
6808
|
["rm", "-r", "--cached", "--quiet", "--pathspec-from-file=-", "--pathspec-file-nul"],
|
|
6581
6809
|
{ cwd: facts.repoReal, encoding: "utf8", input: `${tracked.join("\0")}\0` }
|
|
@@ -6597,32 +6825,32 @@ function shellQuotePath(path) {
|
|
|
6597
6825
|
var PJ_SKILLS = "pj skills";
|
|
6598
6826
|
function skillCoreOptions(ctx) {
|
|
6599
6827
|
return {
|
|
6600
|
-
cwd:
|
|
6601
|
-
project:
|
|
6828
|
+
cwd: resolve5(ctx.repoRoot),
|
|
6829
|
+
project: resolve5(ctx.repoRoot),
|
|
6602
6830
|
scope: "project",
|
|
6603
6831
|
home: ctx.homeDir,
|
|
6604
6832
|
env: process.env,
|
|
6605
6833
|
// Preserve the documented PJangler override while using one core resolver.
|
|
6606
|
-
...process.env.PJ_SKILLS_REGISTRY_ROOT?.trim() ? { registryRoot:
|
|
6834
|
+
...process.env.PJ_SKILLS_REGISTRY_ROOT?.trim() ? { registryRoot: resolve5(process.env.PJ_SKILLS_REGISTRY_ROOT.trim()) } : {}
|
|
6607
6835
|
};
|
|
6608
6836
|
}
|
|
6609
6837
|
function relaySkillexCommands(text) {
|
|
6610
6838
|
return text.replace(SKILLEX_COMMAND, PJ_SKILLS);
|
|
6611
6839
|
}
|
|
6612
|
-
var SKILLEX_COMMAND = /(?<![\w/@.:-])skillex(?=\s+(?:skill|set|pack|init|enable|disable|inherit|sync|status|explain|doctor|vendor|profile|migrate)\b)/g;
|
|
6840
|
+
var SKILLEX_COMMAND = /(?<![\w/@.:-])skillex(?=\s+(?:skill|set|pack|bmad|init|enable|disable|inherit|normalize|sync|status|explain|doctor|vendor|profile|migrate|integrations|clients|root)\b)/g;
|
|
6613
6841
|
function skillDiagnostics(findings) {
|
|
6614
6842
|
return findings.map(
|
|
6615
6843
|
(finding2) => `${finding2.code}: ${finding2.message}${finding2.path ? ` (${finding2.path})` : ""}${finding2.fix ? ` \u2014 ${relaySkillexCommands(finding2.fix)}` : ""}`
|
|
6616
6844
|
);
|
|
6617
6845
|
}
|
|
6618
6846
|
function migrationGuidance(ctx) {
|
|
6619
|
-
const project = `--project ${shellQuotePath(
|
|
6847
|
+
const project = `--project ${shellQuotePath(resolve5(ctx.repoRoot))}`;
|
|
6620
6848
|
return `Legacy selections or skill-root content need explicit review. Preview: ${PJ_SKILLS} migrate ${project}. Apply the reviewed plan: ${PJ_SKILLS} migrate ${project} --apply (add --mapping <file> only for content the preview reports as ambiguous). PJangler relocates CLI-root skill entries losslessly into .agents/skills so each root can become the alias, but never claims ownership of foreign or installer-owned skills and never deletes content that is not a proven duplicate.`;
|
|
6621
6849
|
}
|
|
6622
6850
|
function projectPaths(ctx, rel) {
|
|
6623
|
-
const paths = /* @__PURE__ */ new Set([
|
|
6851
|
+
const paths = /* @__PURE__ */ new Set([join6(resolve5(ctx.repoRoot), rel)]);
|
|
6624
6852
|
try {
|
|
6625
|
-
paths.add(
|
|
6853
|
+
paths.add(join6(realpathSync2(ctx.repoRoot), rel));
|
|
6626
6854
|
} catch {
|
|
6627
6855
|
}
|
|
6628
6856
|
return paths;
|
|
@@ -6632,7 +6860,7 @@ function activationConflict(ctx, findings) {
|
|
|
6632
6860
|
if (!finding2?.path) return void 0;
|
|
6633
6861
|
const aliases = new Set(SUPPORTED_SKILLS_ALIASES.flatMap((alias) => [...projectPaths(ctx, alias)]));
|
|
6634
6862
|
if (aliases.has(finding2.path)) return { kind: "alias", path: finding2.path, name: basename2(dirname4(finding2.path)) };
|
|
6635
|
-
if (projectPaths(ctx,
|
|
6863
|
+
if (projectPaths(ctx, join6(".agents", "skills")).has(dirname4(finding2.path)) && /collides with foreign activation content/.test(finding2.message)) {
|
|
6636
6864
|
return { kind: "root", path: finding2.path, name: basename2(finding2.path) };
|
|
6637
6865
|
}
|
|
6638
6866
|
return void 0;
|
|
@@ -6647,10 +6875,10 @@ async function canonicalSkillPath(ctx, name) {
|
|
|
6647
6875
|
return (await projectBindings(ctx)).find((binding) => binding.name === name)?.path;
|
|
6648
6876
|
}
|
|
6649
6877
|
async function classifyRootCollision(ctx, path) {
|
|
6650
|
-
const label =
|
|
6878
|
+
const label = relative4(resolve5(ctx.repoRoot), path) || path;
|
|
6651
6879
|
let stat;
|
|
6652
6880
|
try {
|
|
6653
|
-
stat =
|
|
6881
|
+
stat = lstatSync5(path);
|
|
6654
6882
|
} catch {
|
|
6655
6883
|
return { action: "block", detail: `${label} disappeared while resolving a root collision` };
|
|
6656
6884
|
}
|
|
@@ -6661,7 +6889,7 @@ async function classifyRootCollision(ctx, path) {
|
|
|
6661
6889
|
} catch {
|
|
6662
6890
|
real = void 0;
|
|
6663
6891
|
}
|
|
6664
|
-
return classifyCollidingLink(ctx, basename2(path), label,
|
|
6892
|
+
return classifyCollidingLink(ctx, basename2(path), label, readlinkSync3(path), real);
|
|
6665
6893
|
}
|
|
6666
6894
|
function realEntryCollision(label, name, directory) {
|
|
6667
6895
|
return { action: "block", detail: `${label} is a real ${directory ? "directory" : "entry"} that collides with the selected catalog skill ${name}; move or rename it, pjangler never removes real content` };
|
|
@@ -6700,14 +6928,14 @@ function realpathOf(path) {
|
|
|
6700
6928
|
}
|
|
6701
6929
|
}
|
|
6702
6930
|
async function predictRootCollisions(ctx, incoming) {
|
|
6703
|
-
const root =
|
|
6704
|
-
const physicalRoot =
|
|
6931
|
+
const root = join6(resolve5(ctx.repoRoot), ".agents", "skills");
|
|
6932
|
+
const physicalRoot = join6(realpathOf(ctx.repoRoot) ?? resolve5(ctx.repoRoot), ".agents", "skills");
|
|
6705
6933
|
const planned = new Map(incoming.map((entry) => [entry.name, entry]));
|
|
6706
6934
|
const owned = await receiptOwnedLinks(ctx);
|
|
6707
6935
|
const predictions = [];
|
|
6708
6936
|
for (const binding of await projectBindings(ctx)) {
|
|
6709
|
-
const path =
|
|
6710
|
-
const label =
|
|
6937
|
+
const path = join6(root, binding.name);
|
|
6938
|
+
const label = relative4(resolve5(ctx.repoRoot), path);
|
|
6711
6939
|
const catalog = new Set([binding.path, realpathOf(binding.path)].filter(Boolean));
|
|
6712
6940
|
const entry = planned.get(binding.name);
|
|
6713
6941
|
if (entry) {
|
|
@@ -6722,12 +6950,12 @@ async function predictRootCollisions(ctx, incoming) {
|
|
|
6722
6950
|
}
|
|
6723
6951
|
let stat;
|
|
6724
6952
|
try {
|
|
6725
|
-
stat =
|
|
6953
|
+
stat = lstatSync5(path);
|
|
6726
6954
|
} catch {
|
|
6727
6955
|
continue;
|
|
6728
6956
|
}
|
|
6729
6957
|
if (stat.isSymbolicLink() && catalog.has(realpathOf(path))) continue;
|
|
6730
|
-
if (owned.has(
|
|
6958
|
+
if (owned.has(join6(physicalRoot, binding.name)) || owned.has(path)) continue;
|
|
6731
6959
|
predictions.push({ path, decision: await classifyRootCollision(ctx, path) });
|
|
6732
6960
|
}
|
|
6733
6961
|
return predictions;
|
|
@@ -6795,7 +7023,7 @@ async function synchronizeProjectSkills(ctx, extra = {}) {
|
|
|
6795
7023
|
const replaceable = predictions.filter(({ decision }) => decision.action === "unlink");
|
|
6796
7024
|
if (blocking.length) {
|
|
6797
7025
|
details.push(...blocking.map(({ decision }) => `blocked: ${decision.detail}`));
|
|
6798
|
-
if (replaceable.length) details.push(`left in place until the blocked collision(s) above are resolved: ${replaceable.map(({ path }) =>
|
|
7026
|
+
if (replaceable.length) details.push(`left in place until the blocked collision(s) above are resolved: ${replaceable.map(({ path }) => relative4(resolve5(ctx.repoRoot), path)).join(", ")}`);
|
|
6799
7027
|
details.push(migrationGuidance(ctx));
|
|
6800
7028
|
return { ok: false, changedFiles: [...new Set(changedFiles)], details };
|
|
6801
7029
|
}
|
|
@@ -7495,20 +7723,13 @@ var HOOKS_COMMENT_HEADER = `# This block will handle the linking of
|
|
|
7495
7723
|
# TODO: Ensure this works for all levels of nesting.
|
|
7496
7724
|
# i.e. All linked agent files MUST be siblings at
|
|
7497
7725
|
# any given level of nesting.`;
|
|
7498
|
-
var SKILLEX_TOOL_VERSION = "0.1.2";
|
|
7499
|
-
var SKILLS_SYNC_TOOLS = `{ "npm:@delorenj/skillex" = { version = "${SKILLEX_TOOL_VERSION}", allow_low_downloads = true }, node = "24" }`;
|
|
7500
7726
|
var LINK_AGENTFILES_WATCH_TASK_BLOCK = `[[watch_files]]
|
|
7501
7727
|
patterns = ["AGENTS.md"]
|
|
7502
7728
|
task = "${LINK_AGENTFILES_TASK}"
|
|
7503
7729
|
|
|
7504
7730
|
${taskHeader(LINK_AGENTFILES_TASK)}
|
|
7505
7731
|
description = "Symlink all agent files to AGENTS.md"
|
|
7506
|
-
run = ${JSON.stringify(LINK_AGENTFILES_SCRIPT)}
|
|
7507
|
-
|
|
7508
|
-
${taskHeader(SKILLS_SYNC_TASK)}
|
|
7509
|
-
description = "Reconcile this project's selected skills"
|
|
7510
|
-
tools = ${SKILLS_SYNC_TOOLS}
|
|
7511
|
-
run = "skillex sync --scope project --project '{{config_root}}'"`;
|
|
7732
|
+
run = ${JSON.stringify(LINK_AGENTFILES_SCRIPT)}`;
|
|
7512
7733
|
var VERSIONING_BLOCK = `# >>> mise-versioning >>> (managed block \u2014 do not edit by hand; re-run init to update)
|
|
7513
7734
|
[tasks."version"]
|
|
7514
7735
|
description = "Print the current version (vX.Y.Z)"
|
|
@@ -7539,10 +7760,10 @@ function normalizeNewlines(value) {
|
|
|
7539
7760
|
return value.replace(/\r\n/g, "\n");
|
|
7540
7761
|
}
|
|
7541
7762
|
function readText(path) {
|
|
7542
|
-
return normalizeNewlines(
|
|
7763
|
+
return normalizeNewlines(readFileSync5(path, "utf8"));
|
|
7543
7764
|
}
|
|
7544
7765
|
function safeReadText2(path) {
|
|
7545
|
-
return
|
|
7766
|
+
return existsSync4(path) ? readText(path) : null;
|
|
7546
7767
|
}
|
|
7547
7768
|
function ensureParent(path) {
|
|
7548
7769
|
mkdirSync3(dirname5(path), { recursive: true });
|
|
@@ -7566,16 +7787,16 @@ function titleCaseSlug(slug) {
|
|
|
7566
7787
|
return slug.split(/[-_]/g).filter(Boolean).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join(" ");
|
|
7567
7788
|
}
|
|
7568
7789
|
function readSymlinkTarget(path) {
|
|
7569
|
-
if (!
|
|
7790
|
+
if (!existsSync4(path)) return null;
|
|
7570
7791
|
try {
|
|
7571
|
-
return
|
|
7792
|
+
return readlinkSync4(path);
|
|
7572
7793
|
} catch {
|
|
7573
7794
|
return null;
|
|
7574
7795
|
}
|
|
7575
7796
|
}
|
|
7576
7797
|
function ensureSymlink(path, target, dryRun) {
|
|
7577
|
-
if (
|
|
7578
|
-
const stat =
|
|
7798
|
+
if (existsSync4(path)) {
|
|
7799
|
+
const stat = lstatSync6(path);
|
|
7579
7800
|
if (stat.isSymbolicLink()) {
|
|
7580
7801
|
const current = readSymlinkTarget(path);
|
|
7581
7802
|
if (current === target) return { changed: false };
|
|
@@ -7585,18 +7806,18 @@ function ensureSymlink(path, target, dryRun) {
|
|
|
7585
7806
|
}
|
|
7586
7807
|
return { changed: true };
|
|
7587
7808
|
}
|
|
7588
|
-
return { changed: false, blocked: `${
|
|
7809
|
+
return { changed: false, blocked: `${relative5(process.cwd(), path) || path} exists and is not a symlink` };
|
|
7589
7810
|
}
|
|
7590
7811
|
if (!dryRun) symlinkSync2(target, path);
|
|
7591
7812
|
return { changed: true };
|
|
7592
7813
|
}
|
|
7593
7814
|
function bootstrapAgentsFile(repoRoot, dryRun) {
|
|
7594
|
-
const agentsPath =
|
|
7595
|
-
if (
|
|
7815
|
+
const agentsPath = join7(repoRoot, "AGENTS.md");
|
|
7816
|
+
if (existsSync4(agentsPath)) return { changedFiles: [], details: [] };
|
|
7596
7817
|
for (const file of ["CLAUDE.md", "GEMINI.md"]) {
|
|
7597
|
-
const source =
|
|
7598
|
-
if (!
|
|
7599
|
-
const stat =
|
|
7818
|
+
const source = join7(repoRoot, file);
|
|
7819
|
+
if (!existsSync4(source)) continue;
|
|
7820
|
+
const stat = lstatSync6(source);
|
|
7600
7821
|
if (stat.isSymbolicLink()) continue;
|
|
7601
7822
|
if (stat.isFile()) {
|
|
7602
7823
|
if (!dryRun) renameSync2(source, agentsPath);
|
|
@@ -7604,9 +7825,9 @@ function bootstrapAgentsFile(repoRoot, dryRun) {
|
|
|
7604
7825
|
}
|
|
7605
7826
|
return { changedFiles: [], details: [], blocked: `${file} exists but is not a regular file; cannot promote to AGENTS.md` };
|
|
7606
7827
|
}
|
|
7607
|
-
const readmePath =
|
|
7608
|
-
if (
|
|
7609
|
-
const stat =
|
|
7828
|
+
const readmePath = join7(repoRoot, "README.md");
|
|
7829
|
+
if (existsSync4(readmePath)) {
|
|
7830
|
+
const stat = lstatSync6(readmePath);
|
|
7610
7831
|
if (!stat.isFile()) return { changedFiles: [], details: [], blocked: "README.md exists but is not a regular file; cannot copy to AGENTS.md" };
|
|
7611
7832
|
if (!dryRun) copyFileSync(readmePath, agentsPath);
|
|
7612
7833
|
return { changedFiles: [agentsPath], details: ["Copied README.md to AGENTS.md before wiring agent-file symlinks"] };
|
|
@@ -7644,12 +7865,12 @@ function yamlGet(text, keyPath) {
|
|
|
7644
7865
|
return "";
|
|
7645
7866
|
}
|
|
7646
7867
|
function discoverRoles(repoRoot) {
|
|
7647
|
-
const rolesDir =
|
|
7648
|
-
if (!
|
|
7649
|
-
return
|
|
7650
|
-
const roleDir =
|
|
7651
|
-
const roleYamlPath =
|
|
7652
|
-
if (!
|
|
7868
|
+
const rolesDir = join7(repoRoot, "agents", "hermes");
|
|
7869
|
+
if (!existsSync4(rolesDir)) return [];
|
|
7870
|
+
return readdirSync4(rolesDir, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => {
|
|
7871
|
+
const roleDir = join7(rolesDir, entry.name);
|
|
7872
|
+
const roleYamlPath = join7(roleDir, "role.yaml");
|
|
7873
|
+
if (!existsSync4(roleYamlPath)) return null;
|
|
7653
7874
|
const text = readText(roleYamlPath);
|
|
7654
7875
|
const runtimeRepoRaw = yamlGet(text, "runtime.github_repo");
|
|
7655
7876
|
return {
|
|
@@ -7681,26 +7902,26 @@ function discoverRoles(repoRoot) {
|
|
|
7681
7902
|
}).filter((value) => Boolean(value));
|
|
7682
7903
|
}
|
|
7683
7904
|
function templateScript(ctx, name) {
|
|
7684
|
-
const source =
|
|
7685
|
-
return
|
|
7905
|
+
const source = join7(ctx.pjanglerRoot, ".mise", "scripts", name);
|
|
7906
|
+
return existsSync4(source) ? readText(source) : void 0;
|
|
7686
7907
|
}
|
|
7687
7908
|
function templateVersioningScript(ctx) {
|
|
7688
7909
|
return templateScript(ctx, "versioning.sh");
|
|
7689
7910
|
}
|
|
7690
7911
|
function templateLinkAgentfilesScript(ctx) {
|
|
7691
|
-
const source =
|
|
7692
|
-
return
|
|
7912
|
+
const source = join7(ctx.pjanglerRoot, "templates", "commonproject", "template", ".mise", "scripts", "link-agentfiles.sh");
|
|
7913
|
+
return existsSync4(source) ? readText(source) : templateScript(ctx, "link-agentfiles.sh");
|
|
7693
7914
|
}
|
|
7694
7915
|
function templateMaterializeEnvScript(ctx) {
|
|
7695
|
-
const source =
|
|
7696
|
-
return
|
|
7916
|
+
const source = join7(ctx.pjanglerRoot, "templates", "commonproject", "template", MATERIALIZE_ENV_SCRIPT_REL);
|
|
7917
|
+
return existsSync4(source) ? readText(source) : void 0;
|
|
7697
7918
|
}
|
|
7698
7919
|
function resolveAgentHooksLayer(ctx) {
|
|
7699
7920
|
const override = process.env.PJ_AGENT_HOOKS_LAYER;
|
|
7700
7921
|
if (override === "0" || override === "false") return false;
|
|
7701
7922
|
if (override === "1" || override === "true") return true;
|
|
7702
|
-
if (
|
|
7703
|
-
return !
|
|
7923
|
+
if (existsSync4(join7(ctx.repoRoot, ".agents", "hooks", "sync.py"))) return true;
|
|
7924
|
+
return !existsSync4(join7(ctx.homeDir, ".agents", "hooks"));
|
|
7704
7925
|
}
|
|
7705
7926
|
function evaluateMiseConditionals(template, agentHooksLayer) {
|
|
7706
7927
|
const out = [];
|
|
@@ -7730,22 +7951,22 @@ function renderGeneratedProjectMiseToml(ctx, template) {
|
|
|
7730
7951
|
return evaluateMiseConditionals(template, resolveAgentHooksLayer(ctx)).replace(/\{%\s*raw\s*%\}([\s\S]*?)\{%\s*endraw\s*%\}/g, "$1").replace(/\{\{\s*project_name\s*\}\}/g, projectName);
|
|
7731
7952
|
}
|
|
7732
7953
|
function ensureMiseTomlFromTemplate(ctx, changedFiles) {
|
|
7733
|
-
const targetPath =
|
|
7734
|
-
if (
|
|
7735
|
-
const sourcePath =
|
|
7736
|
-
if (!
|
|
7954
|
+
const targetPath = join7(ctx.repoRoot, "mise.toml");
|
|
7955
|
+
if (existsSync4(targetPath)) return null;
|
|
7956
|
+
const sourcePath = join7(ctx.pjanglerRoot, "templates", "commonproject", "template", "mise.toml.jinja");
|
|
7957
|
+
if (!existsSync4(sourcePath)) return null;
|
|
7737
7958
|
const rendered = renderGeneratedProjectMiseToml(ctx, readText(sourcePath));
|
|
7738
7959
|
changedFiles.push(targetPath);
|
|
7739
7960
|
if (!ctx.dryRun) writeText(targetPath, rendered);
|
|
7740
7961
|
return rendered;
|
|
7741
7962
|
}
|
|
7742
7963
|
function templateCommonProjectText(ctx, rel) {
|
|
7743
|
-
const path =
|
|
7744
|
-
return
|
|
7964
|
+
const path = join7(ctx.pjanglerRoot, "templates", "commonproject", "template", rel);
|
|
7965
|
+
return existsSync4(path) ? readText(path) : void 0;
|
|
7745
7966
|
}
|
|
7746
7967
|
function lstatIfPresent(path) {
|
|
7747
7968
|
try {
|
|
7748
|
-
return
|
|
7969
|
+
return lstatSync6(path);
|
|
7749
7970
|
} catch (error) {
|
|
7750
7971
|
if (error.code === "ENOENT") return void 0;
|
|
7751
7972
|
throw error;
|
|
@@ -7753,17 +7974,17 @@ function lstatIfPresent(path) {
|
|
|
7753
7974
|
}
|
|
7754
7975
|
function templateVersionFilesConf(ctx, repoRoot) {
|
|
7755
7976
|
if (isObsidianPluginRepo(ctx, repoRoot)) return OBSIDIAN_VERSION_FILES_CONF;
|
|
7756
|
-
const packageJson =
|
|
7757
|
-
return
|
|
7977
|
+
const packageJson = join7(repoRoot, "package.json");
|
|
7978
|
+
return existsSync4(packageJson) ? "# mise-versioning manifest: <type> <path>\n# types: json toml cargo csproj gradle plain gittag\njson package.json\ngittag .\n" : "# mise-versioning manifest: <type> <path>\n# types: json toml cargo csproj gradle plain gittag\ngittag .\n";
|
|
7758
7979
|
}
|
|
7759
7980
|
var OBSIDIAN_VERSION_FILES_CONF = "# mise-versioning manifest: <type> <path>\n# types: json toml cargo csproj gradle plain gittag obsidian\n# Obsidian plugin: bump with `mise run version:bump`, release with `npm version` (bare X.Y.Z tag).\njson package.json\nobsidian manifest.json\n";
|
|
7760
7981
|
function recordedProjectType(ctx) {
|
|
7761
|
-
return
|
|
7982
|
+
return record2(record2(readProjectJson(ctx)?.template).commonproject).project_type;
|
|
7762
7983
|
}
|
|
7763
7984
|
function isObsidianPluginRepo(ctx, repoRoot) {
|
|
7764
7985
|
const recorded = recordedProjectType(ctx);
|
|
7765
7986
|
if (recorded !== void 0) return recorded === "obsidian-plugin";
|
|
7766
|
-
const manifest =
|
|
7987
|
+
const manifest = record2(tryParseJson(safeReadText2(join7(repoRoot, "manifest.json"))));
|
|
7767
7988
|
return typeof manifest.id === "string" && typeof manifest.minAppVersion === "string";
|
|
7768
7989
|
}
|
|
7769
7990
|
function unsupportedVersionTypes(conf, script) {
|
|
@@ -7973,6 +8194,7 @@ function managedLinkHookIssues(parsed) {
|
|
|
7973
8194
|
}
|
|
7974
8195
|
var RETIRED_LINK_TASKS = [
|
|
7975
8196
|
"skills-sync",
|
|
8197
|
+
SKILLS_SYNC_TASK,
|
|
7976
8198
|
"skills-provision-packs",
|
|
7977
8199
|
PROVISION_PACKS_TASK,
|
|
7978
8200
|
"skills-provision-bmad",
|
|
@@ -7986,8 +8208,7 @@ function managedLinkTables() {
|
|
|
7986
8208
|
const parsed = parseToml2(LINK_AGENTFILES_WATCH_TASK_BLOCK);
|
|
7987
8209
|
return {
|
|
7988
8210
|
watch: { lines: chunks[0], value: parsed.watch_files[0] },
|
|
7989
|
-
link: { lines: chunks[1], value: parsed.tasks[LINK_AGENTFILES_TASK] }
|
|
7990
|
-
sync: { lines: chunks[2], value: parsed.tasks[SKILLS_SYNC_TASK] }
|
|
8211
|
+
link: { lines: chunks[1], value: parsed.tasks[LINK_AGENTFILES_TASK] }
|
|
7991
8212
|
};
|
|
7992
8213
|
}
|
|
7993
8214
|
function managedWatchPlan(watches) {
|
|
@@ -8033,7 +8254,7 @@ function reconcileManagedLinkTables(text) {
|
|
|
8033
8254
|
edits.push({ start: span.start, end: span.end, lines: [] });
|
|
8034
8255
|
continue;
|
|
8035
8256
|
}
|
|
8036
|
-
const table =
|
|
8257
|
+
const table = managed.link;
|
|
8037
8258
|
found.add(name);
|
|
8038
8259
|
if (isDeepStrictEqualLoose(parseBody(lines, statements, index), table.value)) continue;
|
|
8039
8260
|
if (name === LINK_AGENTFILES_TASK && !lines.slice(span.start, span.end).join("\n").includes("link-agentfiles")) {
|
|
@@ -8057,8 +8278,7 @@ function reconcileManagedLinkTables(text) {
|
|
|
8057
8278
|
});
|
|
8058
8279
|
const missing = [
|
|
8059
8280
|
...plan.managed >= 0 ? [] : [managed.watch.lines],
|
|
8060
|
-
...found.has(LINK_AGENTFILES_TASK) ? [] : [managed.link.lines]
|
|
8061
|
-
...found.has(SKILLS_SYNC_TASK) ? [] : [managed.sync.lines]
|
|
8281
|
+
...found.has(LINK_AGENTFILES_TASK) ? [] : [managed.link.lines]
|
|
8062
8282
|
];
|
|
8063
8283
|
if (missing.length) {
|
|
8064
8284
|
const block = missing.flatMap((chunk, index) => index ? ["", ...chunk] : chunk);
|
|
@@ -8142,7 +8362,6 @@ function expectedLinkModel(base, required) {
|
|
|
8142
8362
|
const tasks = isTable(model.tasks) ? model.tasks : model.tasks = {};
|
|
8143
8363
|
for (const name of RETIRED_LINK_TASKS) delete tasks[name];
|
|
8144
8364
|
tasks[LINK_AGENTFILES_TASK] = deepClone(managed.link.value);
|
|
8145
|
-
tasks[SKILLS_SYNC_TASK] = deepClone(managed.sync.value);
|
|
8146
8365
|
const watches = Array.isArray(model.watch_files) ? model.watch_files : [];
|
|
8147
8366
|
const plan = managedWatchPlan(watches);
|
|
8148
8367
|
model.watch_files = [
|
|
@@ -8226,7 +8445,7 @@ function describeRewrite(before, after) {
|
|
|
8226
8445
|
return difference ? `migrate would still rewrite ${difference} in mise.toml` : "migrate would still re-lay out mise.toml (its meaning is unchanged)";
|
|
8227
8446
|
}
|
|
8228
8447
|
function readProjectJson(ctx) {
|
|
8229
|
-
return tryParseJson(safeReadText2(
|
|
8448
|
+
return tryParseJson(safeReadText2(join7(ctx.repoRoot, ".project.json")));
|
|
8230
8449
|
}
|
|
8231
8450
|
function readDeclaredAgents(ctx) {
|
|
8232
8451
|
const project = readProjectJson(ctx);
|
|
@@ -8243,8 +8462,8 @@ function readDeclaredAgents(ctx) {
|
|
|
8243
8462
|
});
|
|
8244
8463
|
}
|
|
8245
8464
|
function readRoleYamlAt(roleDir) {
|
|
8246
|
-
const roleYamlPath =
|
|
8247
|
-
if (!
|
|
8465
|
+
const roleYamlPath = join7(roleDir, "role.yaml");
|
|
8466
|
+
if (!existsSync4(roleYamlPath)) return null;
|
|
8248
8467
|
const text = readText(roleYamlPath);
|
|
8249
8468
|
return {
|
|
8250
8469
|
role: yamlGet(text, "role"),
|
|
@@ -8255,7 +8474,7 @@ function readRoleYamlAt(roleDir) {
|
|
|
8255
8474
|
}
|
|
8256
8475
|
function declaredRoleIsUnprovisioned(repoRoot, roleDir) {
|
|
8257
8476
|
if (!roleDir) return true;
|
|
8258
|
-
return !
|
|
8477
|
+
return !existsSync4(join7(resolve6(repoRoot, roleDir), "role.yaml"));
|
|
8259
8478
|
}
|
|
8260
8479
|
function validateDeclaredAgent(ctx, declared) {
|
|
8261
8480
|
const details = [];
|
|
@@ -8263,8 +8482,8 @@ function validateDeclaredAgent(ctx, declared) {
|
|
|
8263
8482
|
details.push(`agents.${declared.agentId}.role_dir missing`);
|
|
8264
8483
|
return { valid: false, details };
|
|
8265
8484
|
}
|
|
8266
|
-
const roleDir =
|
|
8267
|
-
if (!
|
|
8485
|
+
const roleDir = resolve6(ctx.repoRoot, declared.roleDir);
|
|
8486
|
+
if (!existsSync4(roleDir)) {
|
|
8268
8487
|
details.push(`agents.${declared.agentId}.role_dir ${declared.roleDir} does not exist`);
|
|
8269
8488
|
return { valid: false, roleDir, details };
|
|
8270
8489
|
}
|
|
@@ -8280,13 +8499,13 @@ function validateDeclaredAgent(ctx, declared) {
|
|
|
8280
8499
|
details.push(`agents.${declared.agentId} should map to agent_id ${roleYaml.agentId}`);
|
|
8281
8500
|
}
|
|
8282
8501
|
if (roleYaml.providerName) {
|
|
8283
|
-
const dispatcher =
|
|
8284
|
-
if (!
|
|
8285
|
-
details.push(`agents.${declared.agentId} provider dispatcher ${
|
|
8502
|
+
const dispatcher = join7(roleDir, ".scripts", "lib", "ticket-provider.sh");
|
|
8503
|
+
if (!existsSync4(dispatcher)) {
|
|
8504
|
+
details.push(`agents.${declared.agentId} provider dispatcher ${relative5(ctx.repoRoot, dispatcher)} missing`);
|
|
8286
8505
|
}
|
|
8287
|
-
const provider =
|
|
8288
|
-
if (!
|
|
8289
|
-
details.push(`agents.${declared.agentId} provider script ${
|
|
8506
|
+
const provider = join7(roleDir, ".scripts", "providers", `${roleYaml.providerName}.sh`);
|
|
8507
|
+
if (!existsSync4(provider)) {
|
|
8508
|
+
details.push(`agents.${declared.agentId} provider script ${relative5(ctx.repoRoot, provider)} missing`);
|
|
8290
8509
|
}
|
|
8291
8510
|
}
|
|
8292
8511
|
return { valid: details.length === 0, role: roleYaml.role, agentId: roleYaml.agentId, roleDir, details };
|
|
@@ -8312,7 +8531,7 @@ function canonicalProjectJson(ctx) {
|
|
|
8312
8531
|
role.agentId || `${slug}-${role.role}`,
|
|
8313
8532
|
{
|
|
8314
8533
|
role: role.role,
|
|
8315
|
-
role_dir:
|
|
8534
|
+
role_dir: relative5(ctx.repoRoot, role.roleDir)
|
|
8316
8535
|
}
|
|
8317
8536
|
])
|
|
8318
8537
|
);
|
|
@@ -8344,7 +8563,7 @@ function canonicalProjectJson(ctx) {
|
|
|
8344
8563
|
}
|
|
8345
8564
|
agents[validated.agentId] = {
|
|
8346
8565
|
role: validated.role,
|
|
8347
|
-
role_dir:
|
|
8566
|
+
role_dir: relative5(ctx.repoRoot, validated.roleDir),
|
|
8348
8567
|
...declared.extras
|
|
8349
8568
|
};
|
|
8350
8569
|
}
|
|
@@ -8362,12 +8581,12 @@ function canonicalProjectJson(ctx) {
|
|
|
8362
8581
|
};
|
|
8363
8582
|
}
|
|
8364
8583
|
function projectJsonFinding(ctx) {
|
|
8365
|
-
const projectPath =
|
|
8366
|
-
const planeJsonPath =
|
|
8584
|
+
const projectPath = join7(ctx.repoRoot, ".project.json");
|
|
8585
|
+
const planeJsonPath = join7(ctx.repoRoot, ".plane.json");
|
|
8367
8586
|
const details = [];
|
|
8368
8587
|
const data = readProjectJson(ctx);
|
|
8369
8588
|
const roles = discoverRoles(ctx.repoRoot);
|
|
8370
|
-
if (!
|
|
8589
|
+
if (!existsSync4(projectPath)) {
|
|
8371
8590
|
return { id: "sot.project-json", title: "Canonical .project.json", status: "fail", summary: ".project.json missing", details: [], fixable: true };
|
|
8372
8591
|
}
|
|
8373
8592
|
if (!data) {
|
|
@@ -8387,8 +8606,8 @@ function projectJsonFinding(ctx) {
|
|
|
8387
8606
|
continue;
|
|
8388
8607
|
}
|
|
8389
8608
|
if (agent.role !== role.role) details.push(`agents.${role.agentId}.role should be ${role.role}`);
|
|
8390
|
-
if (agent.role_dir !==
|
|
8391
|
-
details.push(`agents.${role.agentId}.role_dir should be ${
|
|
8609
|
+
if (agent.role_dir !== relative5(ctx.repoRoot, role.roleDir)) {
|
|
8610
|
+
details.push(`agents.${role.agentId}.role_dir should be ${relative5(ctx.repoRoot, role.roleDir)}`);
|
|
8392
8611
|
}
|
|
8393
8612
|
}
|
|
8394
8613
|
const declaredAgents = readDeclaredAgents(ctx);
|
|
@@ -8416,7 +8635,7 @@ function projectJsonFinding(ctx) {
|
|
|
8416
8635
|
if ("reconcile" in automation) {
|
|
8417
8636
|
details.push("automation.reconcile is vestigial and should be removed; the heartbeat reads role.yaml, never this key");
|
|
8418
8637
|
}
|
|
8419
|
-
if (
|
|
8638
|
+
if (existsSync4(planeJsonPath)) details.push(".plane.json should not exist once .project.json is canonical");
|
|
8420
8639
|
return {
|
|
8421
8640
|
id: "sot.project-json",
|
|
8422
8641
|
title: "Canonical .project.json",
|
|
@@ -8434,7 +8653,7 @@ var BMAD_DIST_TAGS_TTL_MS = 60 * 60 * 1e3;
|
|
|
8434
8653
|
var DEFAULT_BMAD_MODULES = ["bmm", "bmb", "cis"];
|
|
8435
8654
|
var BMAD_INSTALL_TOOLS = SUPPORTED_BMAD_TOOLS;
|
|
8436
8655
|
function manifestBmadModules(repoRoot) {
|
|
8437
|
-
const manifestPath =
|
|
8656
|
+
const manifestPath = join7(repoRoot, "_bmad", "_config", "manifest.yaml");
|
|
8438
8657
|
const raw = safeReadText2(manifestPath);
|
|
8439
8658
|
if (raw === null) return { status: "absent" };
|
|
8440
8659
|
try {
|
|
@@ -8443,14 +8662,16 @@ function manifestBmadModules(repoRoot) {
|
|
|
8443
8662
|
return { status: "invalid", error: `${manifestPath} must define a modules array` };
|
|
8444
8663
|
}
|
|
8445
8664
|
const declared = [];
|
|
8665
|
+
let hasG33 = false;
|
|
8446
8666
|
for (const entry of parsed.modules) {
|
|
8447
8667
|
const name = typeof entry === "string" ? entry : entry && typeof entry === "object" ? entry.name : void 0;
|
|
8448
8668
|
if (typeof name !== "string" || !/^[A-Za-z0-9][A-Za-z0-9_-]*$/.test(name)) {
|
|
8449
8669
|
return { status: "invalid", error: `${manifestPath} contains an invalid module entry` };
|
|
8450
8670
|
}
|
|
8451
|
-
if (name
|
|
8671
|
+
if (name === "g33") hasG33 = true;
|
|
8672
|
+
if (name !== "core" && name !== "custom" && name !== "g33") declared.push(name);
|
|
8452
8673
|
}
|
|
8453
|
-
return { status: "valid", modules: Array.from(new Set(declared)) };
|
|
8674
|
+
return { status: "valid", modules: Array.from(new Set(declared)), hasG33 };
|
|
8454
8675
|
} catch (error) {
|
|
8455
8676
|
return {
|
|
8456
8677
|
status: "invalid",
|
|
@@ -8458,24 +8679,24 @@ function manifestBmadModules(repoRoot) {
|
|
|
8458
8679
|
};
|
|
8459
8680
|
}
|
|
8460
8681
|
}
|
|
8461
|
-
function
|
|
8462
|
-
const raw = safeReadText2(
|
|
8682
|
+
function configuredBmadModuleSelection(repoRoot) {
|
|
8683
|
+
const raw = safeReadText2(join7(repoRoot, "_bmad", "config.toml"));
|
|
8463
8684
|
if (raw === null) return void 0;
|
|
8464
8685
|
const modules = [...raw.matchAll(/^\[modules\.([A-Za-z0-9][A-Za-z0-9_-]*)\]\s*$/gm)].map((match) => match[1]);
|
|
8465
|
-
return Array.from(new Set(modules));
|
|
8686
|
+
return { modules: Array.from(new Set(modules)).filter((name) => name !== "g33"), hasG33: modules.includes("g33") };
|
|
8466
8687
|
}
|
|
8467
8688
|
function selectedBmadModules(repoRoot) {
|
|
8468
8689
|
const manifest = manifestBmadModules(repoRoot);
|
|
8469
8690
|
if (manifest.status === "valid") return manifest.modules;
|
|
8470
8691
|
if (manifest.status === "invalid") throw new Error(manifest.error);
|
|
8471
|
-
return
|
|
8692
|
+
return configuredBmadModuleSelection(repoRoot)?.modules ?? [...DEFAULT_BMAD_MODULES];
|
|
8472
8693
|
}
|
|
8473
8694
|
function requiredBmadSentinels(repoRoot, modules = selectedBmadModules(repoRoot)) {
|
|
8474
8695
|
return [
|
|
8475
|
-
|
|
8476
|
-
|
|
8477
|
-
|
|
8478
|
-
...modules.map((module) =>
|
|
8696
|
+
join7("core", "config.yaml"),
|
|
8697
|
+
join7("config.toml"),
|
|
8698
|
+
join7("_config", "manifest.yaml"),
|
|
8699
|
+
...modules.map((module) => join7(module, "config.yaml"))
|
|
8479
8700
|
];
|
|
8480
8701
|
}
|
|
8481
8702
|
function canonicalBmadProjectName(repoRoot) {
|
|
@@ -8487,7 +8708,7 @@ function bmadProjectNameIssues(repoRoot) {
|
|
|
8487
8708
|
const expected = canonicalBmadProjectName(repoRoot);
|
|
8488
8709
|
const paths = [];
|
|
8489
8710
|
const details = [];
|
|
8490
|
-
const configToml =
|
|
8711
|
+
const configToml = join7(repoRoot, "_bmad", "config.toml");
|
|
8491
8712
|
const tomlText = safeReadText2(configToml);
|
|
8492
8713
|
const tomlMatch = tomlText?.match(/^project_name\s*=\s*"((?:\\.|[^"\\])*)"\s*$/m);
|
|
8493
8714
|
let tomlName;
|
|
@@ -8502,16 +8723,16 @@ function bmadProjectNameIssues(repoRoot) {
|
|
|
8502
8723
|
paths.push(configToml);
|
|
8503
8724
|
details.push(`_bmad/config.toml project_name must be ${JSON.stringify(expected)}`);
|
|
8504
8725
|
}
|
|
8505
|
-
const bmadRoot =
|
|
8506
|
-
if (
|
|
8726
|
+
const bmadRoot = join7(repoRoot, "_bmad");
|
|
8727
|
+
if (existsSync4(bmadRoot)) {
|
|
8507
8728
|
let declared;
|
|
8508
8729
|
try {
|
|
8509
8730
|
declared = selectedBmadModules(repoRoot);
|
|
8510
8731
|
} catch {
|
|
8511
|
-
declared =
|
|
8732
|
+
declared = readdirSync4(bmadRoot);
|
|
8512
8733
|
}
|
|
8513
8734
|
for (const name of new Set(declared)) {
|
|
8514
|
-
const configPath =
|
|
8735
|
+
const configPath = join7(bmadRoot, name, "config.yaml");
|
|
8515
8736
|
const raw = safeReadText2(configPath);
|
|
8516
8737
|
if (raw === null) continue;
|
|
8517
8738
|
let actual;
|
|
@@ -8531,7 +8752,7 @@ function bmadProjectNameIssues(repoRoot) {
|
|
|
8531
8752
|
function isCurrentActivation(path, activations) {
|
|
8532
8753
|
if (activations.owned.has(path)) return true;
|
|
8533
8754
|
try {
|
|
8534
|
-
if (activations.owned.has(
|
|
8755
|
+
if (activations.owned.has(join7(realpathSync3(dirname5(path)), basename3(path)))) return true;
|
|
8535
8756
|
return activations.targets.has(realpathSync3(path));
|
|
8536
8757
|
} catch {
|
|
8537
8758
|
return false;
|
|
@@ -8544,25 +8765,25 @@ async function skillActivations(ctx) {
|
|
|
8544
8765
|
function evictLegacyBmadPackState(ctx, changedFiles, activations) {
|
|
8545
8766
|
const details = [];
|
|
8546
8767
|
const skillDirs = [
|
|
8547
|
-
|
|
8548
|
-
...SUPPORTED_CLI_ROOTS.map((root) =>
|
|
8768
|
+
join7(ctx.repoRoot, ".agents", "skills"),
|
|
8769
|
+
...SUPPORTED_CLI_ROOTS.map((root) => join7(ctx.repoRoot, root, "skills"))
|
|
8549
8770
|
];
|
|
8550
8771
|
for (const dir of skillDirs) {
|
|
8551
8772
|
const dirStat = lstatIfPresent(dir);
|
|
8552
8773
|
if (!dirStat || dirStat.isSymbolicLink() || !dirStat.isDirectory()) continue;
|
|
8553
8774
|
let names;
|
|
8554
8775
|
try {
|
|
8555
|
-
names =
|
|
8776
|
+
names = readdirSync4(dir);
|
|
8556
8777
|
} catch {
|
|
8557
8778
|
continue;
|
|
8558
8779
|
}
|
|
8559
8780
|
for (const name of names) {
|
|
8560
8781
|
if (!name.startsWith(BMAD_SKILL_NAME_PREFIX)) continue;
|
|
8561
|
-
const path =
|
|
8782
|
+
const path = join7(dir, name);
|
|
8562
8783
|
if (!lstatIfPresent(path)?.isSymbolicLink()) continue;
|
|
8563
8784
|
if (isCurrentActivation(path, activations)) continue;
|
|
8564
8785
|
changedFiles.push(path);
|
|
8565
|
-
details.push(`removed retired BMAD pack symlink ${
|
|
8786
|
+
details.push(`removed retired BMAD pack symlink ${relative5(ctx.repoRoot, path)}`);
|
|
8566
8787
|
if (!ctx.dryRun) unlinkSync3(path);
|
|
8567
8788
|
}
|
|
8568
8789
|
}
|
|
@@ -8570,7 +8791,7 @@ function evictLegacyBmadPackState(ctx, changedFiles, activations) {
|
|
|
8570
8791
|
}
|
|
8571
8792
|
function bmadInstallerInvocation(version = BMAD_INSTALLER_VERSION) {
|
|
8572
8793
|
const explicit = process.env.PJ_BMAD_INSTALLER?.trim();
|
|
8573
|
-
if (explicit) return { command:
|
|
8794
|
+
if (explicit) return { command: resolve6(explicit), prefixArgs: [] };
|
|
8574
8795
|
return {
|
|
8575
8796
|
command: "npx",
|
|
8576
8797
|
prefixArgs: ["-y", `${BMAD_NPM_PACKAGE}@${version}`]
|
|
@@ -8597,7 +8818,7 @@ function bmadInstallDisplay(repoRoot, modules = selectedBmadModules(repoRoot), v
|
|
|
8597
8818
|
}
|
|
8598
8819
|
function preflightBmadLifecycle(_ctx) {
|
|
8599
8820
|
const invocation = bmadInstallerInvocation();
|
|
8600
|
-
const probe =
|
|
8821
|
+
const probe = spawnSync4(invocation.command, [...invocation.prefixArgs, "--version"], {
|
|
8601
8822
|
encoding: "utf8",
|
|
8602
8823
|
timeout: 3e4
|
|
8603
8824
|
});
|
|
@@ -8619,14 +8840,17 @@ ${probe.stderr ?? ""}`;
|
|
|
8619
8840
|
}
|
|
8620
8841
|
return { ok: true };
|
|
8621
8842
|
}
|
|
8622
|
-
function runBmadInstall(repoRoot, modules = selectedBmadModules(repoRoot), version = BMAD_INSTALLER_VERSION) {
|
|
8843
|
+
async function runBmadInstall(repoRoot, modules = selectedBmadModules(repoRoot), version = BMAD_INSTALLER_VERSION) {
|
|
8844
|
+
const installedSelection = manifestBmadModules(repoRoot);
|
|
8845
|
+
const preservation = g33BmadUpdateBoundary(repoRoot, installedSelection.status === "valid" && installedSelection.hasG33 || configuredBmadModuleSelection(repoRoot)?.hasG33);
|
|
8846
|
+
if (preservation) return { ok: false, error: preservation.summary, companion: preservation };
|
|
8623
8847
|
const invocation = bmadInstallerInvocation(version);
|
|
8624
|
-
const result2 =
|
|
8848
|
+
const result2 = spawnSync4(invocation.command, [...invocation.prefixArgs, ...bmadInstallerArgs(repoRoot, modules)], { encoding: "utf8" });
|
|
8625
8849
|
if (result2.status !== 0) {
|
|
8626
8850
|
const stdoutTail = result2.stdout?.trim().split("\n").slice(-12).join("\n");
|
|
8627
8851
|
return { ok: false, error: result2.stderr?.trim() || stdoutTail || result2.error?.message || `installer exited ${result2.status}` };
|
|
8628
8852
|
}
|
|
8629
|
-
return { ok: true };
|
|
8853
|
+
return { ok: true, companion: await runG33Companion(repoRoot, false, "bmad-install") };
|
|
8630
8854
|
}
|
|
8631
8855
|
function bmadChannelTarget(distTags) {
|
|
8632
8856
|
const next = distTags?.[BMAD_TARGET_CHANNEL];
|
|
@@ -8635,7 +8859,7 @@ function bmadChannelTarget(distTags) {
|
|
|
8635
8859
|
return next ? { version: next, channel: BMAD_TARGET_CHANNEL } : void 0;
|
|
8636
8860
|
}
|
|
8637
8861
|
function readInstalledBmadVersion(repoRoot) {
|
|
8638
|
-
const raw = safeReadText2(
|
|
8862
|
+
const raw = safeReadText2(join7(repoRoot, "_bmad", "_config", "manifest.yaml"));
|
|
8639
8863
|
if (!raw) return void 0;
|
|
8640
8864
|
try {
|
|
8641
8865
|
const parsed = YAML2.parse(raw);
|
|
@@ -8646,8 +8870,8 @@ function readInstalledBmadVersion(repoRoot) {
|
|
|
8646
8870
|
}
|
|
8647
8871
|
}
|
|
8648
8872
|
function bmadCachePath(homeDir) {
|
|
8649
|
-
const cacheRoot = process.env.XDG_CACHE_HOME?.trim() ||
|
|
8650
|
-
return
|
|
8873
|
+
const cacheRoot = process.env.XDG_CACHE_HOME?.trim() || join7(homeDir, ".cache");
|
|
8874
|
+
return join7(cacheRoot, "pjangler", "bmad-dist-tags.json");
|
|
8651
8875
|
}
|
|
8652
8876
|
function readBmadDistTagsCache(homeDir) {
|
|
8653
8877
|
const raw = safeReadText2(bmadCachePath(homeDir));
|
|
@@ -8662,7 +8886,7 @@ function readBmadDistTagsCache(homeDir) {
|
|
|
8662
8886
|
return void 0;
|
|
8663
8887
|
}
|
|
8664
8888
|
function fetchBmadDistTags() {
|
|
8665
|
-
const result2 =
|
|
8889
|
+
const result2 = spawnSync4("npm", ["view", BMAD_NPM_PACKAGE, "dist-tags", "--json"], {
|
|
8666
8890
|
encoding: "utf8",
|
|
8667
8891
|
timeout: 8e3
|
|
8668
8892
|
});
|
|
@@ -8814,8 +9038,8 @@ var UNSUPPORTED_BMAD_ROOTS = {
|
|
|
8814
9038
|
".zencoder": "zencoder"
|
|
8815
9039
|
};
|
|
8816
9040
|
function unsupportedRootAttestation(repoRoot, rootName) {
|
|
8817
|
-
const root =
|
|
8818
|
-
const stat =
|
|
9041
|
+
const root = join7(repoRoot, rootName);
|
|
9042
|
+
const stat = lstatSync6(root);
|
|
8819
9043
|
if (!stat.isDirectory() || stat.isSymbolicLink()) return { safe: false, reason: `${rootName} is not a regular generated directory` };
|
|
8820
9044
|
const installerTool = UNSUPPORTED_BMAD_ROOTS[rootName];
|
|
8821
9045
|
if (!installedBmadTools(repoRoot).has(installerTool)) {
|
|
@@ -8834,8 +9058,8 @@ var OPTIONAL_TEMPLATE_SCRIPTS = [
|
|
|
8834
9058
|
function optionalTemplateScriptIssues(ctx) {
|
|
8835
9059
|
const issues = [];
|
|
8836
9060
|
for (const rel of OPTIONAL_TEMPLATE_SCRIPTS) {
|
|
8837
|
-
const path =
|
|
8838
|
-
if (!
|
|
9061
|
+
const path = join7(ctx.repoRoot, rel);
|
|
9062
|
+
if (!existsSync4(path)) continue;
|
|
8839
9063
|
const expected = templateCommonProjectText(ctx, rel);
|
|
8840
9064
|
if (expected === void 0) continue;
|
|
8841
9065
|
if (safeReadText2(path) !== expected) issues.push(`${rel} is present but has drifted from the shipped template`);
|
|
@@ -8845,8 +9069,8 @@ function optionalTemplateScriptIssues(ctx) {
|
|
|
8845
9069
|
function refreshOptionalTemplateScripts(ctx) {
|
|
8846
9070
|
const changed = [];
|
|
8847
9071
|
for (const rel of OPTIONAL_TEMPLATE_SCRIPTS) {
|
|
8848
|
-
const path =
|
|
8849
|
-
if (!
|
|
9072
|
+
const path = join7(ctx.repoRoot, rel);
|
|
9073
|
+
if (!existsSync4(path)) continue;
|
|
8850
9074
|
const expected = templateCommonProjectText(ctx, rel);
|
|
8851
9075
|
if (expected === void 0 || safeReadText2(path) === expected) continue;
|
|
8852
9076
|
changed.push(path);
|
|
@@ -8863,14 +9087,14 @@ function createMiseChecks() {
|
|
|
8863
9087
|
id: "mise.config-root",
|
|
8864
9088
|
title: "mise config_root + AGENTS link hooks",
|
|
8865
9089
|
audit: (ctx) => {
|
|
8866
|
-
const misePath =
|
|
8867
|
-
if (!
|
|
9090
|
+
const misePath = join7(ctx.repoRoot, "mise.toml");
|
|
9091
|
+
if (!existsSync4(misePath)) {
|
|
8868
9092
|
return { id: "mise.config-root", title: "mise config_root + AGENTS link hooks", status: "fail", summary: "mise.toml missing", details: [], fixable: true };
|
|
8869
9093
|
}
|
|
8870
9094
|
const text = readText(misePath);
|
|
8871
9095
|
const details = [];
|
|
8872
|
-
const linkAgentfilesPath =
|
|
8873
|
-
if (!
|
|
9096
|
+
const linkAgentfilesPath = join7(ctx.repoRoot, ".mise", "scripts", "link-agentfiles.sh");
|
|
9097
|
+
if (!existsSync4(linkAgentfilesPath)) details.push(".mise/scripts/link-agentfiles.sh missing");
|
|
8874
9098
|
const toml = [];
|
|
8875
9099
|
const manual = [];
|
|
8876
9100
|
const parsed = parsedOrUndefined(text);
|
|
@@ -8917,10 +9141,10 @@ function createMiseChecks() {
|
|
|
8917
9141
|
};
|
|
8918
9142
|
},
|
|
8919
9143
|
migrate: (ctx, finding2) => {
|
|
8920
|
-
const path =
|
|
9144
|
+
const path = join7(ctx.repoRoot, "mise.toml");
|
|
8921
9145
|
const changedFiles = [];
|
|
8922
9146
|
const details = [];
|
|
8923
|
-
if (!
|
|
9147
|
+
if (!existsSync4(path)) {
|
|
8924
9148
|
if (ensureMiseTomlFromTemplate(ctx, changedFiles) === null) {
|
|
8925
9149
|
return { id: finding2.id, title: finding2.title, status: "blocked", summary: "mise.toml missing and no generated-project mise template available to initialize from", changedFiles, details: [] };
|
|
8926
9150
|
}
|
|
@@ -8936,7 +9160,7 @@ function createMiseChecks() {
|
|
|
8936
9160
|
if (!ctx.dryRun) writeText(path, plan.text);
|
|
8937
9161
|
text = plan.text;
|
|
8938
9162
|
}
|
|
8939
|
-
const linkAgentfilesPath =
|
|
9163
|
+
const linkAgentfilesPath = join7(ctx.repoRoot, ".mise", "scripts", "link-agentfiles.sh");
|
|
8940
9164
|
const expectedScript = templateLinkAgentfilesScript(ctx);
|
|
8941
9165
|
if (expectedScript === void 0) {
|
|
8942
9166
|
return { id: finding2.id, title: finding2.title, status: "blocked", summary: "pjangler install is missing .mise/scripts/link-agentfiles.sh \u2014 update @delorenj/pjangler (broken package)", changedFiles, details: [] };
|
|
@@ -8974,13 +9198,13 @@ function createMiseChecks() {
|
|
|
8974
9198
|
title: "managed mise versioning block",
|
|
8975
9199
|
audit: (ctx) => {
|
|
8976
9200
|
const details = [];
|
|
8977
|
-
const misePath =
|
|
8978
|
-
const versioningPath =
|
|
8979
|
-
const manifestPath =
|
|
9201
|
+
const misePath = join7(ctx.repoRoot, "mise.toml");
|
|
9202
|
+
const versioningPath = join7(ctx.repoRoot, ".mise", "scripts", "versioning.sh");
|
|
9203
|
+
const manifestPath = join7(ctx.repoRoot, ".mise", "version-files.conf");
|
|
8980
9204
|
const text = safeReadText2(misePath);
|
|
8981
9205
|
if (!text?.includes("# >>> mise-versioning >>>")) details.push("mise versioning managed block missing");
|
|
8982
|
-
if (!
|
|
8983
|
-
if (!
|
|
9206
|
+
if (!existsSync4(versioningPath)) details.push(".mise/scripts/versioning.sh missing");
|
|
9207
|
+
if (!existsSync4(manifestPath)) details.push(".mise/version-files.conf missing");
|
|
8984
9208
|
const conf = safeReadText2(manifestPath);
|
|
8985
9209
|
const script = safeReadText2(versioningPath);
|
|
8986
9210
|
if (conf !== null && script !== null) {
|
|
@@ -9002,8 +9226,8 @@ function createMiseChecks() {
|
|
|
9002
9226
|
migrate: (ctx, finding2) => {
|
|
9003
9227
|
const changedFiles = [];
|
|
9004
9228
|
const details = [];
|
|
9005
|
-
const misePath =
|
|
9006
|
-
if (!
|
|
9229
|
+
const misePath = join7(ctx.repoRoot, "mise.toml");
|
|
9230
|
+
if (!existsSync4(misePath)) {
|
|
9007
9231
|
if (ensureMiseTomlFromTemplate(ctx, changedFiles) === null) {
|
|
9008
9232
|
return { id: finding2.id, title: finding2.title, status: "blocked", summary: "mise.toml missing and no generated-project mise template available to initialize from", changedFiles, details: [] };
|
|
9009
9233
|
}
|
|
@@ -9020,7 +9244,7 @@ function createMiseChecks() {
|
|
|
9020
9244
|
if (!changedFiles.includes(misePath)) changedFiles.push(misePath);
|
|
9021
9245
|
if (!ctx.dryRun) writeText(misePath, versioningPlan.text);
|
|
9022
9246
|
}
|
|
9023
|
-
const versioningPath =
|
|
9247
|
+
const versioningPath = join7(ctx.repoRoot, ".mise", "scripts", "versioning.sh");
|
|
9024
9248
|
const expectedScript = templateVersioningScript(ctx);
|
|
9025
9249
|
if (expectedScript === void 0) {
|
|
9026
9250
|
return { id: finding2.id, title: finding2.title, status: "blocked", summary: "pjangler install is missing .mise/scripts/versioning.sh \u2014 update @delorenj/pjangler (broken package)", changedFiles, details: [] };
|
|
@@ -9032,7 +9256,7 @@ function createMiseChecks() {
|
|
|
9032
9256
|
chmodSync(versioningPath, 493);
|
|
9033
9257
|
}
|
|
9034
9258
|
}
|
|
9035
|
-
const manifestPath =
|
|
9259
|
+
const manifestPath = join7(ctx.repoRoot, ".mise", "version-files.conf");
|
|
9036
9260
|
const expectedManifest = templateVersionFilesConf(ctx, ctx.repoRoot);
|
|
9037
9261
|
if (safeReadText2(manifestPath) !== expectedManifest) {
|
|
9038
9262
|
changedFiles.push(manifestPath);
|
|
@@ -9062,7 +9286,7 @@ function planVersioningBlock(original) {
|
|
|
9062
9286
|
let replaced = [...VERSIONING_TASK_NAMES];
|
|
9063
9287
|
try {
|
|
9064
9288
|
if (oldBlock) {
|
|
9065
|
-
replaced = Object.keys(
|
|
9289
|
+
replaced = Object.keys(record2(record2(parseToml2(oldBlock)).tasks));
|
|
9066
9290
|
text = text.replace(blockPattern, VERSIONING_BLOCK);
|
|
9067
9291
|
} else {
|
|
9068
9292
|
const statements = scanToml(text);
|
|
@@ -9085,7 +9309,7 @@ function planVersioningBlock(original) {
|
|
|
9085
9309
|
const verdict = checkRewrite(expected, text);
|
|
9086
9310
|
return verdict.ok ? { text } : { text: original, refused: verdict.reason };
|
|
9087
9311
|
}
|
|
9088
|
-
var
|
|
9312
|
+
var record2 = (value) => isTable(value) ? value : {};
|
|
9089
9313
|
var RETIRED_SKILL_SCRIPTS = [
|
|
9090
9314
|
SYNC_SKILLS_SCRIPT_REL,
|
|
9091
9315
|
PROVISION_PACKS_SCRIPT_REL,
|
|
@@ -9094,39 +9318,30 @@ var RETIRED_SKILL_SCRIPTS = [
|
|
|
9094
9318
|
".mise/scripts/unlink-project-skills-from-clis.sh"
|
|
9095
9319
|
];
|
|
9096
9320
|
function retiredSkillsScripts(ctx) {
|
|
9097
|
-
return RETIRED_SKILL_SCRIPTS.map((path) =>
|
|
9321
|
+
return RETIRED_SKILL_SCRIPTS.map((path) => join7(ctx.repoRoot, path)).filter((path) => Boolean(lstatIfPresent(path)));
|
|
9098
9322
|
}
|
|
9099
9323
|
function skillsWiringFindings(text) {
|
|
9100
|
-
if (text === null) return { fixable: [
|
|
9324
|
+
if (text === null) return { fixable: [], manual: [] };
|
|
9101
9325
|
const fixable = [];
|
|
9102
9326
|
const manual = [];
|
|
9103
9327
|
let parsed = parsedOrUndefined(text);
|
|
9104
9328
|
if (!parsed) {
|
|
9105
9329
|
const baseline = parseBaseline(text);
|
|
9106
|
-
if ("refused" in baseline) return { fixable: [], manual: ["mise.toml is invalid TOML; repair it before
|
|
9330
|
+
if ("refused" in baseline) return { fixable: [], manual: ["mise.toml is invalid TOML; repair it before pjangler can retire skill tasks"] };
|
|
9107
9331
|
parsed = baseline.parsed;
|
|
9108
9332
|
fixable.push("mise.toml repeats the close of a [hooks] array (an earlier pjangler run wrote it); migrate repairs it");
|
|
9109
9333
|
}
|
|
9110
|
-
const
|
|
9111
|
-
const tasks =
|
|
9112
|
-
|
|
9113
|
-
|
|
9114
|
-
fixable.push("skills:sync must explicitly target {{config_root}} with the Node CLI");
|
|
9115
|
-
}
|
|
9116
|
-
const pin = record2(task.tools)["npm:@delorenj/skillex"];
|
|
9117
|
-
const pinned = typeof pin === "string" ? pin : record2(pin).version;
|
|
9118
|
-
if (pinned !== SKILLEX_TOOL_VERSION) {
|
|
9119
|
-
fixable.push(`skills:sync must pin @delorenj/skillex ${SKILLEX_TOOL_VERSION} in its own tools table`);
|
|
9120
|
-
} else if (typeof pin === "string") {
|
|
9121
|
-
fixable.push(`skills:sync pins @delorenj/skillex as the plain string "${pin}", which mise 2026.9 refuses to install inside its 30-day minimumPackageAge; pin { version = "${SKILLEX_TOOL_VERSION}", allow_low_downloads = true }`);
|
|
9122
|
-
} else if (record2(pin).allow_low_downloads !== true) {
|
|
9123
|
-
fixable.push(`skills:sync must approve @delorenj/skillex ${SKILLEX_TOOL_VERSION} with allow_low_downloads = true (mise 2026.9 refuses to install it otherwise)`);
|
|
9334
|
+
const record3 = (value) => isTable(value) ? value : {};
|
|
9335
|
+
const tasks = record3(parsed.tasks);
|
|
9336
|
+
if (tasks[SKILLS_SYNC_TASK] !== void 0) {
|
|
9337
|
+
fixable.push(`Retire the ${SKILLS_SYNC_TASK} mise task; invoke skillex directly (pj skills sync --scope project --project .)`);
|
|
9124
9338
|
}
|
|
9125
9339
|
const retired = /sync-skills\.py|provision-(?:packs|bmad-skills)\.py|skills[:\-]provision/;
|
|
9126
9340
|
const renames = new Map(RETIRED_TASK_RENAMES);
|
|
9127
|
-
const replaced = /* @__PURE__ */ new Set([...RETIRED_LINK_TASKS, LINK_AGENTFILES_TASK
|
|
9341
|
+
const replaced = /* @__PURE__ */ new Set([...RETIRED_LINK_TASKS, LINK_AGENTFILES_TASK]);
|
|
9128
9342
|
for (const [name, entry] of Object.entries(tasks)) {
|
|
9129
|
-
if (
|
|
9343
|
+
if (name === SKILLS_SYNC_TASK) continue;
|
|
9344
|
+
if (!retired.test(name) && !retired.test(JSON.stringify(record3(entry).run ?? ""))) continue;
|
|
9130
9345
|
if (replaced.has(renames.get(name) ?? name)) {
|
|
9131
9346
|
if (!fixable.includes("Retire Python skill sync/provision tasks from mise.toml")) fixable.push("Retire Python skill sync/provision tasks from mise.toml");
|
|
9132
9347
|
} else {
|
|
@@ -9140,7 +9355,7 @@ function skillsWiringFindings(text) {
|
|
|
9140
9355
|
}
|
|
9141
9356
|
manual.push(...decisions.manual.filter((entry) => SKILL_WRITER_MENTION.test(entry.command)).map(manualHookLine));
|
|
9142
9357
|
const watches = Array.isArray(parsed.watch_files) ? parsed.watch_files : [];
|
|
9143
|
-
if (managedWatchPlan(watches).skills.length) fixable.push("Remove the skills watch hook; run skills
|
|
9358
|
+
if (managedWatchPlan(watches).skills.length) fixable.push("Remove the skills watch hook; run pj skills sync explicitly, never automatically");
|
|
9144
9359
|
return { fixable, manual };
|
|
9145
9360
|
}
|
|
9146
9361
|
function skillsWiringIssues(text) {
|
|
@@ -9154,15 +9369,16 @@ function createAgentHooksChecks() {
|
|
|
9154
9369
|
title: "Skillex project skills",
|
|
9155
9370
|
audit: async (ctx) => {
|
|
9156
9371
|
const finding2 = await auditProjectSkills(ctx);
|
|
9157
|
-
const mise = safeReadText2(
|
|
9372
|
+
const mise = safeReadText2(join7(ctx.repoRoot, "mise.toml"));
|
|
9158
9373
|
const wiring = skillsWiringIssues(mise);
|
|
9159
9374
|
const retired = retiredSkillsScripts(ctx);
|
|
9160
9375
|
const unsafe = retired.filter((path) => lstatIfPresent(path)?.isDirectory());
|
|
9161
9376
|
const details = [...finding2.details, ...wiring, ...retired.map((path) => unsafe.includes(path) ? `Inspect and preserve retired script directory: ${path}` : `${path} is a retired skill writer`)];
|
|
9377
|
+
const miseDrift = mise !== null && upsertLinkAgentfilesBlock(mise, ctx) !== mise;
|
|
9162
9378
|
return {
|
|
9163
9379
|
...finding2,
|
|
9164
9380
|
status: wiring.length || retired.length ? "fail" : finding2.status,
|
|
9165
|
-
fixable: unsafe.length ? false : finding2.fixable || wiring.length > 0 || retired.length > 0,
|
|
9381
|
+
fixable: unsafe.length ? false : finding2.fixable || wiring.length > 0 || retired.length > 0 || miseDrift,
|
|
9166
9382
|
summary: details.length ? `${details.length} Skillex finding(s)` : finding2.summary,
|
|
9167
9383
|
details
|
|
9168
9384
|
};
|
|
@@ -9171,7 +9387,7 @@ function createAgentHooksChecks() {
|
|
|
9171
9387
|
const changedFiles = [];
|
|
9172
9388
|
const details = [];
|
|
9173
9389
|
const result2 = (status, summary) => ({ id: finding2.id, title: finding2.title, status, summary, changedFiles: [...new Set(changedFiles)], details });
|
|
9174
|
-
let currentMise = safeReadText2(
|
|
9390
|
+
let currentMise = safeReadText2(join7(ctx.repoRoot, "mise.toml"));
|
|
9175
9391
|
if (currentMise === null) {
|
|
9176
9392
|
const template = templateCommonProjectText(ctx, "mise.toml.jinja");
|
|
9177
9393
|
if (template === void 0) return result2("blocked", "Generated-project mise template is unavailable");
|
|
@@ -9188,7 +9404,7 @@ function createAgentHooksChecks() {
|
|
|
9188
9404
|
changedFiles.push(path);
|
|
9189
9405
|
if (!ctx.dryRun) unlinkSync3(path);
|
|
9190
9406
|
}
|
|
9191
|
-
const misePath =
|
|
9407
|
+
const misePath = join7(ctx.repoRoot, "mise.toml");
|
|
9192
9408
|
const nextMise = upsertLinkAgentfilesBlock(currentMise, ctx);
|
|
9193
9409
|
if (safeReadText2(misePath) !== nextMise) {
|
|
9194
9410
|
changedFiles.push(misePath);
|
|
@@ -9207,16 +9423,16 @@ function createAgentHooksChecks() {
|
|
|
9207
9423
|
if (!reconciled.ok || !roots.ok) {
|
|
9208
9424
|
return result2(changedFiles.length ? "partial" : "blocked", "Skillex requires attention before project migration");
|
|
9209
9425
|
}
|
|
9210
|
-
return changedFiles.length ? result2("applied", "Project skills use the Node core and
|
|
9426
|
+
return changedFiles.length ? result2("applied", "Project skills use the Node core and direct invocation") : result2("noop", "No changes required");
|
|
9211
9427
|
}
|
|
9212
9428
|
},
|
|
9213
9429
|
{
|
|
9214
9430
|
id: "sot.agent-symlinks",
|
|
9215
9431
|
title: "AGENTS/CLAUDE/GEMINI symlink contract",
|
|
9216
9432
|
audit: (ctx) => {
|
|
9217
|
-
const agentsPath =
|
|
9218
|
-
if (!
|
|
9219
|
-
const fallbackSources = ["CLAUDE.md", "GEMINI.md", "README.md"].filter((file) =>
|
|
9433
|
+
const agentsPath = join7(ctx.repoRoot, "AGENTS.md");
|
|
9434
|
+
if (!existsSync4(agentsPath)) {
|
|
9435
|
+
const fallbackSources = ["CLAUDE.md", "GEMINI.md", "README.md"].filter((file) => existsSync4(join7(ctx.repoRoot, file)));
|
|
9220
9436
|
if (fallbackSources.length === 0) {
|
|
9221
9437
|
return { id: "sot.agent-symlinks", title: "AGENTS/CLAUDE/GEMINI symlink contract", status: "skip", summary: "AGENTS.md missing; symlink contract not applicable", details: [], fixable: false };
|
|
9222
9438
|
}
|
|
@@ -9231,7 +9447,7 @@ function createAgentHooksChecks() {
|
|
|
9231
9447
|
}
|
|
9232
9448
|
const details = [];
|
|
9233
9449
|
for (const file of ["CLAUDE.md", "GEMINI.md"]) {
|
|
9234
|
-
const full =
|
|
9450
|
+
const full = join7(ctx.repoRoot, file);
|
|
9235
9451
|
const target = readSymlinkTarget(full);
|
|
9236
9452
|
if (target !== "AGENTS.md") details.push(`${file} should be a symlink to AGENTS.md`);
|
|
9237
9453
|
}
|
|
@@ -9255,7 +9471,7 @@ function createAgentHooksChecks() {
|
|
|
9255
9471
|
return { id: finding2.id, title: finding2.title, status: "blocked", summary: "AGENTS.md missing; cannot derive canonical agent file", changedFiles, details: [bootstrap.blocked] };
|
|
9256
9472
|
}
|
|
9257
9473
|
for (const file of ["CLAUDE.md", "GEMINI.md"]) {
|
|
9258
|
-
const full =
|
|
9474
|
+
const full = join7(ctx.repoRoot, file);
|
|
9259
9475
|
const result2 = ensureSymlink(full, "AGENTS.md", ctx.dryRun);
|
|
9260
9476
|
if (result2.blocked) blockedDetails.push(result2.blocked);
|
|
9261
9477
|
if (result2.changed) changedFiles.push(full);
|
|
@@ -9282,7 +9498,7 @@ function createProjectJsonChecks() {
|
|
|
9282
9498
|
const changedFiles = [];
|
|
9283
9499
|
const blockedDetails = [];
|
|
9284
9500
|
const droppedDetails = [];
|
|
9285
|
-
const path =
|
|
9501
|
+
const path = join7(ctx.repoRoot, ".project.json");
|
|
9286
9502
|
const existing = readProjectJson(ctx) ?? {};
|
|
9287
9503
|
const canonical = canonicalProjectJson(ctx);
|
|
9288
9504
|
const preservedDetails = [];
|
|
@@ -9314,11 +9530,11 @@ function createProjectJsonChecks() {
|
|
|
9314
9530
|
changedFiles.push(path);
|
|
9315
9531
|
if (!ctx.dryRun) writeText(path, expected);
|
|
9316
9532
|
}
|
|
9317
|
-
const planeJson =
|
|
9318
|
-
if (
|
|
9533
|
+
const planeJson = join7(ctx.repoRoot, ".plane.json");
|
|
9534
|
+
if (existsSync4(planeJson)) {
|
|
9319
9535
|
const backup = `${planeJson}.migrated-backup`;
|
|
9320
|
-
if (
|
|
9321
|
-
blockedDetails.push(`cannot back up .plane.json because ${
|
|
9536
|
+
if (existsSync4(backup)) {
|
|
9537
|
+
blockedDetails.push(`cannot back up .plane.json because ${relative5(ctx.repoRoot, backup)} already exists`);
|
|
9322
9538
|
} else {
|
|
9323
9539
|
changedFiles.push(backup);
|
|
9324
9540
|
if (!ctx.dryRun) renameSync2(planeJson, backup);
|
|
@@ -9346,10 +9562,10 @@ function createMiseOpInjectChecks() {
|
|
|
9346
9562
|
const details = [];
|
|
9347
9563
|
let envOpNeedsHands = false;
|
|
9348
9564
|
const optedOut = envMaterializationOptedOut(ctx);
|
|
9349
|
-
const envOpPath =
|
|
9350
|
-
const envOpExists =
|
|
9565
|
+
const envOpPath = join7(ctx.repoRoot, ".env.op");
|
|
9566
|
+
const envOpExists = existsSync4(envOpPath);
|
|
9351
9567
|
const envOp = envOpExists ? readText(envOpPath) : void 0;
|
|
9352
|
-
const gitignore = safeReadText2(
|
|
9568
|
+
const gitignore = safeReadText2(join7(ctx.repoRoot, ".gitignore"));
|
|
9353
9569
|
if (!envOpExists) {
|
|
9354
9570
|
details.push(".env.op missing");
|
|
9355
9571
|
} else if (!envOp?.trim()) {
|
|
@@ -9369,7 +9585,7 @@ function createMiseOpInjectChecks() {
|
|
|
9369
9585
|
if (!gitignore?.includes(".env\n") && !gitignore?.includes(".env\r\n")) details.push(".gitignore should ignore .env");
|
|
9370
9586
|
if (!gitignore?.includes(".env.*")) details.push(".gitignore should ignore .env.*");
|
|
9371
9587
|
if (!gitignore?.includes("!.env.op")) details.push(".gitignore should unignore .env.op");
|
|
9372
|
-
const misePath =
|
|
9588
|
+
const misePath = join7(ctx.repoRoot, "mise.toml");
|
|
9373
9589
|
const miseText = safeReadText2(misePath);
|
|
9374
9590
|
if (!miseText) {
|
|
9375
9591
|
details.push("mise.toml missing for .env materialization hook");
|
|
@@ -9392,15 +9608,15 @@ function createMiseOpInjectChecks() {
|
|
|
9392
9608
|
else if (plan.text !== miseText && !mise.length) mise.push(describeRewrite(miseText, plan.text));
|
|
9393
9609
|
details.push(...mise);
|
|
9394
9610
|
}
|
|
9395
|
-
const materializePath =
|
|
9611
|
+
const materializePath = join7(ctx.repoRoot, MATERIALIZE_ENV_SCRIPT_REL);
|
|
9396
9612
|
const expectedMaterializer = templateMaterializeEnvScript(ctx);
|
|
9397
9613
|
if (optedOut) {
|
|
9398
|
-
if (
|
|
9614
|
+
if (existsSync4(materializePath)) details.push(`${MATERIALIZE_ENV_SCRIPT_REL} is present, but .project.json declares secrets.materialize_env: false`);
|
|
9399
9615
|
} else if (!expectedMaterializer) {
|
|
9400
9616
|
details.push("pjangler package is missing the managed materialize-env.sh source");
|
|
9401
9617
|
} else if (safeReadText2(materializePath) !== expectedMaterializer) {
|
|
9402
9618
|
details.push(`${MATERIALIZE_ENV_SCRIPT_REL} missing or drifted`);
|
|
9403
|
-
} else if ((
|
|
9619
|
+
} else if ((lstatSync6(materializePath).mode & 73) === 0) {
|
|
9404
9620
|
details.push(`${MATERIALIZE_ENV_SCRIPT_REL} is not executable`);
|
|
9405
9621
|
}
|
|
9406
9622
|
return {
|
|
@@ -9416,13 +9632,13 @@ function createMiseOpInjectChecks() {
|
|
|
9416
9632
|
const changedFiles = [];
|
|
9417
9633
|
const details = [];
|
|
9418
9634
|
let envOpNeedsHands = false;
|
|
9419
|
-
const envOpPath =
|
|
9420
|
-
const canonicalEnvOpPath =
|
|
9421
|
-
if (!
|
|
9635
|
+
const envOpPath = join7(ctx.repoRoot, ".env.op");
|
|
9636
|
+
const canonicalEnvOpPath = join7(ctx.pjanglerRoot, "templates", "commonproject", "template", ".env.op");
|
|
9637
|
+
if (!existsSync4(canonicalEnvOpPath)) {
|
|
9422
9638
|
return { id: finding2.id, title: finding2.title, status: "blocked", summary: "pjangler package is missing the neutral .env.op template", changedFiles: [], details: [] };
|
|
9423
9639
|
}
|
|
9424
9640
|
const canonicalEnvOp = readText(canonicalEnvOpPath);
|
|
9425
|
-
if (!
|
|
9641
|
+
if (!existsSync4(envOpPath) || !readText(envOpPath).trim()) {
|
|
9426
9642
|
changedFiles.push(envOpPath);
|
|
9427
9643
|
if (!ctx.dryRun) writeText(envOpPath, canonicalEnvOp);
|
|
9428
9644
|
} else {
|
|
@@ -9446,7 +9662,7 @@ function createMiseOpInjectChecks() {
|
|
|
9446
9662
|
}
|
|
9447
9663
|
}
|
|
9448
9664
|
}
|
|
9449
|
-
const gitignorePath =
|
|
9665
|
+
const gitignorePath = join7(ctx.repoRoot, ".gitignore");
|
|
9450
9666
|
const gitignore = safeReadText2(gitignorePath) ?? "";
|
|
9451
9667
|
const requiredBlock = `# Secrets \u2014 .env is materialized from .env.op by \`op inject\` on mise enter,
|
|
9452
9668
|
# staged through a mktemp file and moved into place only on success.
|
|
@@ -9459,7 +9675,7 @@ function createMiseOpInjectChecks() {
|
|
|
9459
9675
|
changedFiles.push(gitignorePath);
|
|
9460
9676
|
if (!ctx.dryRun) writeText(gitignorePath, `${gitignore.replace(/\s*$/, "")}${gitignore.trim() ? "\n\n" : ""}${requiredBlock}`);
|
|
9461
9677
|
}
|
|
9462
|
-
const misePath =
|
|
9678
|
+
const misePath = join7(ctx.repoRoot, "mise.toml");
|
|
9463
9679
|
let currentMise = safeReadText2(misePath);
|
|
9464
9680
|
if (currentMise === null) {
|
|
9465
9681
|
const initialized = ensureMiseTomlFromTemplate(ctx, changedFiles);
|
|
@@ -9476,16 +9692,16 @@ function createMiseOpInjectChecks() {
|
|
|
9476
9692
|
if (!changedFiles.includes(misePath)) changedFiles.push(misePath);
|
|
9477
9693
|
if (!ctx.dryRun) writeText(misePath, opPlan.text);
|
|
9478
9694
|
}
|
|
9479
|
-
const materializePath =
|
|
9695
|
+
const materializePath = join7(ctx.repoRoot, MATERIALIZE_ENV_SCRIPT_REL);
|
|
9480
9696
|
const expectedMaterializer = templateMaterializeEnvScript(ctx);
|
|
9481
9697
|
if (optedOut) {
|
|
9482
|
-
if (
|
|
9698
|
+
if (existsSync4(materializePath)) {
|
|
9483
9699
|
changedFiles.push(materializePath);
|
|
9484
9700
|
if (!ctx.dryRun) rmSync2(materializePath);
|
|
9485
9701
|
}
|
|
9486
9702
|
} else if (!expectedMaterializer) {
|
|
9487
9703
|
return { id: finding2.id, title: finding2.title, status: "blocked", summary: "pjangler package is missing materialize-env.sh", changedFiles: [], details: [] };
|
|
9488
|
-
} else if (safeReadText2(materializePath) !== expectedMaterializer ||
|
|
9704
|
+
} else if (safeReadText2(materializePath) !== expectedMaterializer || existsSync4(materializePath) && (lstatSync6(materializePath).mode & 73) === 0) {
|
|
9489
9705
|
changedFiles.push(materializePath);
|
|
9490
9706
|
if (!ctx.dryRun) {
|
|
9491
9707
|
writeText(materializePath, expectedMaterializer);
|
|
@@ -9547,7 +9763,7 @@ function createProjectProvenanceChecks() {
|
|
|
9547
9763
|
title: ".copier-answers.yml provenance + drift report",
|
|
9548
9764
|
audit: (ctx) => {
|
|
9549
9765
|
const details = [];
|
|
9550
|
-
const path =
|
|
9766
|
+
const path = join7(ctx.repoRoot, ".copier-answers.yml");
|
|
9551
9767
|
const text = safeReadText2(path);
|
|
9552
9768
|
const project = readProjectJson(ctx);
|
|
9553
9769
|
if (!text) {
|
|
@@ -9575,11 +9791,11 @@ function createProjectProvenanceChecks() {
|
|
|
9575
9791
|
migrate: (ctx, finding2) => {
|
|
9576
9792
|
const changedFiles = [];
|
|
9577
9793
|
const project = canonicalProjectJson(ctx);
|
|
9578
|
-
const path =
|
|
9794
|
+
const path = join7(ctx.repoRoot, ".copier-answers.yml");
|
|
9579
9795
|
const current = safeReadText2(path);
|
|
9580
9796
|
const existing = current ? YAML2.parseDocument(current) : void 0;
|
|
9581
9797
|
const document = existing && !existing.errors.length && YAML2.isMap(existing.contents) ? existing : new YAML2.Document({});
|
|
9582
|
-
document.set("_src_path",
|
|
9798
|
+
document.set("_src_path", join7(ctx.pjanglerRoot, "templates", "commonproject"));
|
|
9583
9799
|
document.set("project_description", String(project.project_description));
|
|
9584
9800
|
document.set("project_name", String(project.project_name));
|
|
9585
9801
|
document.set("ticket_provider", String(project.ticket_provider?.type ?? "plane"));
|
|
@@ -9604,9 +9820,9 @@ ${body}`;
|
|
|
9604
9820
|
}
|
|
9605
9821
|
function supportedCliProjectionIssues(repoRoot, plan) {
|
|
9606
9822
|
const issues = [];
|
|
9607
|
-
const managedSkills =
|
|
9823
|
+
const managedSkills = join7(repoRoot, ".agents", "skills");
|
|
9608
9824
|
for (const rootName of SUPPORTED_CLI_ROOTS) {
|
|
9609
|
-
const root =
|
|
9825
|
+
const root = join7(repoRoot, rootName);
|
|
9610
9826
|
const rootStat = lstatIfPresent(root);
|
|
9611
9827
|
if (!rootStat) {
|
|
9612
9828
|
issues.push(`${rootName} missing`);
|
|
@@ -9616,7 +9832,7 @@ function supportedCliProjectionIssues(repoRoot, plan) {
|
|
|
9616
9832
|
issues.push(`${rootName} must be a real configuration directory`);
|
|
9617
9833
|
continue;
|
|
9618
9834
|
}
|
|
9619
|
-
const skills =
|
|
9835
|
+
const skills = join7(root, "skills");
|
|
9620
9836
|
const skillsStat = lstatIfPresent(skills);
|
|
9621
9837
|
if (!skillsStat) {
|
|
9622
9838
|
issues.push(`${rootName}/skills missing`);
|
|
@@ -9625,7 +9841,7 @@ function supportedCliProjectionIssues(repoRoot, plan) {
|
|
|
9625
9841
|
let projectedSkills = skills;
|
|
9626
9842
|
if (skillsStat.isSymbolicLink()) {
|
|
9627
9843
|
try {
|
|
9628
|
-
|
|
9844
|
+
readlinkSync4(skills);
|
|
9629
9845
|
} catch {
|
|
9630
9846
|
issues.push(`${rootName}/skills is an unreadable symlink`);
|
|
9631
9847
|
continue;
|
|
@@ -9653,9 +9869,9 @@ function supportedCliProjectionIssues(repoRoot, plan) {
|
|
|
9653
9869
|
issues.push(`${rootName}/skills is not a directory`);
|
|
9654
9870
|
continue;
|
|
9655
9871
|
}
|
|
9656
|
-
const hasGeneratedSkill =
|
|
9657
|
-
const skillFile =
|
|
9658
|
-
return
|
|
9872
|
+
const hasGeneratedSkill = existsSync4(projectedSkills) && readdirSync4(projectedSkills).some((name) => {
|
|
9873
|
+
const skillFile = join7(projectedSkills, name, "SKILL.md");
|
|
9874
|
+
return existsSync4(skillFile) && lstatSync6(skillFile).isFile();
|
|
9659
9875
|
});
|
|
9660
9876
|
if (!hasGeneratedSkill) issues.push(`${rootName}/skills contains no BMAD skill configuration`);
|
|
9661
9877
|
}
|
|
@@ -9681,7 +9897,7 @@ var LEGACY_CLI_GITIGNORE_LINES = /* @__PURE__ */ new Set([
|
|
|
9681
9897
|
"# some CLIs get a real directory here and some get a symlink."
|
|
9682
9898
|
]);
|
|
9683
9899
|
function supportedCliGitignoreIssues(repoRoot) {
|
|
9684
|
-
const lines = (safeReadText2(
|
|
9900
|
+
const lines = (safeReadText2(join7(repoRoot, ".gitignore")) ?? "").split(/\r?\n/);
|
|
9685
9901
|
const legacy = lines.filter((line) => LEGACY_CLI_GITIGNORE_LINES.has(line.trim()));
|
|
9686
9902
|
return [
|
|
9687
9903
|
...legacy.length ? [`.gitignore contains ${legacy.length} legacy client-root override line(s); only .agents is canonical`] : [],
|
|
@@ -9689,7 +9905,7 @@ function supportedCliGitignoreIssues(repoRoot) {
|
|
|
9689
9905
|
];
|
|
9690
9906
|
}
|
|
9691
9907
|
function ensureSupportedCliGitignore(ctx) {
|
|
9692
|
-
const path =
|
|
9908
|
+
const path = join7(ctx.repoRoot, ".gitignore");
|
|
9693
9909
|
const current = safeReadText2(path) ?? "";
|
|
9694
9910
|
const kept = current.split(/\r?\n/).filter((line) => {
|
|
9695
9911
|
const trimmed2 = line.trim();
|
|
@@ -9727,10 +9943,10 @@ function createBmadChecks() {
|
|
|
9727
9943
|
fixable: false
|
|
9728
9944
|
};
|
|
9729
9945
|
}
|
|
9730
|
-
const targetRoot =
|
|
9731
|
-
const selectedModules = manifestSelection.status === "valid" ? manifestSelection.modules :
|
|
9946
|
+
const targetRoot = join7(ctx.repoRoot, "_bmad");
|
|
9947
|
+
const selectedModules = manifestSelection.status === "valid" ? manifestSelection.modules : configuredBmadModuleSelection(ctx.repoRoot)?.modules ?? [...DEFAULT_BMAD_MODULES];
|
|
9732
9948
|
const sentinels = requiredBmadSentinels(ctx.repoRoot, selectedModules);
|
|
9733
|
-
const missing = sentinels.filter((file) => !
|
|
9949
|
+
const missing = sentinels.filter((file) => !existsSync4(join7(targetRoot, file)));
|
|
9734
9950
|
const projectNameIssues = bmadProjectNameIssues(ctx.repoRoot);
|
|
9735
9951
|
const details = [
|
|
9736
9952
|
...missing.map((file) => `_bmad/${file}`),
|
|
@@ -9758,10 +9974,21 @@ function createBmadChecks() {
|
|
|
9758
9974
|
details: [manifestSelection.error]
|
|
9759
9975
|
};
|
|
9760
9976
|
}
|
|
9761
|
-
const selectedModules = manifestSelection.status === "valid" ? manifestSelection.modules :
|
|
9977
|
+
const selectedModules = manifestSelection.status === "valid" ? manifestSelection.modules : configuredBmadModuleSelection(ctx.repoRoot)?.modules ?? [...DEFAULT_BMAD_MODULES];
|
|
9978
|
+
const preservation = g33BmadUpdateBoundary(ctx.repoRoot, manifestSelection.status === "valid" && manifestSelection.hasG33 || configuredBmadModuleSelection(ctx.repoRoot)?.hasG33);
|
|
9979
|
+
if (preservation) return {
|
|
9980
|
+
id: finding2.id,
|
|
9981
|
+
title: finding2.title,
|
|
9982
|
+
status: "blocked",
|
|
9983
|
+
summary: preservation.summary,
|
|
9984
|
+
changedFiles: [],
|
|
9985
|
+
details: companionDetails(preservation),
|
|
9986
|
+
bmadCompanion: preservation
|
|
9987
|
+
};
|
|
9762
9988
|
if (ctx.dryRun) {
|
|
9763
9989
|
const sentinels = requiredBmadSentinels(ctx.repoRoot, selectedModules);
|
|
9764
|
-
|
|
9990
|
+
const companion = await runG33Companion(ctx.repoRoot, true, "bmad-install");
|
|
9991
|
+
changedFiles.push(...sentinels.map((file) => join7(ctx.repoRoot, "_bmad", file)).filter((path) => !existsSync4(path)));
|
|
9765
9992
|
changedFiles.push(...bmadProjectNameIssues(ctx.repoRoot).paths);
|
|
9766
9993
|
return {
|
|
9767
9994
|
id: finding2.id,
|
|
@@ -9770,16 +9997,18 @@ function createBmadChecks() {
|
|
|
9770
9997
|
summary: changedFiles.length ? "Would run non-interactive bmad-method install" : "No changes required",
|
|
9771
9998
|
changedFiles,
|
|
9772
9999
|
details: [
|
|
9773
|
-
`Would run: ${bmadInstallDisplay(ctx.repoRoot, selectedModules)}
|
|
9774
|
-
|
|
10000
|
+
`Would run: ${bmadInstallDisplay(ctx.repoRoot, selectedModules)}`,
|
|
10001
|
+
...companionDetails(companion)
|
|
10002
|
+
],
|
|
10003
|
+
bmadCompanion: companion
|
|
9775
10004
|
};
|
|
9776
10005
|
}
|
|
9777
10006
|
const expectedChangedPaths = [
|
|
9778
|
-
...requiredBmadSentinels(ctx.repoRoot, selectedModules).map((file) =>
|
|
10007
|
+
...requiredBmadSentinels(ctx.repoRoot, selectedModules).map((file) => join7(ctx.repoRoot, "_bmad", file)).filter((path) => !existsSync4(path)),
|
|
9779
10008
|
...bmadProjectNameIssues(ctx.repoRoot).paths
|
|
9780
10009
|
];
|
|
9781
10010
|
const evicted = evictLegacyBmadPackState(ctx, changedFiles, await skillActivations(ctx));
|
|
9782
|
-
const install = runBmadInstall(ctx.repoRoot, selectedModules);
|
|
10011
|
+
const install = await runBmadInstall(ctx.repoRoot, selectedModules);
|
|
9783
10012
|
if (!install.ok) {
|
|
9784
10013
|
return {
|
|
9785
10014
|
id: finding2.id,
|
|
@@ -9790,14 +10019,15 @@ function createBmadChecks() {
|
|
|
9790
10019
|
details: [...evicted, install.error ?? "Unknown error"]
|
|
9791
10020
|
};
|
|
9792
10021
|
}
|
|
9793
|
-
changedFiles.push(...expectedChangedPaths.filter(
|
|
10022
|
+
changedFiles.push(...expectedChangedPaths.filter(existsSync4), ...install.companion?.changedFiles ?? []);
|
|
9794
10023
|
return {
|
|
9795
10024
|
id: finding2.id,
|
|
9796
10025
|
title: finding2.title,
|
|
9797
10026
|
status: changedFiles.length ? "applied" : "noop",
|
|
9798
10027
|
summary: changedFiles.length ? "Installed BMAD scaffold via bmad-method" : "No changes required",
|
|
9799
10028
|
changedFiles,
|
|
9800
|
-
details: evicted
|
|
10029
|
+
details: [...evicted, ...install.companion ? companionDetails(install.companion) : []],
|
|
10030
|
+
bmadCompanion: install.companion
|
|
9801
10031
|
};
|
|
9802
10032
|
}
|
|
9803
10033
|
},
|
|
@@ -9811,7 +10041,7 @@ function createBmadChecks() {
|
|
|
9811
10041
|
id: "bmad.version",
|
|
9812
10042
|
title: "BMAD version currency",
|
|
9813
10043
|
status: "skip",
|
|
9814
|
-
summary:
|
|
10044
|
+
summary: existsSync4(join7(ctx.repoRoot, "_bmad")) ? "BMAD installed but version manifest unreadable" : "No BMAD install present",
|
|
9815
10045
|
details: [],
|
|
9816
10046
|
fixable: false
|
|
9817
10047
|
};
|
|
@@ -9899,7 +10129,7 @@ function createBmadChecks() {
|
|
|
9899
10129
|
}
|
|
9900
10130
|
const installed = readInstalledBmadVersion(ctx.repoRoot);
|
|
9901
10131
|
const available = ctx.bmadVersionPin?.trim() ?? bmadChannelTarget(resolveBmadDistTags(ctx.homeDir)?.distTags)?.version;
|
|
9902
|
-
const manifestPath =
|
|
10132
|
+
const manifestPath = join7(ctx.repoRoot, "_bmad", "_config", "manifest.yaml");
|
|
9903
10133
|
const manifestSelection = manifestBmadModules(ctx.repoRoot);
|
|
9904
10134
|
if (manifestSelection.status === "invalid") {
|
|
9905
10135
|
return {
|
|
@@ -9911,8 +10141,19 @@ function createBmadChecks() {
|
|
|
9911
10141
|
details: [manifestSelection.error]
|
|
9912
10142
|
};
|
|
9913
10143
|
}
|
|
9914
|
-
const selectedModules = manifestSelection.status === "valid" ? manifestSelection.modules :
|
|
10144
|
+
const selectedModules = manifestSelection.status === "valid" ? manifestSelection.modules : configuredBmadModuleSelection(ctx.repoRoot)?.modules ?? [...DEFAULT_BMAD_MODULES];
|
|
10145
|
+
const preservation = g33BmadUpdateBoundary(ctx.repoRoot, manifestSelection.status === "valid" && manifestSelection.hasG33 || configuredBmadModuleSelection(ctx.repoRoot)?.hasG33);
|
|
10146
|
+
if (preservation) return {
|
|
10147
|
+
id: finding2.id,
|
|
10148
|
+
title: finding2.title,
|
|
10149
|
+
status: "blocked",
|
|
10150
|
+
summary: preservation.summary,
|
|
10151
|
+
changedFiles: [],
|
|
10152
|
+
details: companionDetails(preservation),
|
|
10153
|
+
bmadCompanion: preservation
|
|
10154
|
+
};
|
|
9915
10155
|
if (ctx.dryRun) {
|
|
10156
|
+
const companion = await runG33Companion(ctx.repoRoot, true, "bmad-install");
|
|
9916
10157
|
return {
|
|
9917
10158
|
id: finding2.id,
|
|
9918
10159
|
title: finding2.title,
|
|
@@ -9920,11 +10161,13 @@ function createBmadChecks() {
|
|
|
9920
10161
|
summary: `Would upgrade BMAD ${installed ?? "?"} -> ${available ?? BMAD_TARGET_CHANNEL}`,
|
|
9921
10162
|
changedFiles: [manifestPath],
|
|
9922
10163
|
details: [
|
|
9923
|
-
`Would run: ${bmadInstallDisplay(ctx.repoRoot, selectedModules, available ?? BMAD_TARGET_CHANNEL)}
|
|
9924
|
-
|
|
10164
|
+
`Would run: ${bmadInstallDisplay(ctx.repoRoot, selectedModules, available ?? BMAD_TARGET_CHANNEL)}`,
|
|
10165
|
+
...companionDetails(companion)
|
|
10166
|
+
],
|
|
10167
|
+
bmadCompanion: companion
|
|
9925
10168
|
};
|
|
9926
10169
|
}
|
|
9927
|
-
const install = runBmadInstall(ctx.repoRoot, selectedModules, available ?? BMAD_TARGET_CHANNEL);
|
|
10170
|
+
const install = await runBmadInstall(ctx.repoRoot, selectedModules, available ?? BMAD_TARGET_CHANNEL);
|
|
9928
10171
|
if (!install.ok) {
|
|
9929
10172
|
return {
|
|
9930
10173
|
id: finding2.id,
|
|
@@ -9941,7 +10184,8 @@ function createBmadChecks() {
|
|
|
9941
10184
|
const upgraded = Boolean(nowInstalled && installed && compareBmadVersions(nowInstalled, installed) > 0);
|
|
9942
10185
|
const changed = Array.from(/* @__PURE__ */ new Set([
|
|
9943
10186
|
...upgraded ? [manifestPath] : [],
|
|
9944
|
-
...evictedChanges
|
|
10187
|
+
...evictedChanges,
|
|
10188
|
+
...install.companion?.changedFiles ?? []
|
|
9945
10189
|
]));
|
|
9946
10190
|
return {
|
|
9947
10191
|
id: finding2.id,
|
|
@@ -9949,7 +10193,8 @@ function createBmadChecks() {
|
|
|
9949
10193
|
status: changed.length ? "applied" : "noop",
|
|
9950
10194
|
summary: upgraded ? `Upgraded BMAD ${installed} -> ${nowInstalled}` : `BMAD reinstalled (${nowInstalled ?? "?"})`,
|
|
9951
10195
|
changedFiles: changed,
|
|
9952
|
-
details: evicted
|
|
10196
|
+
details: [...evicted, ...install.companion ? companionDetails(install.companion) : []],
|
|
10197
|
+
bmadCompanion: install.companion
|
|
9953
10198
|
};
|
|
9954
10199
|
}
|
|
9955
10200
|
},
|
|
@@ -9958,7 +10203,7 @@ function createBmadChecks() {
|
|
|
9958
10203
|
title: "Supported BMAD CLI projection roots",
|
|
9959
10204
|
audit: (ctx) => {
|
|
9960
10205
|
const unsupportedNames = Object.keys(UNSUPPORTED_BMAD_ROOTS);
|
|
9961
|
-
const present = unsupportedNames.filter((name) =>
|
|
10206
|
+
const present = unsupportedNames.filter((name) => existsSync4(join7(ctx.repoRoot, name)));
|
|
9962
10207
|
const attestations = present.map((name) => ({ name, ...unsupportedRootAttestation(ctx.repoRoot, name) }));
|
|
9963
10208
|
const plan = planSkillRoots(ctx.repoRoot);
|
|
9964
10209
|
const supportedIssues = supportedCliProjectionIssues(ctx.repoRoot, plan);
|
|
@@ -9980,7 +10225,7 @@ function createBmadChecks() {
|
|
|
9980
10225
|
},
|
|
9981
10226
|
migrate: (ctx, finding2) => {
|
|
9982
10227
|
const unsupportedNames = Object.keys(UNSUPPORTED_BMAD_ROOTS);
|
|
9983
|
-
const present = unsupportedNames.filter((name) =>
|
|
10228
|
+
const present = unsupportedNames.filter((name) => existsSync4(join7(ctx.repoRoot, name)));
|
|
9984
10229
|
const attestations = present.map((name) => ({ name, ...unsupportedRootAttestation(ctx.repoRoot, name) }));
|
|
9985
10230
|
const blocked = attestations.filter((entry) => !entry.safe);
|
|
9986
10231
|
if (blocked.length) {
|
|
@@ -10005,7 +10250,7 @@ function createBmadChecks() {
|
|
|
10005
10250
|
};
|
|
10006
10251
|
}
|
|
10007
10252
|
const gitignoreChanges = ensureSupportedCliGitignore(ctx);
|
|
10008
|
-
const removedRoots = attestations.map((entry) =>
|
|
10253
|
+
const removedRoots = attestations.map((entry) => join7(ctx.repoRoot, entry.name));
|
|
10009
10254
|
if (!ctx.dryRun) for (const path of removedRoots) rmSync2(path, { recursive: true, force: true });
|
|
10010
10255
|
const changedFiles = [.../* @__PURE__ */ new Set([...projectionResult.changedFiles, ...gitignoreChanges, ...removedRoots])].sort();
|
|
10011
10256
|
return {
|
|
@@ -10064,11 +10309,11 @@ function linkedPlaneBoard(ctx) {
|
|
|
10064
10309
|
return { boardId, workspace };
|
|
10065
10310
|
}
|
|
10066
10311
|
function runPx(args, ctx) {
|
|
10067
|
-
const probe =
|
|
10312
|
+
const probe = spawnSync4("which", ["px"], { encoding: "utf8" });
|
|
10068
10313
|
if (probe.status !== 0) {
|
|
10069
10314
|
return { error: "px not found on PATH \u2014 install Pilot to manage board schemas" };
|
|
10070
10315
|
}
|
|
10071
|
-
const result2 =
|
|
10316
|
+
const result2 = spawnSync4("px", args, {
|
|
10072
10317
|
cwd: ctx.repoRoot,
|
|
10073
10318
|
encoding: "utf8",
|
|
10074
10319
|
shell: false,
|
|
@@ -10248,7 +10493,7 @@ var AgentHooksRecipe = class extends Recipe {
|
|
|
10248
10493
|
printNextSteps() {
|
|
10249
10494
|
console.log("\u{1FA9D} Agent-hooks layer installed!");
|
|
10250
10495
|
console.log(" Next steps:");
|
|
10251
|
-
console.log(" 1.
|
|
10496
|
+
console.log(" 1. pj skills sync --scope project --project . # sync .agents/skills.json into local CLI dirs");
|
|
10252
10497
|
console.log(" 2. mise run hooks:sync # generate .claude/settings.json + inject codex/kimi/hermes");
|
|
10253
10498
|
console.log(" 3. git add .claude/settings.json .agents/hooks .agents/skills.json && commit (codex/kimi/hermes are per-dev)");
|
|
10254
10499
|
console.log(" 4. mise run hindsight:setup # set HINDSIGHT_OP_KEY_REF to your 1Password item first");
|
|
@@ -10258,11 +10503,12 @@ var AgentHooksRecipe = class extends Recipe {
|
|
|
10258
10503
|
|
|
10259
10504
|
// src/recipes/BmadRecipe.ts
|
|
10260
10505
|
var BmadRecipe = class extends Recipe {
|
|
10261
|
-
|
|
10506
|
+
requiredChecks = createBmadChecks();
|
|
10507
|
+
checks = [...this.requiredChecks, createG33CompanionCheck()];
|
|
10262
10508
|
metadata = {
|
|
10263
10509
|
id: "bmad",
|
|
10264
10510
|
name: "bmad",
|
|
10265
|
-
description: "BMAD methodology and the
|
|
10511
|
+
description: "BMAD methodology, supported CLI projections, and the optional owner-provided g33 companion",
|
|
10266
10512
|
dependencies: ["agent-hooks"],
|
|
10267
10513
|
commands: [],
|
|
10268
10514
|
publicRuleIds: this.checks.map((check) => check.id)
|
|
@@ -10270,8 +10516,31 @@ var BmadRecipe = class extends Recipe {
|
|
|
10270
10516
|
constructor(context) {
|
|
10271
10517
|
super(context);
|
|
10272
10518
|
}
|
|
10273
|
-
init(ctx, _input) {
|
|
10274
|
-
|
|
10519
|
+
async init(ctx, _input) {
|
|
10520
|
+
const companions = [];
|
|
10521
|
+
const checks = this.requiredChecks.map((check) => ({
|
|
10522
|
+
...check,
|
|
10523
|
+
migrate: async (...args) => {
|
|
10524
|
+
const result2 = await check.migrate(...args);
|
|
10525
|
+
if (result2.bmadCompanion) companions.push(result2.bmadCompanion);
|
|
10526
|
+
return result2;
|
|
10527
|
+
}
|
|
10528
|
+
}));
|
|
10529
|
+
const required = await this.initializeOwnedChecks(ctx, checks);
|
|
10530
|
+
if (!required.ok) return { ...required, ...companions.length ? { bmadCompanion: companions.at(-1) } : {} };
|
|
10531
|
+
const companion = companions.at(-1) ?? await runG33Companion(ctx.repoRoot, ctx.dryRun);
|
|
10532
|
+
return {
|
|
10533
|
+
...required,
|
|
10534
|
+
bmadCompanion: companion,
|
|
10535
|
+
changedFiles: [.../* @__PURE__ */ new Set([...required.changedFiles, ...companion.changedFiles])].sort(),
|
|
10536
|
+
logs: [...required.logs, ...companionDetails(companion)],
|
|
10537
|
+
phases: [...required.phases, {
|
|
10538
|
+
id: "bmad.g33-companion",
|
|
10539
|
+
status: companion.status === "planned" ? "planned" : companion.status === "changed" ? "changed" : companion.status === "unchanged" ? "unchanged" : "skipped",
|
|
10540
|
+
message: `Optional g33 (${companion.status}): ${companion.summary}`,
|
|
10541
|
+
changedFiles: companion.changedFiles
|
|
10542
|
+
}]
|
|
10543
|
+
};
|
|
10275
10544
|
}
|
|
10276
10545
|
printNextSteps() {
|
|
10277
10546
|
console.log(`BMAD lifecycle initialized for the ${SUPPORTED_CLIS.length} supported CLIs.`);
|
|
@@ -10595,24 +10864,24 @@ var NodeRecipe = class extends Recipe {
|
|
|
10595
10864
|
|
|
10596
10865
|
// src/recipes/ProjectRecipe.ts
|
|
10597
10866
|
init_project();
|
|
10598
|
-
import { spawnSync as
|
|
10599
|
-
import { existsSync as existsSync14, lstatSync as
|
|
10600
|
-
import { dirname as dirname14, isAbsolute as
|
|
10867
|
+
import { spawnSync as spawnSync12 } from "node:child_process";
|
|
10868
|
+
import { existsSync as existsSync14, lstatSync as lstatSync13, readFileSync as readFileSync15, rmSync as rmSync5 } from "node:fs";
|
|
10869
|
+
import { dirname as dirname14, isAbsolute as isAbsolute9, join as join19, relative as relativePath, resolve as resolve18 } from "node:path";
|
|
10601
10870
|
init_tree_diff();
|
|
10602
10871
|
|
|
10603
10872
|
// src/recipes/NotebookRecipe.ts
|
|
10604
10873
|
init_project();
|
|
10605
|
-
import { existsSync as existsSync13, readFileSync as
|
|
10606
|
-
import { join as
|
|
10874
|
+
import { existsSync as existsSync13, readFileSync as readFileSync14 } from "node:fs";
|
|
10875
|
+
import { join as join18 } from "node:path";
|
|
10607
10876
|
|
|
10608
10877
|
// src/notebook/checks.ts
|
|
10609
10878
|
init_config();
|
|
10610
10879
|
init_notes();
|
|
10611
10880
|
init_output();
|
|
10612
10881
|
init_state();
|
|
10613
|
-
import { spawnSync as
|
|
10614
|
-
import { existsSync as existsSync10, readFileSync as
|
|
10615
|
-
import { join as
|
|
10882
|
+
import { spawnSync as spawnSync6 } from "node:child_process";
|
|
10883
|
+
import { existsSync as existsSync10, readFileSync as readFileSync12 } from "node:fs";
|
|
10884
|
+
import { join as join14 } from "node:path";
|
|
10616
10885
|
var NOTEBOOK_RULE_IDS = [
|
|
10617
10886
|
"notebook.configuration",
|
|
10618
10887
|
"notebook.binding",
|
|
@@ -10629,10 +10898,10 @@ function result(check, status, summary, changedFiles = [], details = []) {
|
|
|
10629
10898
|
return { id: check.id, title: check.title, status, summary, changedFiles, details };
|
|
10630
10899
|
}
|
|
10631
10900
|
function manifestNotebook(repo) {
|
|
10632
|
-
const path =
|
|
10901
|
+
const path = join14(repo, ".project.json");
|
|
10633
10902
|
if (!existsSync10(path)) return null;
|
|
10634
10903
|
try {
|
|
10635
|
-
const parsed = JSON.parse(
|
|
10904
|
+
const parsed = JSON.parse(readFileSync12(path, "utf8"));
|
|
10636
10905
|
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return null;
|
|
10637
10906
|
const notebook = parsed.notebook;
|
|
10638
10907
|
return notebook && typeof notebook === "object" && !Array.isArray(notebook) ? notebook : null;
|
|
@@ -10861,10 +11130,10 @@ function captureAudit(ctx) {
|
|
|
10861
11130
|
function overviewMigrate(ctx, current) {
|
|
10862
11131
|
if (ctx.dryRun) return result(current, "applied", "Would reconcile drifted Project Overview note");
|
|
10863
11132
|
if (!ctx.live) return result(current, "blocked", "Reconciling the remote Overview note requires --live");
|
|
10864
|
-
const pjanglerEntry = ctx.pjanglerRoot ?
|
|
11133
|
+
const pjanglerEntry = ctx.pjanglerRoot ? join14(ctx.pjanglerRoot, "dist", "index.js") : null;
|
|
10865
11134
|
const entry = pjanglerEntry && existsSync10(pjanglerEntry) ? pjanglerEntry : "pj";
|
|
10866
11135
|
const cmd = entry === "pj" ? ["pj"] : [process.execPath, entry];
|
|
10867
|
-
const spawned =
|
|
11136
|
+
const spawned = spawnSync6(cmd[0], [...cmd.slice(1), "notebook", "overview", ctx.repoRoot], {
|
|
10868
11137
|
encoding: "utf8",
|
|
10869
11138
|
timeout: 1e4,
|
|
10870
11139
|
env: { ...process.env, ...ctx.registryPath ? { PJ_PROJECT_REGISTRY: ctx.registryPath } : {} }
|
|
@@ -10909,7 +11178,7 @@ init_project();
|
|
|
10909
11178
|
init_config();
|
|
10910
11179
|
init_open_notebook_client();
|
|
10911
11180
|
init_notes();
|
|
10912
|
-
import { resolve as
|
|
11181
|
+
import { resolve as resolve17 } from "node:path";
|
|
10913
11182
|
|
|
10914
11183
|
// src/notebook/journal-key.ts
|
|
10915
11184
|
init_types();
|
|
@@ -10943,18 +11212,18 @@ function journalNoteOperationId(period, key) {
|
|
|
10943
11212
|
init_notes();
|
|
10944
11213
|
init_git_evidence();
|
|
10945
11214
|
init_types();
|
|
10946
|
-
import { spawnSync as
|
|
11215
|
+
import { spawnSync as spawnSync8 } from "node:child_process";
|
|
10947
11216
|
import { realpathSync as realpathSync8 } from "node:fs";
|
|
10948
|
-
import { relative as relative10, resolve as
|
|
11217
|
+
import { relative as relative10, resolve as resolve14, sep as sep5 } from "node:path";
|
|
10949
11218
|
var DEFAULT_OVERVIEW_REFERENCES = [".project.json", "README.md", "AGENTS.md", "CLAUDE.md", "docs/architecture.md"];
|
|
10950
11219
|
function git2(repo, args, timeout) {
|
|
10951
|
-
const result2 =
|
|
11220
|
+
const result2 = spawnSync8("git", args, { cwd: repo, encoding: "utf8", maxBuffer: 1024 * 1024, timeout, shell: false });
|
|
10952
11221
|
return { ok: result2.status === 0, stdout: result2.stdout?.trim() ?? "" };
|
|
10953
11222
|
}
|
|
10954
11223
|
function normalizedReference(repo, value) {
|
|
10955
11224
|
if (!value || value.includes("\0") || value.startsWith("/") || value.split(/[\\/]/u).includes("..")) throw new NotebookError("INVALID_INPUT", `Overview reference is not a contained relative path: ${value}`);
|
|
10956
11225
|
const root = realpathSync8(repo);
|
|
10957
|
-
const candidate =
|
|
11226
|
+
const candidate = resolve14(root, value);
|
|
10958
11227
|
const rel = relative10(root, candidate).split(sep5).join("/");
|
|
10959
11228
|
if (!rel || rel === ".." || rel.startsWith("../")) throw new NotebookError("INVALID_INPUT", `Overview reference escapes the repository: ${value}`);
|
|
10960
11229
|
return rel.normalize("NFC");
|
|
@@ -11087,21 +11356,21 @@ init_git_evidence();
|
|
|
11087
11356
|
init_notes();
|
|
11088
11357
|
init_state();
|
|
11089
11358
|
init_types();
|
|
11090
|
-
import { spawn, spawnSync as
|
|
11359
|
+
import { spawn, spawnSync as spawnSync9 } from "node:child_process";
|
|
11091
11360
|
import { createHash as createHash8, randomUUID as randomUUID5 } from "node:crypto";
|
|
11092
|
-
import { chmodSync as chmodSync4, closeSync as closeSync5, constants as constants4, copyFileSync as copyFileSync3, existsSync as existsSync11, fstatSync as fstatSync3, fsyncSync as fsyncSync4, lstatSync as
|
|
11093
|
-
import { basename as basename8, dirname as dirname13, isAbsolute as
|
|
11361
|
+
import { chmodSync as chmodSync4, closeSync as closeSync5, constants as constants4, copyFileSync as copyFileSync3, existsSync as existsSync11, fstatSync as fstatSync3, fsyncSync as fsyncSync4, lstatSync as lstatSync12, mkdirSync as mkdirSync6, openSync as openSync5, readFileSync as readFileSync13, readSync as readSync3, readdirSync as readdirSync7, realpathSync as realpathSync9, renameSync as renameSync6, rmSync as rmSync4, symlinkSync as symlinkSync3, unlinkSync as unlinkSync6, writeFileSync as writeFileSync7 } from "node:fs";
|
|
11362
|
+
import { basename as basename8, dirname as dirname13, isAbsolute as isAbsolute7, join as join17, parse as parse3, relative as relative11, resolve as resolve15, sep as sep6 } from "node:path";
|
|
11094
11363
|
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
|
11095
11364
|
function bundledSkillCandidates() {
|
|
11096
11365
|
const candidates = [];
|
|
11097
11366
|
let cursor = dirname13(fileURLToPath3(import.meta.url));
|
|
11098
11367
|
for (let depth = 0; depth < 8; depth++) {
|
|
11099
|
-
candidates.push(
|
|
11368
|
+
candidates.push(join17(cursor, "dist", "assets", "project-notebook-skill"), join17(cursor, "assets", "project-notebook-skill"));
|
|
11100
11369
|
const parent = dirname13(cursor);
|
|
11101
11370
|
if (parent === cursor) break;
|
|
11102
11371
|
cursor = parent;
|
|
11103
11372
|
}
|
|
11104
|
-
return [...new Set(candidates.map((candidate) =>
|
|
11373
|
+
return [...new Set(candidates.map((candidate) => resolve15(candidate)))];
|
|
11105
11374
|
}
|
|
11106
11375
|
function safeSkillRelativePath(value) {
|
|
11107
11376
|
if (!value || value.startsWith("/") || value.includes("\0")) return false;
|
|
@@ -11114,14 +11383,14 @@ function assertOwnedSkillTree(source) {
|
|
|
11114
11383
|
assertNoSymlinkComponents(source);
|
|
11115
11384
|
const uid = typeof process.getuid === "function" ? process.getuid() : void 0;
|
|
11116
11385
|
const walk = (directory) => {
|
|
11117
|
-
const directoryStat =
|
|
11386
|
+
const directoryStat = lstatSync12(directory);
|
|
11118
11387
|
if (!directoryStat.isDirectory() || directoryStat.isSymbolicLink()) throw new NotebookError("CONFLICT", "Project Notebook skill source contains a non-directory component");
|
|
11119
11388
|
if (uid !== void 0 && directoryStat.uid !== uid) throw new NotebookError("CONFLICT", "Project Notebook skill source is not owned by the current user");
|
|
11120
11389
|
if (directoryStat.mode & 3586) throw new NotebookError("CONFLICT", "Project Notebook skill source has unsafe directory mode bits");
|
|
11121
11390
|
for (const entry of readdirSync7(directory, { withFileTypes: true })) {
|
|
11122
11391
|
if (entry.isDirectory() && (entry.name === "__pycache__" || entry.name === ".git" || entry.name === "node_modules")) continue;
|
|
11123
|
-
const path =
|
|
11124
|
-
const stat =
|
|
11392
|
+
const path = join17(directory, entry.name);
|
|
11393
|
+
const stat = lstatSync12(path);
|
|
11125
11394
|
if (stat.isSymbolicLink()) throw new NotebookError("CONFLICT", "Project Notebook skill source contains a symlink");
|
|
11126
11395
|
if (entry.isDirectory()) walk(path);
|
|
11127
11396
|
else if (!entry.isFile()) throw new NotebookError("CONFLICT", "Project Notebook skill source contains a non-regular entry");
|
|
@@ -11138,16 +11407,16 @@ function enumerateSkillPayload(source) {
|
|
|
11138
11407
|
const walk = (directory) => {
|
|
11139
11408
|
for (const entry of readdirSync7(directory, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name, "en"))) {
|
|
11140
11409
|
if (entry.isDirectory() && (entry.name === "__pycache__" || entry.name === ".git" || entry.name === "node_modules")) continue;
|
|
11141
|
-
const path =
|
|
11410
|
+
const path = join17(directory, entry.name);
|
|
11142
11411
|
const rel = relative11(source, path).split(sep6).join("/");
|
|
11143
11412
|
if (!rel.includes("/") && (rel === "export-manifest.json" || rel === "SHA256SUMS" || rel === ".source.yaml")) continue;
|
|
11144
11413
|
if (!safeSkillRelativePath(rel)) throw new NotebookError("CONFLICT", `Project Notebook skill export path is unsafe: ${rel}`);
|
|
11145
|
-
const stat =
|
|
11414
|
+
const stat = lstatSync12(path);
|
|
11146
11415
|
if (stat.isSymbolicLink()) throw new NotebookError("CONFLICT", "Project Notebook skill export contains a symlink");
|
|
11147
11416
|
if (entry.isDirectory()) walk(path);
|
|
11148
11417
|
else if (entry.isFile()) {
|
|
11149
11418
|
const expectedMode = rel.endsWith(".sh") || rel.startsWith("scripts/") ? "0755" : "0644";
|
|
11150
|
-
result2.push({ path: rel, sha256: createHash8("sha256").update(
|
|
11419
|
+
result2.push({ path: rel, sha256: createHash8("sha256").update(readFileSync13(path)).digest("hex"), mode: expectedMode });
|
|
11151
11420
|
} else throw new NotebookError("CONFLICT", "Project Notebook skill export contains a non-regular entry");
|
|
11152
11421
|
}
|
|
11153
11422
|
};
|
|
@@ -11155,9 +11424,9 @@ function enumerateSkillPayload(source) {
|
|
|
11155
11424
|
return result2.sort((a, b) => a.path.localeCompare(b.path, "en"));
|
|
11156
11425
|
}
|
|
11157
11426
|
function parsePackedManifest(source) {
|
|
11158
|
-
const manifestPath =
|
|
11427
|
+
const manifestPath = join17(source, "export-manifest.json");
|
|
11159
11428
|
if (!existsSync11(manifestPath)) return null;
|
|
11160
|
-
const value = JSON.parse(
|
|
11429
|
+
const value = JSON.parse(readFileSync13(manifestPath, "utf8"));
|
|
11161
11430
|
if (!value || typeof value !== "object" || Array.isArray(value)) throw new NotebookError("CONFLICT", "Project Notebook skill export manifest is invalid");
|
|
11162
11431
|
const manifest = value;
|
|
11163
11432
|
if (manifest.schema_version !== 1 || manifest.skill !== "project-notebook" || !Array.isArray(manifest.files)) throw new NotebookError("CONFLICT", "Project Notebook skill export manifest is incompatible");
|
|
@@ -11166,10 +11435,10 @@ function parsePackedManifest(source) {
|
|
|
11166
11435
|
if (!entry || typeof entry.path !== "string" || !/^[a-f0-9]{64}$/u.test(entry.sha256) || !/^(?:0644|0755)$/u.test(entry.mode)) throw new NotebookError("CONFLICT", "Project Notebook skill export entry is invalid");
|
|
11167
11436
|
if (!safeSkillRelativePath(entry.path) || paths.has(entry.path)) throw new NotebookError("CONFLICT", "Project Notebook skill export path is unsafe or duplicated");
|
|
11168
11437
|
paths.add(entry.path);
|
|
11169
|
-
const path =
|
|
11170
|
-
const stat =
|
|
11438
|
+
const path = join17(source, ...entry.path.split("/"));
|
|
11439
|
+
const stat = lstatSync12(path);
|
|
11171
11440
|
if (!stat.isFile() || stat.isSymbolicLink()) throw new NotebookError("CONFLICT", "Project Notebook skill export contains a non-regular entry");
|
|
11172
|
-
const actual = createHash8("sha256").update(
|
|
11441
|
+
const actual = createHash8("sha256").update(readFileSync13(path)).digest("hex");
|
|
11173
11442
|
if (actual !== entry.sha256) throw new NotebookError("CONFLICT", `Project Notebook skill digest mismatch: ${entry.path}`);
|
|
11174
11443
|
const actualMode = stat.mode & 511;
|
|
11175
11444
|
const executable = entry.mode === "0755";
|
|
@@ -11181,12 +11450,12 @@ function parsePackedManifest(source) {
|
|
|
11181
11450
|
if (JSON.stringify(actualPaths) !== JSON.stringify(manifest.files.map((entry) => entry.path))) throw new NotebookError("CONFLICT", "Project Notebook skill manifest does not exactly enumerate its payload");
|
|
11182
11451
|
const sums = `${manifest.files.map((entry) => `${entry.sha256} ${entry.path}`).join("\n")}
|
|
11183
11452
|
`;
|
|
11184
|
-
if (!existsSync11(
|
|
11453
|
+
if (!existsSync11(join17(source, "SHA256SUMS")) || readFileSync13(join17(source, "SHA256SUMS"), "utf8") !== sums) throw new NotebookError("CONFLICT", "Project Notebook skill SHA256SUMS is missing or stale");
|
|
11185
11454
|
return manifest;
|
|
11186
11455
|
}
|
|
11187
11456
|
function expectedPackedSkill() {
|
|
11188
11457
|
for (const candidate of bundledSkillCandidates()) {
|
|
11189
|
-
if (!existsSync11(
|
|
11458
|
+
if (!existsSync11(join17(candidate, "export-manifest.json"))) continue;
|
|
11190
11459
|
assertOwnedSkillTree(candidate);
|
|
11191
11460
|
const manifest = parsePackedManifest(candidate);
|
|
11192
11461
|
if (!manifest) continue;
|
|
@@ -11196,7 +11465,7 @@ function expectedPackedSkill() {
|
|
|
11196
11465
|
throw new NotebookError("NOT_CONFIGURED", "PJ\xE1ngler package has no verified Project Notebook skill export");
|
|
11197
11466
|
}
|
|
11198
11467
|
function isCanonicalSkillexProjectionPath(path) {
|
|
11199
|
-
return
|
|
11468
|
+
return resolve15(path).split(sep6).join("/").endsWith("/all-skills/project-notebook");
|
|
11200
11469
|
}
|
|
11201
11470
|
function isVerifiedCanonicalSkillexProjection(path) {
|
|
11202
11471
|
if (!isCanonicalSkillexProjectionPath(path)) return false;
|
|
@@ -11216,7 +11485,7 @@ function describeProjectNotebookSkillDrift(source) {
|
|
|
11216
11485
|
}
|
|
11217
11486
|
let actual;
|
|
11218
11487
|
try {
|
|
11219
|
-
const absolute =
|
|
11488
|
+
const absolute = resolve15(source);
|
|
11220
11489
|
const packed = parsePackedManifest(absolute);
|
|
11221
11490
|
actual = packed?.files ?? enumerateSkillPayload(absolute);
|
|
11222
11491
|
} catch (error) {
|
|
@@ -11240,7 +11509,7 @@ function describeProjectNotebookSkillDrift(source) {
|
|
|
11240
11509
|
return drift.length ? drift.slice(0, 20) : ["no per-file drift detected; the export enumeration itself differs"];
|
|
11241
11510
|
}
|
|
11242
11511
|
function verifyProjectNotebookSkillExport(source) {
|
|
11243
|
-
const absolute =
|
|
11512
|
+
const absolute = resolve15(source);
|
|
11244
11513
|
assertOwnedSkillTree(absolute);
|
|
11245
11514
|
const packed = parsePackedManifest(absolute);
|
|
11246
11515
|
const actual = packed ?? { schema_version: 1, skill: "project-notebook", files: enumerateSkillPayload(absolute) };
|
|
@@ -11252,14 +11521,14 @@ function verifyProjectNotebookSkillExport(source) {
|
|
|
11252
11521
|
}
|
|
11253
11522
|
function resolveProjectNotebookSkillSource(env2 = process.env) {
|
|
11254
11523
|
if (env2.PJ_PROJECT_NOTEBOOK_SKILL_ROOT) {
|
|
11255
|
-
const explicit =
|
|
11256
|
-
if (!existsSync11(
|
|
11524
|
+
const explicit = resolve15(env2.PJ_PROJECT_NOTEBOOK_SKILL_ROOT);
|
|
11525
|
+
if (!existsSync11(join17(explicit, "SKILL.md"))) throw new NotebookError("NOT_CONFIGURED", "Configured Project Notebook skill source is unavailable");
|
|
11257
11526
|
verifyProjectNotebookSkillExport(explicit);
|
|
11258
11527
|
return explicit;
|
|
11259
11528
|
}
|
|
11260
11529
|
if (env2.PJ_SKILLS_REGISTRY_ROOT) {
|
|
11261
|
-
const canonical =
|
|
11262
|
-
if (existsSync11(
|
|
11530
|
+
const canonical = resolve15(env2.PJ_SKILLS_REGISTRY_ROOT, "all-skills", "project-notebook");
|
|
11531
|
+
if (existsSync11(join17(canonical, "SKILL.md"))) {
|
|
11263
11532
|
verifyProjectNotebookSkillExport(canonical);
|
|
11264
11533
|
return canonical;
|
|
11265
11534
|
}
|
|
@@ -11274,7 +11543,7 @@ var REPAIR_COMMAND = "pj notebook skill --apply";
|
|
|
11274
11543
|
function probeCanonicalSkillexRootProjection(skillsRoot, link) {
|
|
11275
11544
|
let rootLink;
|
|
11276
11545
|
try {
|
|
11277
|
-
rootLink =
|
|
11546
|
+
rootLink = lstatSync12(skillsRoot);
|
|
11278
11547
|
} catch (error) {
|
|
11279
11548
|
if (error.code === "ENOENT") return { state: "absent" };
|
|
11280
11549
|
throw error;
|
|
@@ -11287,15 +11556,15 @@ function probeCanonicalSkillexRootProjection(skillsRoot, link) {
|
|
|
11287
11556
|
if (uid !== void 0 && rootLink.uid !== uid) throw new Error("owner");
|
|
11288
11557
|
const globalRoot = realpathSync9(skillsRoot);
|
|
11289
11558
|
assertNoSymlinkComponents(globalRoot);
|
|
11290
|
-
const globalStat =
|
|
11559
|
+
const globalStat = lstatSync12(globalRoot);
|
|
11291
11560
|
if (!globalStat.isDirectory() || globalStat.isSymbolicLink()) throw new Error("root-type");
|
|
11292
11561
|
if (uid !== void 0 && globalStat.uid !== uid) throw new Error("root-owner");
|
|
11293
11562
|
if (globalStat.mode & 3586) throw new Error("root-mode");
|
|
11294
11563
|
const skillSetsRoot = dirname13(globalRoot);
|
|
11295
11564
|
if (basename8(globalRoot) !== "global" || basename8(skillSetsRoot) !== "skill-sets") throw new Error("layout");
|
|
11296
11565
|
const checkoutRoot = dirname13(skillSetsRoot);
|
|
11297
|
-
const expectedSource =
|
|
11298
|
-
const linkStat =
|
|
11566
|
+
const expectedSource = join17(checkoutRoot, "all-skills", "project-notebook");
|
|
11567
|
+
const linkStat = lstatSync12(link);
|
|
11299
11568
|
if (!linkStat.isSymbolicLink()) throw new Error("projection-type");
|
|
11300
11569
|
if (uid !== void 0 && linkStat.uid !== uid) throw new Error("projection-owner");
|
|
11301
11570
|
const projectedSource = realpathSync9(link);
|
|
@@ -11331,28 +11600,28 @@ function installPackagedProjectNotebookSkill(input = {}) {
|
|
|
11331
11600
|
const manifest = verifyProjectNotebookSkillExport(source);
|
|
11332
11601
|
const digest = createHash8("sha256").update(JSON.stringify(manifest)).digest("hex");
|
|
11333
11602
|
const home = env2.HOME;
|
|
11334
|
-
if (!home || !
|
|
11335
|
-
const skillsRoot =
|
|
11336
|
-
const link =
|
|
11603
|
+
if (!home || !resolve15(home).startsWith("/")) throw new NotebookError("NOT_CONFIGURED", "A trusted HOME is required to install the Project Notebook skill");
|
|
11604
|
+
const skillsRoot = join17(home, ".agents", "skills");
|
|
11605
|
+
const link = join17(skillsRoot, "project-notebook");
|
|
11337
11606
|
const probe = probeCanonicalSkillexRootProjection(skillsRoot, link);
|
|
11338
11607
|
if (probe.state === "adopted") return { installed: false, path: link, digest };
|
|
11339
11608
|
if (probe.state === "declined") return { installed: false, path: link, digest, blocked: probe.block };
|
|
11340
|
-
const dataRoot =
|
|
11341
|
-
const payload =
|
|
11609
|
+
const dataRoot = resolve15(env2.XDG_DATA_HOME || join17(home, ".local", "share"), "pjangler", "skills", "project-notebook");
|
|
11610
|
+
const payload = join17(dataRoot, `${PJANGLER_VERSION}-${digest}`);
|
|
11342
11611
|
assertNoSymlinkComponents(dirname13(dataRoot), true);
|
|
11343
11612
|
mkdirSync6(dataRoot, { recursive: true, mode: 448 });
|
|
11344
11613
|
assertNoSymlinkComponents(dataRoot);
|
|
11345
|
-
const dataStat =
|
|
11614
|
+
const dataStat = lstatSync12(dataRoot);
|
|
11346
11615
|
if (!dataStat.isDirectory() || dataStat.isSymbolicLink() || typeof process.getuid === "function" && dataStat.uid !== process.getuid()) throw new NotebookError("CONFLICT", "Project Notebook skill data root is not a current-user directory");
|
|
11347
11616
|
chmodSync4(dataRoot, 448);
|
|
11348
11617
|
if (!existsSync11(payload)) {
|
|
11349
|
-
const staging =
|
|
11618
|
+
const staging = join17(dataRoot, `.staging-${randomUUID5()}`);
|
|
11350
11619
|
mkdirSync6(staging, { recursive: false, mode: 448 });
|
|
11351
11620
|
try {
|
|
11352
11621
|
for (const entry of manifest.files) {
|
|
11353
|
-
const destination =
|
|
11622
|
+
const destination = join17(staging, ...entry.path.split("/"));
|
|
11354
11623
|
mkdirSync6(dirname13(destination), { recursive: true, mode: 493 });
|
|
11355
|
-
copyFileSync3(
|
|
11624
|
+
copyFileSync3(join17(source, ...entry.path.split("/")), destination);
|
|
11356
11625
|
const mode = entry.mode === "0755" ? 493 : 420;
|
|
11357
11626
|
chmodSync4(destination, mode);
|
|
11358
11627
|
const fd = openSync5(destination, constants4.O_RDONLY | (constants4.O_NOFOLLOW ?? 0));
|
|
@@ -11364,9 +11633,9 @@ function installPackagedProjectNotebookSkill(input = {}) {
|
|
|
11364
11633
|
closeSync5(fd);
|
|
11365
11634
|
}
|
|
11366
11635
|
}
|
|
11367
|
-
writeFileSync7(
|
|
11636
|
+
writeFileSync7(join17(staging, "export-manifest.json"), `${JSON.stringify(manifest, null, 2)}
|
|
11368
11637
|
`, { mode: 420, flag: "wx" });
|
|
11369
|
-
writeFileSync7(
|
|
11638
|
+
writeFileSync7(join17(staging, "SHA256SUMS"), `${manifest.files.map((entry) => `${entry.sha256} ${entry.path}`).join("\n")}
|
|
11370
11639
|
`, { mode: 420, flag: "wx" });
|
|
11371
11640
|
verifyProjectNotebookSkillExport(staging);
|
|
11372
11641
|
renameSync6(staging, payload);
|
|
@@ -11374,7 +11643,7 @@ function installPackagedProjectNotebookSkill(input = {}) {
|
|
|
11374
11643
|
if (existsSync11(staging)) rmSync4(staging, { recursive: true, force: true });
|
|
11375
11644
|
}
|
|
11376
11645
|
} else {
|
|
11377
|
-
const stat =
|
|
11646
|
+
const stat = lstatSync12(payload);
|
|
11378
11647
|
if (!stat.isDirectory() || stat.isSymbolicLink()) throw new NotebookError("CONFLICT", "Installed Project Notebook payload is not a real directory");
|
|
11379
11648
|
verifyProjectNotebookSkillExport(payload);
|
|
11380
11649
|
}
|
|
@@ -11384,7 +11653,7 @@ function installPackagedProjectNotebookSkill(input = {}) {
|
|
|
11384
11653
|
let linkExists = existsSync11(link);
|
|
11385
11654
|
if (!linkExists) {
|
|
11386
11655
|
try {
|
|
11387
|
-
|
|
11656
|
+
lstatSync12(link);
|
|
11388
11657
|
linkExists = true;
|
|
11389
11658
|
} catch {
|
|
11390
11659
|
}
|
|
@@ -11401,7 +11670,7 @@ function installPackagedProjectNotebookSkill(input = {}) {
|
|
|
11401
11670
|
repair: REPAIR_COMMAND
|
|
11402
11671
|
}
|
|
11403
11672
|
});
|
|
11404
|
-
const stat =
|
|
11673
|
+
const stat = lstatSync12(link);
|
|
11405
11674
|
if (!stat.isSymbolicLink()) return foreign(["the path is a real file or directory, not a PJ\xE1ngler-owned link"]);
|
|
11406
11675
|
const target = realpathSync9(link);
|
|
11407
11676
|
if (target !== realpathSync9(payload) && !isVerifiedCanonicalSkillexProjection(target)) {
|
|
@@ -11435,9 +11704,9 @@ function collectSupersededPayloads(dataRoot, keep) {
|
|
|
11435
11704
|
}
|
|
11436
11705
|
for (const name of entries) {
|
|
11437
11706
|
if (!/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?-[0-9a-f]{64}$/u.test(name)) continue;
|
|
11438
|
-
const candidate =
|
|
11707
|
+
const candidate = join17(dataRoot, name);
|
|
11439
11708
|
try {
|
|
11440
|
-
const stat =
|
|
11709
|
+
const stat = lstatSync12(candidate);
|
|
11441
11710
|
if (!stat.isDirectory() || stat.isSymbolicLink()) continue;
|
|
11442
11711
|
if (kept.has(realpathSync9(candidate))) continue;
|
|
11443
11712
|
rmSync4(candidate, { recursive: true, force: true });
|
|
@@ -11450,9 +11719,9 @@ function collectSupersededPayloads(dataRoot, keep) {
|
|
|
11450
11719
|
function repairProjectNotebookSkillProjection(input = {}) {
|
|
11451
11720
|
const env2 = input.env ?? process.env;
|
|
11452
11721
|
const home = env2.HOME;
|
|
11453
|
-
if (!home || !
|
|
11454
|
-
const skillsRoot =
|
|
11455
|
-
const link =
|
|
11722
|
+
if (!home || !resolve15(home).startsWith("/")) throw new NotebookError("NOT_CONFIGURED", "A trusted HOME is required to repair the Project Notebook skill projection");
|
|
11723
|
+
const skillsRoot = join17(home, ".agents", "skills");
|
|
11724
|
+
const link = join17(skillsRoot, "project-notebook");
|
|
11456
11725
|
const probe = probeCanonicalSkillexRootProjection(skillsRoot, link);
|
|
11457
11726
|
if (probe.state === "adopted") return { status: "clean", summary: "Canonical Skillex projection already matches the version-pinned export", source: probe.source, drift: [], changed_files: [] };
|
|
11458
11727
|
if (probe.state !== "declined") {
|
|
@@ -11466,7 +11735,7 @@ function repairProjectNotebookSkillProjection(input = {}) {
|
|
|
11466
11735
|
const present = [];
|
|
11467
11736
|
const walk = (directory) => {
|
|
11468
11737
|
for (const entry of readdirSync7(directory, { withFileTypes: true })) {
|
|
11469
|
-
const path =
|
|
11738
|
+
const path = join17(directory, entry.name);
|
|
11470
11739
|
const rel = relative11(source, path).split(sep6).join("/");
|
|
11471
11740
|
if (entry.isDirectory()) {
|
|
11472
11741
|
walk(path);
|
|
@@ -11480,11 +11749,11 @@ function repairProjectNotebookSkillProjection(input = {}) {
|
|
|
11480
11749
|
walk(source);
|
|
11481
11750
|
const stale = present.filter((rel) => !wanted.has(rel)).sort();
|
|
11482
11751
|
const changed = expected.manifest.files.filter((entry) => {
|
|
11483
|
-
const path =
|
|
11752
|
+
const path = join17(source, ...entry.path.split("/"));
|
|
11484
11753
|
if (!existsSync11(path)) return true;
|
|
11485
|
-
const stat =
|
|
11754
|
+
const stat = lstatSync12(path);
|
|
11486
11755
|
if (!stat.isFile()) return true;
|
|
11487
|
-
if (createHash8("sha256").update(
|
|
11756
|
+
if (createHash8("sha256").update(readFileSync13(path)).digest("hex") !== entry.sha256) return true;
|
|
11488
11757
|
const mode = stat.mode & 511;
|
|
11489
11758
|
const executable = entry.mode === "0755";
|
|
11490
11759
|
return executable && (mode & 64) === 0 || !executable && (mode & 73) !== 0;
|
|
@@ -11499,15 +11768,15 @@ function repairProjectNotebookSkillProjection(input = {}) {
|
|
|
11499
11768
|
summary: `Would restore ${affected.length} file(s) in ${source} from the version-pinned export`,
|
|
11500
11769
|
source,
|
|
11501
11770
|
drift: probe.block.details,
|
|
11502
|
-
changed_files: affected.map((rel) =>
|
|
11771
|
+
changed_files: affected.map((rel) => join17(source, ...rel.split("/")))
|
|
11503
11772
|
};
|
|
11504
11773
|
}
|
|
11505
|
-
for (const rel of stale) rmSync4(
|
|
11774
|
+
for (const rel of stale) rmSync4(join17(source, ...rel.split("/")), { force: true });
|
|
11506
11775
|
for (const rel of changed) {
|
|
11507
|
-
const destination =
|
|
11776
|
+
const destination = join17(source, ...rel.split("/"));
|
|
11508
11777
|
const entry = expected.manifest.files.find((item) => item.path === rel);
|
|
11509
11778
|
mkdirSync6(dirname13(destination), { recursive: true, mode: 493 });
|
|
11510
|
-
copyFileSync3(
|
|
11779
|
+
copyFileSync3(join17(expected.source, ...rel.split("/")), destination);
|
|
11511
11780
|
chmodSync4(destination, entry.mode === "0755" ? 493 : 420);
|
|
11512
11781
|
}
|
|
11513
11782
|
verifyProjectNotebookSkillExport(source);
|
|
@@ -11516,21 +11785,21 @@ function repairProjectNotebookSkillProjection(input = {}) {
|
|
|
11516
11785
|
summary: `Restored ${affected.length} file(s) in ${source} from the version-pinned export; commit the change in the Skillex checkout`,
|
|
11517
11786
|
source,
|
|
11518
11787
|
drift: probe.block.details,
|
|
11519
|
-
changed_files: affected.map((rel) =>
|
|
11788
|
+
changed_files: affected.map((rel) => join17(source, ...rel.split("/")))
|
|
11520
11789
|
};
|
|
11521
11790
|
}
|
|
11522
11791
|
function inspectProjectNotebookIntegration(env2 = process.env) {
|
|
11523
11792
|
const home = env2.HOME;
|
|
11524
11793
|
if (!home) return { skill_installed: false, hooks_projected: false, details: ["HOME is unavailable"] };
|
|
11525
|
-
const skillsRoot =
|
|
11526
|
-
const link =
|
|
11794
|
+
const skillsRoot = join17(home, ".agents", "skills");
|
|
11795
|
+
const link = join17(skillsRoot, "project-notebook");
|
|
11527
11796
|
try {
|
|
11528
11797
|
const expected = expectedPackedSkill();
|
|
11529
|
-
const dataRoot =
|
|
11530
|
-
const expectedPayload =
|
|
11798
|
+
const dataRoot = resolve15(env2.XDG_DATA_HOME || join17(home, ".local", "share"), "pjangler", "skills", "project-notebook");
|
|
11799
|
+
const expectedPayload = join17(dataRoot, `${PJANGLER_VERSION}-${expected.digest}`);
|
|
11531
11800
|
const probe = probeCanonicalSkillexRootProjection(skillsRoot, link);
|
|
11532
11801
|
if (probe.state === "declined") return { skill_installed: false, hooks_projected: false, details: probe.block.details, blocked: probe.block };
|
|
11533
|
-
const stat =
|
|
11802
|
+
const stat = lstatSync12(link);
|
|
11534
11803
|
if (!stat.isSymbolicLink()) {
|
|
11535
11804
|
return {
|
|
11536
11805
|
skill_installed: false,
|
|
@@ -11567,8 +11836,8 @@ function inspectProjectNotebookIntegration(env2 = process.env) {
|
|
|
11567
11836
|
}
|
|
11568
11837
|
}
|
|
11569
11838
|
function projectorArguments(source, command, input) {
|
|
11570
|
-
const script =
|
|
11571
|
-
const args = [script, command, "--master",
|
|
11839
|
+
const script = join17(source, "scripts", "project-hooks.py");
|
|
11840
|
+
const args = [script, command, "--master", join17(source, "hooks", "hooks.master.json"), "--fragment", join17(source, "hooks", "claude.settings.json"), "--target", input.target];
|
|
11572
11841
|
if (command === "check") args.push("--json");
|
|
11573
11842
|
else if (input.stateHome) args.push("--state-home", input.stateHome);
|
|
11574
11843
|
return args;
|
|
@@ -11590,8 +11859,8 @@ function checkProjectNotebookHooks(input = {}) {
|
|
|
11590
11859
|
verifyProjectNotebookSkillExport(source);
|
|
11591
11860
|
const home = env2.HOME;
|
|
11592
11861
|
if (!home) throw new NotebookError("NOT_CONFIGURED", "HOME is required to check Project Notebook hooks");
|
|
11593
|
-
const target =
|
|
11594
|
-
const result2 =
|
|
11862
|
+
const target = resolve15(input.target ?? env2.PJ_PROJECT_NOTEBOOK_CLAUDE_SETTINGS ?? join17(home, ".claude", "settings.json"));
|
|
11863
|
+
const result2 = spawnSync9("/usr/bin/python3", ["-I", ...projectorArguments(source, "check", { target })], { encoding: "utf8", env: projectorEnvironment(env2), timeout: 5e3, maxBuffer: 1048576 });
|
|
11595
11864
|
if (result2.status !== 0 && result2.status !== 1) throw new NotebookError("CONFLICT", (result2.stderr || "Project Notebook projector check failed").trim().slice(0, 512));
|
|
11596
11865
|
try {
|
|
11597
11866
|
const parsed = JSON.parse(result2.stdout);
|
|
@@ -11608,13 +11877,13 @@ function installProjectNotebookIntegration(input = {}) {
|
|
|
11608
11877
|
const source = realpathSync9(skill.path);
|
|
11609
11878
|
const home = env2.HOME;
|
|
11610
11879
|
if (!home) throw new NotebookError("NOT_CONFIGURED", "HOME is required to install Project Notebook hooks");
|
|
11611
|
-
const target =
|
|
11612
|
-
const stateHome =
|
|
11613
|
-
const result2 =
|
|
11880
|
+
const target = resolve15(input.target ?? env2.PJ_PROJECT_NOTEBOOK_CLAUDE_SETTINGS ?? join17(home, ".claude", "settings.json"));
|
|
11881
|
+
const stateHome = resolve15(env2.XDG_STATE_HOME || join17(home, ".local", "state"));
|
|
11882
|
+
const result2 = spawnSync9("/usr/bin/python3", ["-I", ...projectorArguments(source, "install", { target, stateHome })], { encoding: "utf8", env: projectorEnvironment(env2), timeout: 5e3, maxBuffer: 1048576 });
|
|
11614
11883
|
if (result2.status !== 0) {
|
|
11615
11884
|
if (skill.installed) {
|
|
11616
11885
|
try {
|
|
11617
|
-
const stat =
|
|
11886
|
+
const stat = lstatSync12(skill.path);
|
|
11618
11887
|
if (stat.isSymbolicLink() && realpathSync9(skill.path) === source) unlinkSync6(skill.path);
|
|
11619
11888
|
} catch {
|
|
11620
11889
|
}
|
|
@@ -11628,14 +11897,14 @@ function boundedDiagnostic(value, max = 512) {
|
|
|
11628
11897
|
return message.replace(/[\u0000-\u001f\u007f]/gu, " ").replace(/\s+/gu, " ").slice(0, max);
|
|
11629
11898
|
}
|
|
11630
11899
|
function assertNoSymlinkComponents(path, allowMissing = false) {
|
|
11631
|
-
const absolute =
|
|
11900
|
+
const absolute = resolve15(path);
|
|
11632
11901
|
const root = parse3(absolute).root;
|
|
11633
11902
|
let cursor = root;
|
|
11634
11903
|
for (const component of absolute.slice(root.length).split(sep6).filter(Boolean)) {
|
|
11635
|
-
cursor =
|
|
11904
|
+
cursor = join17(cursor, component);
|
|
11636
11905
|
let stat;
|
|
11637
11906
|
try {
|
|
11638
|
-
stat =
|
|
11907
|
+
stat = lstatSync12(cursor);
|
|
11639
11908
|
} catch (error) {
|
|
11640
11909
|
if (allowMissing && error.code === "ENOENT") return;
|
|
11641
11910
|
throw error;
|
|
@@ -11835,7 +12104,7 @@ var NotebookModule = class {
|
|
|
11835
12104
|
return {
|
|
11836
12105
|
changedFiles: [
|
|
11837
12106
|
...installed.skill.installed ? [installed.skill.path] : [],
|
|
11838
|
-
...installed.hooksChanged ? [
|
|
12107
|
+
...installed.hooksChanged ? [resolve17(env2.PJ_PROJECT_NOTEBOOK_CLAUDE_SETTINGS ?? `${home}/.claude/settings.json`)] : []
|
|
11839
12108
|
],
|
|
11840
12109
|
...installed.blocked ? { blocked: installed.blocked } : {}
|
|
11841
12110
|
};
|
|
@@ -12241,17 +12510,17 @@ init_remote_mutation_journal();
|
|
|
12241
12510
|
function integrationEnvironment(module, ctx) {
|
|
12242
12511
|
const env2 = { ...module.environment, HOME: ctx.homeDir };
|
|
12243
12512
|
if (module.environment.HOME !== ctx.homeDir) {
|
|
12244
|
-
env2.XDG_DATA_HOME =
|
|
12245
|
-
env2.XDG_STATE_HOME =
|
|
12246
|
-
env2.PJ_PROJECT_NOTEBOOK_CLAUDE_SETTINGS =
|
|
12513
|
+
env2.XDG_DATA_HOME = join18(ctx.homeDir, ".local", "share");
|
|
12514
|
+
env2.XDG_STATE_HOME = join18(ctx.homeDir, ".local", "state");
|
|
12515
|
+
env2.PJ_PROJECT_NOTEBOOK_CLAUDE_SETTINGS = join18(ctx.homeDir, ".claude", "settings.json");
|
|
12247
12516
|
}
|
|
12248
12517
|
return env2;
|
|
12249
12518
|
}
|
|
12250
12519
|
function resolvedForPlan(plan) {
|
|
12251
|
-
const manifestPath =
|
|
12520
|
+
const manifestPath = join18(plan.project.repo_path, ".project.json");
|
|
12252
12521
|
let manifest = plan.manifest;
|
|
12253
12522
|
if (existsSync13(manifestPath)) {
|
|
12254
|
-
const parsed = JSON.parse(
|
|
12523
|
+
const parsed = JSON.parse(readFileSync14(manifestPath, "utf8"));
|
|
12255
12524
|
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error(`${manifestPath} must contain a JSON object`);
|
|
12256
12525
|
manifest = parsed;
|
|
12257
12526
|
}
|
|
@@ -12392,7 +12661,7 @@ var PRODUCTION_RUNTIME = {
|
|
|
12392
12661
|
executePlan: executeProjectInitPlan,
|
|
12393
12662
|
preflightBmad: preflightBmadLifecycle,
|
|
12394
12663
|
runGit(cwd, args, options) {
|
|
12395
|
-
const result2 =
|
|
12664
|
+
const result2 = spawnSync12("git", [...args], {
|
|
12396
12665
|
cwd,
|
|
12397
12666
|
encoding: "utf8",
|
|
12398
12667
|
env: options?.env ? { ...process.env, ...options.env } : process.env
|
|
@@ -12406,13 +12675,13 @@ var PRODUCTION_RUNTIME = {
|
|
|
12406
12675
|
}
|
|
12407
12676
|
};
|
|
12408
12677
|
function unsafeToRemove(targetDir) {
|
|
12409
|
-
const absolute =
|
|
12678
|
+
const absolute = resolve18(targetDir);
|
|
12410
12679
|
let cursor = absolute;
|
|
12411
12680
|
const seen = /* @__PURE__ */ new Set();
|
|
12412
12681
|
while (!seen.has(cursor)) {
|
|
12413
12682
|
seen.add(cursor);
|
|
12414
12683
|
try {
|
|
12415
|
-
const stat =
|
|
12684
|
+
const stat = lstatSync13(cursor);
|
|
12416
12685
|
if (stat.isSymbolicLink()) {
|
|
12417
12686
|
return cursor === absolute ? `${absolute} is a symlink; removing it would leave the tree it points at orphaned` : `${cursor} is a symlink on the path to ${absolute}; a recursive remove would traverse it`;
|
|
12418
12687
|
}
|
|
@@ -12440,12 +12709,12 @@ function publicMigration(report) {
|
|
|
12440
12709
|
};
|
|
12441
12710
|
}
|
|
12442
12711
|
function hasGitRepository(runtime, targetDir) {
|
|
12443
|
-
if (!existsSync14(
|
|
12712
|
+
if (!existsSync14(join19(targetDir, ".git"))) return false;
|
|
12444
12713
|
return runtime.runGit(targetDir, ["rev-parse", "--is-inside-work-tree"]).status === 0;
|
|
12445
12714
|
}
|
|
12446
12715
|
function refreshPlanFromCanonicalManifest(plan) {
|
|
12447
|
-
const manifestPath =
|
|
12448
|
-
const manifest = JSON.parse(
|
|
12716
|
+
const manifestPath = join19(plan.project.repo_path, ".project.json");
|
|
12717
|
+
const manifest = JSON.parse(readFileSync15(manifestPath, "utf8"));
|
|
12449
12718
|
if (!manifest || typeof manifest !== "object" || Array.isArray(manifest)) {
|
|
12450
12719
|
throw new Error(`${manifestPath} must contain a JSON object`);
|
|
12451
12720
|
}
|
|
@@ -12537,6 +12806,8 @@ var ProjectRecipe = class extends Recipe {
|
|
|
12537
12806
|
repoRoot: targetDir,
|
|
12538
12807
|
bmadVersionPin: mode === "create" ? BMAD_INSTALLER_VERSION : ctx.bmadVersionPin
|
|
12539
12808
|
};
|
|
12809
|
+
let bmadCompanion;
|
|
12810
|
+
let g33DeferredForEnrollment = false;
|
|
12540
12811
|
let migrationReport;
|
|
12541
12812
|
let audit;
|
|
12542
12813
|
let notebookPlan;
|
|
@@ -12577,6 +12848,10 @@ var ProjectRecipe = class extends Recipe {
|
|
|
12577
12848
|
message: planBlocked ? "Project plan skipped after failed preflight" : executed.ok ? "Project plan executed" : executed.errors.join("; ")
|
|
12578
12849
|
});
|
|
12579
12850
|
if (errors.length === 0 && executed.ok && mode === "create") {
|
|
12851
|
+
try {
|
|
12852
|
+
g33DeferredForEnrollment = !g33CompanionEnabled(targetDir);
|
|
12853
|
+
} catch {
|
|
12854
|
+
}
|
|
12580
12855
|
const dependencyResult = await this.registry.initDependencies(this.metadata.id, transactionContext, normalized, ["notebook"]);
|
|
12581
12856
|
logs.push(...dependencyResult.logs);
|
|
12582
12857
|
errors.push(...dependencyResult.errors);
|
|
@@ -12657,7 +12932,7 @@ var ProjectRecipe = class extends Recipe {
|
|
|
12657
12932
|
if (hasGitRepository(this.runtime, targetDir)) {
|
|
12658
12933
|
phases.push({ id: "project.git", status: "unchanged", changedFiles: [], message: "Git repository already initialized" });
|
|
12659
12934
|
} else {
|
|
12660
|
-
const gitPath =
|
|
12935
|
+
const gitPath = join19(targetDir, ".git");
|
|
12661
12936
|
for (const { args, label, options } of [
|
|
12662
12937
|
{ args: ["init", "--initial-branch=main"], label: "git init" },
|
|
12663
12938
|
{ args: ["add", "-A"], label: "git add" },
|
|
@@ -12757,6 +13032,17 @@ var ProjectRecipe = class extends Recipe {
|
|
|
12757
13032
|
errors.push(`project manifest refresh after external effects failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
12758
13033
|
}
|
|
12759
13034
|
}
|
|
13035
|
+
if (errors.length === 0 && registryFinalizerEligible && mode === "create" && g33DeferredForEnrollment && plan.project.ticket_provider.state === "linked") {
|
|
13036
|
+
bmadCompanion = await runG33Companion(targetDir, ctx.dryRun);
|
|
13037
|
+
logs.push(...companionDetails(bmadCompanion));
|
|
13038
|
+
changedFiles.push(...bmadCompanion.changedFiles);
|
|
13039
|
+
phases.push({
|
|
13040
|
+
id: "project.optional:g33-companion",
|
|
13041
|
+
status: bmadCompanion.status === "changed" ? "changed" : bmadCompanion.status === "planned" ? "planned" : bmadCompanion.status === "unchanged" ? "unchanged" : "skipped",
|
|
13042
|
+
changedFiles: bmadCompanion.changedFiles,
|
|
13043
|
+
message: `Optional g33 (${bmadCompanion.status}): ${bmadCompanion.summary}`
|
|
13044
|
+
});
|
|
13045
|
+
}
|
|
12760
13046
|
if (registryFinalizerEligible && registryActions.length) {
|
|
12761
13047
|
rollbackEligible = false;
|
|
12762
13048
|
const registryPlan = { ...plan, actions: registryActions };
|
|
@@ -12810,8 +13096,8 @@ var ProjectRecipe = class extends Recipe {
|
|
|
12810
13096
|
} else try {
|
|
12811
13097
|
rmSync5(targetDir, { recursive: true, force: true });
|
|
12812
13098
|
const insideTarget = (path) => {
|
|
12813
|
-
const relative12 = relativePath(
|
|
12814
|
-
return relative12 === "" || !relative12.startsWith("..") && !
|
|
13099
|
+
const relative12 = relativePath(resolve18(targetDir), resolve18(path));
|
|
13100
|
+
return relative12 === "" || !relative12.startsWith("..") && !isAbsolute9(relative12);
|
|
12815
13101
|
};
|
|
12816
13102
|
const orphaned = [...new Set(changedFiles.filter((path) => !insideTarget(path)))].sort();
|
|
12817
13103
|
changedFiles.length = 0;
|
|
@@ -12847,7 +13133,8 @@ var ProjectRecipe = class extends Recipe {
|
|
|
12847
13133
|
audit,
|
|
12848
13134
|
selectedOperations: normalized.selectedOperations ?? [],
|
|
12849
13135
|
selectedParityRules: normalized.selectedRuleIds ?? [],
|
|
12850
|
-
migrationReport
|
|
13136
|
+
migrationReport,
|
|
13137
|
+
...bmadCompanion ? { bmadCompanion } : {}
|
|
12851
13138
|
};
|
|
12852
13139
|
}
|
|
12853
13140
|
printNextSteps() {
|
|
@@ -12855,12 +13142,12 @@ var ProjectRecipe = class extends Recipe {
|
|
|
12855
13142
|
};
|
|
12856
13143
|
|
|
12857
13144
|
// src/recipes/registry.ts
|
|
12858
|
-
import { readFileSync as
|
|
12859
|
-
import { join as
|
|
13145
|
+
import { readFileSync as readFileSync16 } from "node:fs";
|
|
13146
|
+
import { join as join20 } from "node:path";
|
|
12860
13147
|
function projectWaivers(repoRoot) {
|
|
12861
13148
|
let waive;
|
|
12862
13149
|
try {
|
|
12863
|
-
waive = JSON.parse(
|
|
13150
|
+
waive = JSON.parse(readFileSync16(join20(repoRoot, ".project.json"), "utf8"))?.parity?.waive;
|
|
12864
13151
|
} catch {
|
|
12865
13152
|
return /* @__PURE__ */ new Map();
|
|
12866
13153
|
}
|
|
@@ -13201,8 +13488,8 @@ var recipeRegistry = new RecipeRegistry([
|
|
|
13201
13488
|
|
|
13202
13489
|
// src/commands/AgentHooksCommands.ts
|
|
13203
13490
|
import { homedir as homedir6 } from "node:os";
|
|
13204
|
-
import { join as
|
|
13205
|
-
import { existsSync as existsSync15, cpSync, mkdirSync as mkdirSync7, readFileSync as
|
|
13491
|
+
import { join as join21, dirname as dirname15 } from "node:path";
|
|
13492
|
+
import { existsSync as existsSync15, cpSync, mkdirSync as mkdirSync7, readFileSync as readFileSync17, writeFileSync as writeFileSync8 } from "node:fs";
|
|
13206
13493
|
import { fileURLToPath as fileURLToPath4 } from "node:url";
|
|
13207
13494
|
init_project();
|
|
13208
13495
|
import { parse as parseToml3 } from "smol-toml";
|
|
@@ -13215,16 +13502,16 @@ function resolveTemplateRoot() {
|
|
|
13215
13502
|
try {
|
|
13216
13503
|
let dir = dirname15(fileURLToPath4(import.meta.url));
|
|
13217
13504
|
for (let i = 0; i < 8; i++) {
|
|
13218
|
-
candidates.push(
|
|
13505
|
+
candidates.push(join21(dir, "templates", "commonproject", "template"));
|
|
13219
13506
|
const parent = dirname15(dir);
|
|
13220
13507
|
if (parent === dir) break;
|
|
13221
13508
|
dir = parent;
|
|
13222
13509
|
}
|
|
13223
13510
|
} catch {
|
|
13224
13511
|
}
|
|
13225
|
-
candidates.push(
|
|
13512
|
+
candidates.push(join21(homedir6(), "code", "pjangler", "templates", "commonproject", "template"));
|
|
13226
13513
|
for (const c of candidates) {
|
|
13227
|
-
if (existsSync15(
|
|
13514
|
+
if (existsSync15(join21(c, ".agents", "hooks", "hooks.master.json"))) return c;
|
|
13228
13515
|
}
|
|
13229
13516
|
throw new Error(
|
|
13230
13517
|
"Could not locate the CommonProject template. Set PJANGLER_COMMONPROJECT_TEMPLATE to <repo>/templates/commonproject/template."
|
|
@@ -13250,8 +13537,8 @@ var CopyAgentHooksTree = class extends Command {
|
|
|
13250
13537
|
const created = [];
|
|
13251
13538
|
const skipped = [];
|
|
13252
13539
|
for (const { rel, dir } of items) {
|
|
13253
|
-
const src =
|
|
13254
|
-
const dest =
|
|
13540
|
+
const src = join21(templateRoot, rel);
|
|
13541
|
+
const dest = join21(this.context.targetDir, rel);
|
|
13255
13542
|
if (!existsSync15(src)) continue;
|
|
13256
13543
|
if (existsSync15(dest) && !this.context.force) {
|
|
13257
13544
|
skipped.push(rel);
|
|
@@ -13279,14 +13566,14 @@ var WireMiseAgentHooks = class _WireMiseAgentHooks extends Command {
|
|
|
13279
13566
|
if (!resolveAgentHooksLayer2()) {
|
|
13280
13567
|
return { success: true, message: this.formatMessage(AGENT_HOOKS_SKIP_MESSAGE) };
|
|
13281
13568
|
}
|
|
13282
|
-
const misePath =
|
|
13569
|
+
const misePath = join21(this.context.targetDir, "mise.toml");
|
|
13283
13570
|
if (!existsSync15(misePath)) {
|
|
13284
13571
|
return {
|
|
13285
13572
|
success: false,
|
|
13286
13573
|
message: "\u26A0\uFE0F No mise.toml found \u2014 run `pj add mise` first, then re-run."
|
|
13287
13574
|
};
|
|
13288
13575
|
}
|
|
13289
|
-
let content =
|
|
13576
|
+
let content = readFileSync17(misePath, "utf8");
|
|
13290
13577
|
if (content.includes(_WireMiseAgentHooks.MARKER)) {
|
|
13291
13578
|
return { success: true, message: this.formatMessage("\u2713 mise.toml already wired for agent-hooks") };
|
|
13292
13579
|
}
|
|
@@ -13326,21 +13613,13 @@ ${leaveBlock}`);
|
|
|
13326
13613
|
}
|
|
13327
13614
|
wiredHooks = true;
|
|
13328
13615
|
}
|
|
13329
|
-
const skillsSync = [
|
|
13330
|
-
'[tasks."skills:sync"]',
|
|
13331
|
-
`description = "Reconcile this project's selected skills"`,
|
|
13332
|
-
`tools = ${SKILLS_SYNC_TOOLS}`,
|
|
13333
|
-
`run = "skillex sync --scope project --project '${cr}'"`,
|
|
13334
|
-
""
|
|
13335
|
-
];
|
|
13336
|
-
const managedSkillsSync = parseToml3(skillsSync.join("\n")).tasks["skills:sync"];
|
|
13337
|
-
const keepSkillsSync = existingTasks["skills:sync"] !== void 0 && canonicalJson(existingTasks["skills:sync"]) === canonicalJson(managedSkillsSync);
|
|
13338
13616
|
const appended = [
|
|
13339
13617
|
"",
|
|
13340
13618
|
_WireMiseAgentHooks.MARKER + " (generated \u2014 see .agents/hooks/README.md)",
|
|
13341
13619
|
// PJAN-61: task names use the colon namespace form. A colon is not legal
|
|
13342
13620
|
// in a BARE toml key, so every header here MUST stay quoted.
|
|
13343
|
-
|
|
13621
|
+
// PJAN-135 retired 2026-10-10: no skills:sync task is appended — skill
|
|
13622
|
+
// operations invoke skillex directly (`pj skills …`), never a mise task.
|
|
13344
13623
|
"[[watch_files]]",
|
|
13345
13624
|
'patterns = [".agents/hooks/hooks.master.json"]',
|
|
13346
13625
|
'task = "hooks:sync"',
|
|
@@ -13509,7 +13788,7 @@ if __name__ == "__main__":
|
|
|
13509
13788
|
|
|
13510
13789
|
// src/commands/AddMiseCodegraphScript.ts
|
|
13511
13790
|
import { chmodSync as chmodSync5 } from "fs";
|
|
13512
|
-
import { join as
|
|
13791
|
+
import { join as join22 } from "path";
|
|
13513
13792
|
var AddMiseCodegraphScript = class extends Command {
|
|
13514
13793
|
async invoke() {
|
|
13515
13794
|
const filePath = ".mise/scripts/codegraph.sh";
|
|
@@ -13563,7 +13842,7 @@ fi
|
|
|
13563
13842
|
`;
|
|
13564
13843
|
this.writeFile(filePath, content);
|
|
13565
13844
|
if (!this.context.dryRun) {
|
|
13566
|
-
chmodSync5(
|
|
13845
|
+
chmodSync5(join22(this.context.targetDir, filePath), 493);
|
|
13567
13846
|
}
|
|
13568
13847
|
return {
|
|
13569
13848
|
success: true,
|
|
@@ -13601,18 +13880,18 @@ SECRET_KEY=""
|
|
|
13601
13880
|
// src/parity/index.ts
|
|
13602
13881
|
import { existsSync as existsSync16 } from "node:fs";
|
|
13603
13882
|
import { homedir as homedir7 } from "node:os";
|
|
13604
|
-
import { dirname as dirname16, join as
|
|
13883
|
+
import { dirname as dirname16, join as join23, resolve as resolve19 } from "node:path";
|
|
13605
13884
|
import { fileURLToPath as fileURLToPath5 } from "node:url";
|
|
13606
13885
|
function resolvePjanglerRoot2() {
|
|
13607
13886
|
let dir = dirname16(fileURLToPath5(import.meta.url));
|
|
13608
13887
|
while (dir !== dirname16(dir)) {
|
|
13609
|
-
if (existsSync16(
|
|
13888
|
+
if (existsSync16(join23(dir, "package.json")) && existsSync16(join23(dir, "templates", "commonproject", "copier.yml"))) return dir;
|
|
13610
13889
|
dir = dirname16(dir);
|
|
13611
13890
|
}
|
|
13612
|
-
return
|
|
13891
|
+
return resolve19(process.cwd());
|
|
13613
13892
|
}
|
|
13614
13893
|
function lifecycleContext(repoArg, dryRun, acceptRegistryMatches = false, overrides = {}) {
|
|
13615
|
-
const repoRoot =
|
|
13894
|
+
const repoRoot = resolve19(repoArg ?? process.cwd());
|
|
13616
13895
|
return {
|
|
13617
13896
|
...overrides,
|
|
13618
13897
|
targetDir: repoRoot,
|
|
@@ -13781,13 +14060,13 @@ init_version();
|
|
|
13781
14060
|
// src/project/info.ts
|
|
13782
14061
|
init_boardUrl();
|
|
13783
14062
|
init_project();
|
|
13784
|
-
import { readFileSync as
|
|
13785
|
-
import { join as
|
|
14063
|
+
import { readFileSync as readFileSync18 } from "node:fs";
|
|
14064
|
+
import { join as join24, resolve as resolve20 } from "node:path";
|
|
13786
14065
|
function readManifest2(root) {
|
|
13787
|
-
const path =
|
|
14066
|
+
const path = join24(root, ".project.json");
|
|
13788
14067
|
let manifest;
|
|
13789
14068
|
try {
|
|
13790
|
-
const value = JSON.parse(
|
|
14069
|
+
const value = JSON.parse(readFileSync18(path, "utf8"));
|
|
13791
14070
|
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("expected a JSON object");
|
|
13792
14071
|
manifest = value;
|
|
13793
14072
|
} catch (error) {
|
|
@@ -13827,8 +14106,8 @@ function readProjectInfo(input = {}) {
|
|
|
13827
14106
|
const info = {
|
|
13828
14107
|
ok: true,
|
|
13829
14108
|
project_id: projectId,
|
|
13830
|
-
repo_path:
|
|
13831
|
-
manifest_path:
|
|
14109
|
+
repo_path: resolve20(root),
|
|
14110
|
+
manifest_path: join24(resolve20(root), ".project.json"),
|
|
13832
14111
|
manifest,
|
|
13833
14112
|
registry: { location, status: "indexed" }
|
|
13834
14113
|
};
|
|
@@ -13849,27 +14128,27 @@ function readProjectInfo(input = {}) {
|
|
|
13849
14128
|
info.registry = { location, status: "not-indexed", message: "This project is not indexed. Run pj init --apply from its repository." };
|
|
13850
14129
|
return info;
|
|
13851
14130
|
}
|
|
13852
|
-
if (
|
|
14131
|
+
if (resolve20(indexed.repo_path) !== resolve20(root)) {
|
|
13853
14132
|
info.registry = { location, status: "stale", message: `The registry points to ${indexed.repo_path}; this manifest is at ${root}. Reindex the repository.` };
|
|
13854
14133
|
}
|
|
13855
14134
|
return info;
|
|
13856
14135
|
}
|
|
13857
14136
|
|
|
13858
14137
|
// src/describe/index.ts
|
|
13859
|
-
import { existsSync as existsSync17, readFileSync as
|
|
13860
|
-
import { join as
|
|
14138
|
+
import { existsSync as existsSync17, readFileSync as readFileSync19, readdirSync as readdirSync8, statSync as statSync6 } from "node:fs";
|
|
14139
|
+
import { join as join26, resolve as resolve21 } from "node:path";
|
|
13861
14140
|
init_project();
|
|
13862
14141
|
|
|
13863
14142
|
// src/describe/activity.ts
|
|
13864
|
-
import { spawn as spawn2, spawnSync as
|
|
14143
|
+
import { spawn as spawn2, spawnSync as spawnSync13 } from "node:child_process";
|
|
13865
14144
|
import { statSync as statSync5 } from "node:fs";
|
|
13866
|
-
import { join as
|
|
14145
|
+
import { join as join25 } from "node:path";
|
|
13867
14146
|
var ACTIVE_WINDOW_SECONDS = 24 * 60 * 60;
|
|
13868
14147
|
var MAX_DIRTY_STATS = 500;
|
|
13869
14148
|
var GIT_TIMEOUT_MS = 5e3;
|
|
13870
14149
|
var GIT_MAX_BUFFER = 16 * 1024 * 1024;
|
|
13871
14150
|
function git3(repo, args) {
|
|
13872
|
-
const result2 =
|
|
14151
|
+
const result2 = spawnSync13("git", ["-C", repo, ...args], {
|
|
13873
14152
|
encoding: "utf8",
|
|
13874
14153
|
timeout: GIT_TIMEOUT_MS,
|
|
13875
14154
|
maxBuffer: GIT_MAX_BUFFER
|
|
@@ -13992,7 +14271,7 @@ function uncommittedSource(repo, paths) {
|
|
|
13992
14271
|
let newest = 0;
|
|
13993
14272
|
for (const path of paths.slice(0, MAX_DIRTY_STATS)) {
|
|
13994
14273
|
try {
|
|
13995
|
-
const mtime = Math.floor(statSync5(
|
|
14274
|
+
const mtime = Math.floor(statSync5(join25(repo, path)).mtimeMs / 1e3);
|
|
13996
14275
|
if (mtime > newest) newest = mtime;
|
|
13997
14276
|
} catch {
|
|
13998
14277
|
}
|
|
@@ -14101,7 +14380,7 @@ var CONFIG_FILES = [
|
|
|
14101
14380
|
];
|
|
14102
14381
|
function readJson(path) {
|
|
14103
14382
|
try {
|
|
14104
|
-
const parsed = JSON.parse(
|
|
14383
|
+
const parsed = JSON.parse(readFileSync19(path, "utf8"));
|
|
14105
14384
|
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : void 0;
|
|
14106
14385
|
} catch {
|
|
14107
14386
|
return void 0;
|
|
@@ -14123,7 +14402,7 @@ function describeType(repo) {
|
|
|
14123
14402
|
const evidence = [];
|
|
14124
14403
|
const note = (signal, file) => evidence.push(`${signal} (${file})`);
|
|
14125
14404
|
for (const marker of LANGUAGE_MARKERS) {
|
|
14126
|
-
if (!existsSync17(
|
|
14405
|
+
if (!existsSync17(join26(repo, marker.file))) continue;
|
|
14127
14406
|
if (!languages.includes(marker.language)) {
|
|
14128
14407
|
languages.push(marker.language);
|
|
14129
14408
|
note(marker.language, marker.file);
|
|
@@ -14137,9 +14416,9 @@ function describeType(repo) {
|
|
|
14137
14416
|
}
|
|
14138
14417
|
} catch {
|
|
14139
14418
|
}
|
|
14140
|
-
const pkg = readJson(
|
|
14419
|
+
const pkg = readJson(join26(repo, "package.json"));
|
|
14141
14420
|
if (pkg) {
|
|
14142
|
-
if (existsSync17(
|
|
14421
|
+
if (existsSync17(join26(repo, "tsconfig.json"))) {
|
|
14143
14422
|
const index = languages.indexOf("javascript");
|
|
14144
14423
|
if (index >= 0) languages.splice(index, 1);
|
|
14145
14424
|
if (!languages.includes("typescript")) {
|
|
@@ -14173,26 +14452,26 @@ function describeType(repo) {
|
|
|
14173
14452
|
["hermes-fleet-host", "agents/hermes"]
|
|
14174
14453
|
];
|
|
14175
14454
|
for (const [role, marker] of roleMarkers) {
|
|
14176
|
-
if (!existsSync17(
|
|
14455
|
+
if (!existsSync17(join26(repo, marker))) continue;
|
|
14177
14456
|
roles.push(role);
|
|
14178
14457
|
note(role, marker);
|
|
14179
14458
|
}
|
|
14180
14459
|
return { primaryLanguage: languages[0], languages, roles, evidence };
|
|
14181
14460
|
}
|
|
14182
14461
|
function describeIdentity(repo, registryPath) {
|
|
14183
|
-
const manifestPath =
|
|
14462
|
+
const manifestPath = join26(repo, ".project.json");
|
|
14184
14463
|
const manifest = readJson(manifestPath);
|
|
14185
14464
|
const drift = [];
|
|
14186
|
-
let
|
|
14465
|
+
let record3;
|
|
14187
14466
|
let registryReadable = true;
|
|
14188
14467
|
try {
|
|
14189
14468
|
const registry = loadProjectRegistry(registryPath);
|
|
14190
14469
|
const identity = manifest?.project_id ?? manifest?.project_slug;
|
|
14191
14470
|
const slug = typeof identity === "string" ? identity.toLowerCase() : void 0;
|
|
14192
|
-
const resolved =
|
|
14193
|
-
|
|
14471
|
+
const resolved = resolve21(repo);
|
|
14472
|
+
record3 = (slug ? registry.projects[slug] : void 0) ?? Object.values(registry.projects).find((project) => resolve21(project.repo_path) === resolved);
|
|
14194
14473
|
const statuses = registry.__registry_status;
|
|
14195
|
-
const status =
|
|
14474
|
+
const status = record3 && statuses?.[record3.slug];
|
|
14196
14475
|
if (status && status.status !== "ok") {
|
|
14197
14476
|
drift.push({ note: `canonical manifest is ${status.status}; displaying the last indexed record. ${status.error ?? ""}`, command: "pjangler doctor" });
|
|
14198
14477
|
}
|
|
@@ -14200,14 +14479,14 @@ function describeIdentity(repo, registryPath) {
|
|
|
14200
14479
|
registryReadable = false;
|
|
14201
14480
|
drift.push({ note: `registry unreadable: ${err instanceof Error ? err.message : String(err)}` });
|
|
14202
14481
|
}
|
|
14203
|
-
if (manifest && !
|
|
14482
|
+
if (manifest && !record3 && registryReadable) {
|
|
14204
14483
|
drift.push({ note: ".project.json exists but this repo is not in the pjangler registry" });
|
|
14205
14484
|
}
|
|
14206
|
-
if (
|
|
14207
|
-
drift.push({ note: `registered as ${
|
|
14485
|
+
if (record3 && !manifest) {
|
|
14486
|
+
drift.push({ note: `registered as ${record3.slug} but .project.json is missing`, command: "pjangler doctor" });
|
|
14208
14487
|
}
|
|
14209
|
-
if (
|
|
14210
|
-
drift.push({ note: `registry repo_path points elsewhere: ${
|
|
14488
|
+
if (record3 && resolve21(record3.repo_path) !== resolve21(repo)) {
|
|
14489
|
+
drift.push({ note: `registry repo_path points elsewhere: ${record3.repo_path}`, command: "pjangler doctor" });
|
|
14211
14490
|
}
|
|
14212
14491
|
const manifestProvider = manifest?.ticket_provider;
|
|
14213
14492
|
const provider = manifestProvider ? {
|
|
@@ -14216,13 +14495,13 @@ function describeIdentity(repo, registryPath) {
|
|
|
14216
14495
|
identifier: manifestProvider.identifier,
|
|
14217
14496
|
board_id: manifestProvider.board_id,
|
|
14218
14497
|
state: manifestProvider.state
|
|
14219
|
-
} :
|
|
14220
|
-
if (manifestProvider &&
|
|
14498
|
+
} : record3?.ticket_provider;
|
|
14499
|
+
if (manifestProvider && record3) {
|
|
14221
14500
|
const fields = [
|
|
14222
|
-
["type", provider?.type,
|
|
14223
|
-
["workspace", provider?.workspace,
|
|
14224
|
-
["identifier", provider?.identifier,
|
|
14225
|
-
["board_id", provider?.board_id,
|
|
14501
|
+
["type", provider?.type, record3.ticket_provider.type],
|
|
14502
|
+
["workspace", provider?.workspace, record3.ticket_provider.workspace],
|
|
14503
|
+
["identifier", provider?.identifier, record3.ticket_provider.identifier],
|
|
14504
|
+
["board_id", provider?.board_id, record3.ticket_provider.board_id]
|
|
14226
14505
|
];
|
|
14227
14506
|
for (const [field2, fromManifest, fromRegistry] of fields) {
|
|
14228
14507
|
if ((fromManifest ?? "") === (fromRegistry ?? "")) continue;
|
|
@@ -14232,7 +14511,7 @@ function describeIdentity(repo, registryPath) {
|
|
|
14232
14511
|
}
|
|
14233
14512
|
}
|
|
14234
14513
|
const manifestAgents = manifest?.agents ?? {};
|
|
14235
|
-
const agents =
|
|
14514
|
+
const agents = record3 ? Object.entries(record3.agents).map(([name, agent]) => ({
|
|
14236
14515
|
name,
|
|
14237
14516
|
role: agent.role,
|
|
14238
14517
|
provisioningState: agent.provisioning_state,
|
|
@@ -14245,11 +14524,11 @@ function describeIdentity(repo, registryPath) {
|
|
|
14245
14524
|
}));
|
|
14246
14525
|
return {
|
|
14247
14526
|
manifest: Boolean(manifest),
|
|
14248
|
-
registered: Boolean(
|
|
14527
|
+
registered: Boolean(record3),
|
|
14249
14528
|
registryPath,
|
|
14250
|
-
slug:
|
|
14251
|
-
name:
|
|
14252
|
-
description:
|
|
14529
|
+
slug: record3?.slug ?? (manifest?.project_id ?? manifest?.project_slug),
|
|
14530
|
+
name: record3?.name ?? manifest?.project_name,
|
|
14531
|
+
description: record3?.description ?? manifest?.project_description,
|
|
14253
14532
|
ticketProvider: provider ? {
|
|
14254
14533
|
type: provider.type,
|
|
14255
14534
|
workspace: provider.workspace,
|
|
@@ -14273,12 +14552,12 @@ function describeSubsystems(repo, findings) {
|
|
|
14273
14552
|
const markers = SUBSYSTEM_MARKERS[metadata.id] ?? [];
|
|
14274
14553
|
const evidence = metadata.id === "notebook" ? (() => {
|
|
14275
14554
|
try {
|
|
14276
|
-
const manifest = JSON.parse(
|
|
14555
|
+
const manifest = JSON.parse(readFileSync19(join26(repo, ".project.json"), "utf8"));
|
|
14277
14556
|
return manifest.notebook && typeof manifest.notebook === "object" ? [".project.json#notebook"] : [];
|
|
14278
14557
|
} catch {
|
|
14279
14558
|
return [];
|
|
14280
14559
|
}
|
|
14281
|
-
})() : markers.filter((marker) => existsSync17(
|
|
14560
|
+
})() : markers.filter((marker) => existsSync17(join26(repo, marker)));
|
|
14282
14561
|
const rules = (byRecipe.get(metadata.id) ?? []).map((finding2) => ({
|
|
14283
14562
|
id: finding2.id,
|
|
14284
14563
|
title: finding2.title,
|
|
@@ -14296,8 +14575,8 @@ function describeSubsystems(repo, findings) {
|
|
|
14296
14575
|
function describeNotebook(repo, registryPath) {
|
|
14297
14576
|
try {
|
|
14298
14577
|
const registry = loadProjectRegistry(registryPath);
|
|
14299
|
-
const project = Object.values(registry.projects).find((entry) =>
|
|
14300
|
-
const manifest = existsSync17(
|
|
14578
|
+
const project = Object.values(registry.projects).find((entry) => resolve21(entry.repo_path) === resolve21(repo));
|
|
14579
|
+
const manifest = existsSync17(join26(repo, ".project.json")) ? JSON.parse(readFileSync19(join26(repo, ".project.json"), "utf8")) : void 0;
|
|
14301
14580
|
const declared = Boolean(project?.notebook || manifest?.notebook && typeof manifest.notebook === "object");
|
|
14302
14581
|
if (!declared) return { declared: false, bindingState: null, notebookId: null, overviewNoteId: null, health: null, remoteCheck: "skip", captureAdmission: null };
|
|
14303
14582
|
const config = loadEffectiveNotebookConfig(repo, registryPath);
|
|
@@ -14315,7 +14594,7 @@ function describeNotebook(repo, registryPath) {
|
|
|
14315
14594
|
}
|
|
14316
14595
|
}
|
|
14317
14596
|
function describeConfigFiles(repo) {
|
|
14318
|
-
return CONFIG_FILES.filter((spec) => existsSync17(
|
|
14597
|
+
return CONFIG_FILES.filter((spec) => existsSync17(join26(repo, spec.path))).map((spec) => ({ path: spec.path, purpose: spec.purpose, subsystem: spec.subsystem }));
|
|
14319
14598
|
}
|
|
14320
14599
|
function describeNextSteps(description, findings) {
|
|
14321
14600
|
const steps = [];
|
|
@@ -14393,7 +14672,7 @@ function describeNextSteps(description, findings) {
|
|
|
14393
14672
|
return steps;
|
|
14394
14673
|
}
|
|
14395
14674
|
async function describeProject(input = {}) {
|
|
14396
|
-
const repo =
|
|
14675
|
+
const repo = resolve21(input.repoArg ?? process.cwd());
|
|
14397
14676
|
if (!existsSync17(repo)) throw new Error(`Path does not exist: ${repo}`);
|
|
14398
14677
|
if (!statSync6(repo).isDirectory()) throw new Error(`Not a directory: ${repo}`);
|
|
14399
14678
|
const registryPath = input.registryPath ?? projectRegistryPath();
|
|
@@ -14582,7 +14861,7 @@ function validateExternalEffectConsent(input, options) {
|
|
|
14582
14861
|
return selected;
|
|
14583
14862
|
}
|
|
14584
14863
|
function resolveTargetDir(targetDir) {
|
|
14585
|
-
const dir =
|
|
14864
|
+
const dir = resolve22(targetDir ?? process.cwd());
|
|
14586
14865
|
if (!existsSync18(dir)) {
|
|
14587
14866
|
throw new Error(`Target directory does not exist: ${dir}`);
|
|
14588
14867
|
}
|
|
@@ -14594,7 +14873,7 @@ function resolveTargetDir(targetDir) {
|
|
|
14594
14873
|
function resolvePjanglerRoot3() {
|
|
14595
14874
|
let dir = dirname17(fileURLToPath6(import.meta.url));
|
|
14596
14875
|
while (dir !== dirname17(dir)) {
|
|
14597
|
-
if (existsSync18(
|
|
14876
|
+
if (existsSync18(join27(dir, "package.json")) && existsSync18(join27(dir, "templates", "commonproject", "copier.yml"))) {
|
|
14598
14877
|
return dir;
|
|
14599
14878
|
}
|
|
14600
14879
|
dir = dirname17(dir);
|
|
@@ -14889,12 +15168,12 @@ server.registerTool(
|
|
|
14889
15168
|
);
|
|
14890
15169
|
const pjanglerRoot = resolvePjanglerRoot3();
|
|
14891
15170
|
const projectSlug = validateSafePathSegment(input.projectSlug ?? slugify(input.projectName), "Project slug");
|
|
14892
|
-
const explicitTargetDir = input.targetDir ?
|
|
14893
|
-
const parentDir =
|
|
15171
|
+
const explicitTargetDir = input.targetDir ? resolve22(input.targetDir) : void 0;
|
|
15172
|
+
const parentDir = resolve22(input.parentDir ?? (explicitTargetDir ? dirname17(explicitTargetDir) : process.cwd()));
|
|
14894
15173
|
if (!existsSync18(parentDir) || !statSync7(parentDir).isDirectory()) throw new Error(`Parent directory does not exist: ${parentDir}`);
|
|
14895
15174
|
const targetDir = resolveContainedPath(
|
|
14896
15175
|
parentDir,
|
|
14897
|
-
explicitTargetDir ??
|
|
15176
|
+
explicitTargetDir ?? join27(parentDir, projectSlug),
|
|
14898
15177
|
"Bootstrap target"
|
|
14899
15178
|
);
|
|
14900
15179
|
const overwrite = input.overwrite ?? input.force ?? false;
|
|
@@ -15017,7 +15296,7 @@ server.registerTool(
|
|
|
15017
15296
|
registryPath: input.registryPath,
|
|
15018
15297
|
force: input.force ?? false,
|
|
15019
15298
|
overwrite: input.force ?? false,
|
|
15020
|
-
scaffold: !(input.targetDir && existsSync18(
|
|
15299
|
+
scaffold: !(input.targetDir && existsSync18(join27(resolve22(input.targetDir), ".git")))
|
|
15021
15300
|
});
|
|
15022
15301
|
if (!input.apply) return asText(publicCompositeProjectResponse(publicProjectPlan(plan), plan));
|
|
15023
15302
|
const preflight = await preflightProjectApply(plan, resolvePjanglerRoot3());
|