@delorenj/pjangler 1.5.12 → 1.5.14
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 +1176 -859
- package/dist/mcp-server.js +1059 -745
- package/package.json +1 -1
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,13 +6825,13 @@ 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) {
|
|
@@ -6616,13 +6844,13 @@ function skillDiagnostics(findings) {
|
|
|
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
|
}
|
|
@@ -7539,10 +7767,10 @@ function normalizeNewlines(value) {
|
|
|
7539
7767
|
return value.replace(/\r\n/g, "\n");
|
|
7540
7768
|
}
|
|
7541
7769
|
function readText(path) {
|
|
7542
|
-
return normalizeNewlines(
|
|
7770
|
+
return normalizeNewlines(readFileSync5(path, "utf8"));
|
|
7543
7771
|
}
|
|
7544
7772
|
function safeReadText2(path) {
|
|
7545
|
-
return
|
|
7773
|
+
return existsSync4(path) ? readText(path) : null;
|
|
7546
7774
|
}
|
|
7547
7775
|
function ensureParent(path) {
|
|
7548
7776
|
mkdirSync3(dirname5(path), { recursive: true });
|
|
@@ -7566,16 +7794,16 @@ function titleCaseSlug(slug) {
|
|
|
7566
7794
|
return slug.split(/[-_]/g).filter(Boolean).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join(" ");
|
|
7567
7795
|
}
|
|
7568
7796
|
function readSymlinkTarget(path) {
|
|
7569
|
-
if (!
|
|
7797
|
+
if (!existsSync4(path)) return null;
|
|
7570
7798
|
try {
|
|
7571
|
-
return
|
|
7799
|
+
return readlinkSync4(path);
|
|
7572
7800
|
} catch {
|
|
7573
7801
|
return null;
|
|
7574
7802
|
}
|
|
7575
7803
|
}
|
|
7576
7804
|
function ensureSymlink(path, target, dryRun) {
|
|
7577
|
-
if (
|
|
7578
|
-
const stat =
|
|
7805
|
+
if (existsSync4(path)) {
|
|
7806
|
+
const stat = lstatSync6(path);
|
|
7579
7807
|
if (stat.isSymbolicLink()) {
|
|
7580
7808
|
const current = readSymlinkTarget(path);
|
|
7581
7809
|
if (current === target) return { changed: false };
|
|
@@ -7585,18 +7813,18 @@ function ensureSymlink(path, target, dryRun) {
|
|
|
7585
7813
|
}
|
|
7586
7814
|
return { changed: true };
|
|
7587
7815
|
}
|
|
7588
|
-
return { changed: false, blocked: `${
|
|
7816
|
+
return { changed: false, blocked: `${relative5(process.cwd(), path) || path} exists and is not a symlink` };
|
|
7589
7817
|
}
|
|
7590
7818
|
if (!dryRun) symlinkSync2(target, path);
|
|
7591
7819
|
return { changed: true };
|
|
7592
7820
|
}
|
|
7593
7821
|
function bootstrapAgentsFile(repoRoot, dryRun) {
|
|
7594
|
-
const agentsPath =
|
|
7595
|
-
if (
|
|
7822
|
+
const agentsPath = join7(repoRoot, "AGENTS.md");
|
|
7823
|
+
if (existsSync4(agentsPath)) return { changedFiles: [], details: [] };
|
|
7596
7824
|
for (const file of ["CLAUDE.md", "GEMINI.md"]) {
|
|
7597
|
-
const source =
|
|
7598
|
-
if (!
|
|
7599
|
-
const stat =
|
|
7825
|
+
const source = join7(repoRoot, file);
|
|
7826
|
+
if (!existsSync4(source)) continue;
|
|
7827
|
+
const stat = lstatSync6(source);
|
|
7600
7828
|
if (stat.isSymbolicLink()) continue;
|
|
7601
7829
|
if (stat.isFile()) {
|
|
7602
7830
|
if (!dryRun) renameSync2(source, agentsPath);
|
|
@@ -7604,9 +7832,9 @@ function bootstrapAgentsFile(repoRoot, dryRun) {
|
|
|
7604
7832
|
}
|
|
7605
7833
|
return { changedFiles: [], details: [], blocked: `${file} exists but is not a regular file; cannot promote to AGENTS.md` };
|
|
7606
7834
|
}
|
|
7607
|
-
const readmePath =
|
|
7608
|
-
if (
|
|
7609
|
-
const stat =
|
|
7835
|
+
const readmePath = join7(repoRoot, "README.md");
|
|
7836
|
+
if (existsSync4(readmePath)) {
|
|
7837
|
+
const stat = lstatSync6(readmePath);
|
|
7610
7838
|
if (!stat.isFile()) return { changedFiles: [], details: [], blocked: "README.md exists but is not a regular file; cannot copy to AGENTS.md" };
|
|
7611
7839
|
if (!dryRun) copyFileSync(readmePath, agentsPath);
|
|
7612
7840
|
return { changedFiles: [agentsPath], details: ["Copied README.md to AGENTS.md before wiring agent-file symlinks"] };
|
|
@@ -7644,12 +7872,12 @@ function yamlGet(text, keyPath) {
|
|
|
7644
7872
|
return "";
|
|
7645
7873
|
}
|
|
7646
7874
|
function discoverRoles(repoRoot) {
|
|
7647
|
-
const rolesDir =
|
|
7648
|
-
if (!
|
|
7649
|
-
return
|
|
7650
|
-
const roleDir =
|
|
7651
|
-
const roleYamlPath =
|
|
7652
|
-
if (!
|
|
7875
|
+
const rolesDir = join7(repoRoot, "agents", "hermes");
|
|
7876
|
+
if (!existsSync4(rolesDir)) return [];
|
|
7877
|
+
return readdirSync4(rolesDir, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => {
|
|
7878
|
+
const roleDir = join7(rolesDir, entry.name);
|
|
7879
|
+
const roleYamlPath = join7(roleDir, "role.yaml");
|
|
7880
|
+
if (!existsSync4(roleYamlPath)) return null;
|
|
7653
7881
|
const text = readText(roleYamlPath);
|
|
7654
7882
|
const runtimeRepoRaw = yamlGet(text, "runtime.github_repo");
|
|
7655
7883
|
return {
|
|
@@ -7681,26 +7909,26 @@ function discoverRoles(repoRoot) {
|
|
|
7681
7909
|
}).filter((value) => Boolean(value));
|
|
7682
7910
|
}
|
|
7683
7911
|
function templateScript(ctx, name) {
|
|
7684
|
-
const source =
|
|
7685
|
-
return
|
|
7912
|
+
const source = join7(ctx.pjanglerRoot, ".mise", "scripts", name);
|
|
7913
|
+
return existsSync4(source) ? readText(source) : void 0;
|
|
7686
7914
|
}
|
|
7687
7915
|
function templateVersioningScript(ctx) {
|
|
7688
7916
|
return templateScript(ctx, "versioning.sh");
|
|
7689
7917
|
}
|
|
7690
7918
|
function templateLinkAgentfilesScript(ctx) {
|
|
7691
|
-
const source =
|
|
7692
|
-
return
|
|
7919
|
+
const source = join7(ctx.pjanglerRoot, "templates", "commonproject", "template", ".mise", "scripts", "link-agentfiles.sh");
|
|
7920
|
+
return existsSync4(source) ? readText(source) : templateScript(ctx, "link-agentfiles.sh");
|
|
7693
7921
|
}
|
|
7694
7922
|
function templateMaterializeEnvScript(ctx) {
|
|
7695
|
-
const source =
|
|
7696
|
-
return
|
|
7923
|
+
const source = join7(ctx.pjanglerRoot, "templates", "commonproject", "template", MATERIALIZE_ENV_SCRIPT_REL);
|
|
7924
|
+
return existsSync4(source) ? readText(source) : void 0;
|
|
7697
7925
|
}
|
|
7698
7926
|
function resolveAgentHooksLayer(ctx) {
|
|
7699
7927
|
const override = process.env.PJ_AGENT_HOOKS_LAYER;
|
|
7700
7928
|
if (override === "0" || override === "false") return false;
|
|
7701
7929
|
if (override === "1" || override === "true") return true;
|
|
7702
|
-
if (
|
|
7703
|
-
return !
|
|
7930
|
+
if (existsSync4(join7(ctx.repoRoot, ".agents", "hooks", "sync.py"))) return true;
|
|
7931
|
+
return !existsSync4(join7(ctx.homeDir, ".agents", "hooks"));
|
|
7704
7932
|
}
|
|
7705
7933
|
function evaluateMiseConditionals(template, agentHooksLayer) {
|
|
7706
7934
|
const out = [];
|
|
@@ -7730,22 +7958,22 @@ function renderGeneratedProjectMiseToml(ctx, template) {
|
|
|
7730
7958
|
return evaluateMiseConditionals(template, resolveAgentHooksLayer(ctx)).replace(/\{%\s*raw\s*%\}([\s\S]*?)\{%\s*endraw\s*%\}/g, "$1").replace(/\{\{\s*project_name\s*\}\}/g, projectName);
|
|
7731
7959
|
}
|
|
7732
7960
|
function ensureMiseTomlFromTemplate(ctx, changedFiles) {
|
|
7733
|
-
const targetPath =
|
|
7734
|
-
if (
|
|
7735
|
-
const sourcePath =
|
|
7736
|
-
if (!
|
|
7961
|
+
const targetPath = join7(ctx.repoRoot, "mise.toml");
|
|
7962
|
+
if (existsSync4(targetPath)) return null;
|
|
7963
|
+
const sourcePath = join7(ctx.pjanglerRoot, "templates", "commonproject", "template", "mise.toml.jinja");
|
|
7964
|
+
if (!existsSync4(sourcePath)) return null;
|
|
7737
7965
|
const rendered = renderGeneratedProjectMiseToml(ctx, readText(sourcePath));
|
|
7738
7966
|
changedFiles.push(targetPath);
|
|
7739
7967
|
if (!ctx.dryRun) writeText(targetPath, rendered);
|
|
7740
7968
|
return rendered;
|
|
7741
7969
|
}
|
|
7742
7970
|
function templateCommonProjectText(ctx, rel) {
|
|
7743
|
-
const path =
|
|
7744
|
-
return
|
|
7971
|
+
const path = join7(ctx.pjanglerRoot, "templates", "commonproject", "template", rel);
|
|
7972
|
+
return existsSync4(path) ? readText(path) : void 0;
|
|
7745
7973
|
}
|
|
7746
7974
|
function lstatIfPresent(path) {
|
|
7747
7975
|
try {
|
|
7748
|
-
return
|
|
7976
|
+
return lstatSync6(path);
|
|
7749
7977
|
} catch (error) {
|
|
7750
7978
|
if (error.code === "ENOENT") return void 0;
|
|
7751
7979
|
throw error;
|
|
@@ -7753,17 +7981,17 @@ function lstatIfPresent(path) {
|
|
|
7753
7981
|
}
|
|
7754
7982
|
function templateVersionFilesConf(ctx, repoRoot) {
|
|
7755
7983
|
if (isObsidianPluginRepo(ctx, repoRoot)) return OBSIDIAN_VERSION_FILES_CONF;
|
|
7756
|
-
const packageJson =
|
|
7757
|
-
return
|
|
7984
|
+
const packageJson = join7(repoRoot, "package.json");
|
|
7985
|
+
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
7986
|
}
|
|
7759
7987
|
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
7988
|
function recordedProjectType(ctx) {
|
|
7761
|
-
return
|
|
7989
|
+
return record2(record2(readProjectJson(ctx)?.template).commonproject).project_type;
|
|
7762
7990
|
}
|
|
7763
7991
|
function isObsidianPluginRepo(ctx, repoRoot) {
|
|
7764
7992
|
const recorded = recordedProjectType(ctx);
|
|
7765
7993
|
if (recorded !== void 0) return recorded === "obsidian-plugin";
|
|
7766
|
-
const manifest =
|
|
7994
|
+
const manifest = record2(tryParseJson(safeReadText2(join7(repoRoot, "manifest.json"))));
|
|
7767
7995
|
return typeof manifest.id === "string" && typeof manifest.minAppVersion === "string";
|
|
7768
7996
|
}
|
|
7769
7997
|
function unsupportedVersionTypes(conf, script) {
|
|
@@ -8226,7 +8454,7 @@ function describeRewrite(before, after) {
|
|
|
8226
8454
|
return difference ? `migrate would still rewrite ${difference} in mise.toml` : "migrate would still re-lay out mise.toml (its meaning is unchanged)";
|
|
8227
8455
|
}
|
|
8228
8456
|
function readProjectJson(ctx) {
|
|
8229
|
-
return tryParseJson(safeReadText2(
|
|
8457
|
+
return tryParseJson(safeReadText2(join7(ctx.repoRoot, ".project.json")));
|
|
8230
8458
|
}
|
|
8231
8459
|
function readDeclaredAgents(ctx) {
|
|
8232
8460
|
const project = readProjectJson(ctx);
|
|
@@ -8243,8 +8471,8 @@ function readDeclaredAgents(ctx) {
|
|
|
8243
8471
|
});
|
|
8244
8472
|
}
|
|
8245
8473
|
function readRoleYamlAt(roleDir) {
|
|
8246
|
-
const roleYamlPath =
|
|
8247
|
-
if (!
|
|
8474
|
+
const roleYamlPath = join7(roleDir, "role.yaml");
|
|
8475
|
+
if (!existsSync4(roleYamlPath)) return null;
|
|
8248
8476
|
const text = readText(roleYamlPath);
|
|
8249
8477
|
return {
|
|
8250
8478
|
role: yamlGet(text, "role"),
|
|
@@ -8255,7 +8483,7 @@ function readRoleYamlAt(roleDir) {
|
|
|
8255
8483
|
}
|
|
8256
8484
|
function declaredRoleIsUnprovisioned(repoRoot, roleDir) {
|
|
8257
8485
|
if (!roleDir) return true;
|
|
8258
|
-
return !
|
|
8486
|
+
return !existsSync4(join7(resolve6(repoRoot, roleDir), "role.yaml"));
|
|
8259
8487
|
}
|
|
8260
8488
|
function validateDeclaredAgent(ctx, declared) {
|
|
8261
8489
|
const details = [];
|
|
@@ -8263,8 +8491,8 @@ function validateDeclaredAgent(ctx, declared) {
|
|
|
8263
8491
|
details.push(`agents.${declared.agentId}.role_dir missing`);
|
|
8264
8492
|
return { valid: false, details };
|
|
8265
8493
|
}
|
|
8266
|
-
const roleDir =
|
|
8267
|
-
if (!
|
|
8494
|
+
const roleDir = resolve6(ctx.repoRoot, declared.roleDir);
|
|
8495
|
+
if (!existsSync4(roleDir)) {
|
|
8268
8496
|
details.push(`agents.${declared.agentId}.role_dir ${declared.roleDir} does not exist`);
|
|
8269
8497
|
return { valid: false, roleDir, details };
|
|
8270
8498
|
}
|
|
@@ -8280,13 +8508,13 @@ function validateDeclaredAgent(ctx, declared) {
|
|
|
8280
8508
|
details.push(`agents.${declared.agentId} should map to agent_id ${roleYaml.agentId}`);
|
|
8281
8509
|
}
|
|
8282
8510
|
if (roleYaml.providerName) {
|
|
8283
|
-
const dispatcher =
|
|
8284
|
-
if (!
|
|
8285
|
-
details.push(`agents.${declared.agentId} provider dispatcher ${
|
|
8511
|
+
const dispatcher = join7(roleDir, ".scripts", "lib", "ticket-provider.sh");
|
|
8512
|
+
if (!existsSync4(dispatcher)) {
|
|
8513
|
+
details.push(`agents.${declared.agentId} provider dispatcher ${relative5(ctx.repoRoot, dispatcher)} missing`);
|
|
8286
8514
|
}
|
|
8287
|
-
const provider =
|
|
8288
|
-
if (!
|
|
8289
|
-
details.push(`agents.${declared.agentId} provider script ${
|
|
8515
|
+
const provider = join7(roleDir, ".scripts", "providers", `${roleYaml.providerName}.sh`);
|
|
8516
|
+
if (!existsSync4(provider)) {
|
|
8517
|
+
details.push(`agents.${declared.agentId} provider script ${relative5(ctx.repoRoot, provider)} missing`);
|
|
8290
8518
|
}
|
|
8291
8519
|
}
|
|
8292
8520
|
return { valid: details.length === 0, role: roleYaml.role, agentId: roleYaml.agentId, roleDir, details };
|
|
@@ -8312,7 +8540,7 @@ function canonicalProjectJson(ctx) {
|
|
|
8312
8540
|
role.agentId || `${slug}-${role.role}`,
|
|
8313
8541
|
{
|
|
8314
8542
|
role: role.role,
|
|
8315
|
-
role_dir:
|
|
8543
|
+
role_dir: relative5(ctx.repoRoot, role.roleDir)
|
|
8316
8544
|
}
|
|
8317
8545
|
])
|
|
8318
8546
|
);
|
|
@@ -8344,7 +8572,7 @@ function canonicalProjectJson(ctx) {
|
|
|
8344
8572
|
}
|
|
8345
8573
|
agents[validated.agentId] = {
|
|
8346
8574
|
role: validated.role,
|
|
8347
|
-
role_dir:
|
|
8575
|
+
role_dir: relative5(ctx.repoRoot, validated.roleDir),
|
|
8348
8576
|
...declared.extras
|
|
8349
8577
|
};
|
|
8350
8578
|
}
|
|
@@ -8362,12 +8590,12 @@ function canonicalProjectJson(ctx) {
|
|
|
8362
8590
|
};
|
|
8363
8591
|
}
|
|
8364
8592
|
function projectJsonFinding(ctx) {
|
|
8365
|
-
const projectPath =
|
|
8366
|
-
const planeJsonPath =
|
|
8593
|
+
const projectPath = join7(ctx.repoRoot, ".project.json");
|
|
8594
|
+
const planeJsonPath = join7(ctx.repoRoot, ".plane.json");
|
|
8367
8595
|
const details = [];
|
|
8368
8596
|
const data = readProjectJson(ctx);
|
|
8369
8597
|
const roles = discoverRoles(ctx.repoRoot);
|
|
8370
|
-
if (!
|
|
8598
|
+
if (!existsSync4(projectPath)) {
|
|
8371
8599
|
return { id: "sot.project-json", title: "Canonical .project.json", status: "fail", summary: ".project.json missing", details: [], fixable: true };
|
|
8372
8600
|
}
|
|
8373
8601
|
if (!data) {
|
|
@@ -8387,8 +8615,8 @@ function projectJsonFinding(ctx) {
|
|
|
8387
8615
|
continue;
|
|
8388
8616
|
}
|
|
8389
8617
|
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 ${
|
|
8618
|
+
if (agent.role_dir !== relative5(ctx.repoRoot, role.roleDir)) {
|
|
8619
|
+
details.push(`agents.${role.agentId}.role_dir should be ${relative5(ctx.repoRoot, role.roleDir)}`);
|
|
8392
8620
|
}
|
|
8393
8621
|
}
|
|
8394
8622
|
const declaredAgents = readDeclaredAgents(ctx);
|
|
@@ -8416,7 +8644,7 @@ function projectJsonFinding(ctx) {
|
|
|
8416
8644
|
if ("reconcile" in automation) {
|
|
8417
8645
|
details.push("automation.reconcile is vestigial and should be removed; the heartbeat reads role.yaml, never this key");
|
|
8418
8646
|
}
|
|
8419
|
-
if (
|
|
8647
|
+
if (existsSync4(planeJsonPath)) details.push(".plane.json should not exist once .project.json is canonical");
|
|
8420
8648
|
return {
|
|
8421
8649
|
id: "sot.project-json",
|
|
8422
8650
|
title: "Canonical .project.json",
|
|
@@ -8427,13 +8655,14 @@ function projectJsonFinding(ctx) {
|
|
|
8427
8655
|
};
|
|
8428
8656
|
}
|
|
8429
8657
|
var BMAD_NPM_PACKAGE = "bmad-method";
|
|
8430
|
-
var BMAD_INSTALLER_VERSION = "6.12.
|
|
8658
|
+
var BMAD_INSTALLER_VERSION = "6.12.1";
|
|
8431
8659
|
var BMAD_TARGET_CHANNEL = "next";
|
|
8660
|
+
var BMAD_STABLE_CHANNEL = "latest";
|
|
8432
8661
|
var BMAD_DIST_TAGS_TTL_MS = 60 * 60 * 1e3;
|
|
8433
8662
|
var DEFAULT_BMAD_MODULES = ["bmm", "bmb", "cis"];
|
|
8434
8663
|
var BMAD_INSTALL_TOOLS = SUPPORTED_BMAD_TOOLS;
|
|
8435
8664
|
function manifestBmadModules(repoRoot) {
|
|
8436
|
-
const manifestPath =
|
|
8665
|
+
const manifestPath = join7(repoRoot, "_bmad", "_config", "manifest.yaml");
|
|
8437
8666
|
const raw = safeReadText2(manifestPath);
|
|
8438
8667
|
if (raw === null) return { status: "absent" };
|
|
8439
8668
|
try {
|
|
@@ -8442,14 +8671,16 @@ function manifestBmadModules(repoRoot) {
|
|
|
8442
8671
|
return { status: "invalid", error: `${manifestPath} must define a modules array` };
|
|
8443
8672
|
}
|
|
8444
8673
|
const declared = [];
|
|
8674
|
+
let hasG33 = false;
|
|
8445
8675
|
for (const entry of parsed.modules) {
|
|
8446
8676
|
const name = typeof entry === "string" ? entry : entry && typeof entry === "object" ? entry.name : void 0;
|
|
8447
8677
|
if (typeof name !== "string" || !/^[A-Za-z0-9][A-Za-z0-9_-]*$/.test(name)) {
|
|
8448
8678
|
return { status: "invalid", error: `${manifestPath} contains an invalid module entry` };
|
|
8449
8679
|
}
|
|
8450
|
-
if (name
|
|
8680
|
+
if (name === "g33") hasG33 = true;
|
|
8681
|
+
if (name !== "core" && name !== "custom" && name !== "g33") declared.push(name);
|
|
8451
8682
|
}
|
|
8452
|
-
return { status: "valid", modules: Array.from(new Set(declared)) };
|
|
8683
|
+
return { status: "valid", modules: Array.from(new Set(declared)), hasG33 };
|
|
8453
8684
|
} catch (error) {
|
|
8454
8685
|
return {
|
|
8455
8686
|
status: "invalid",
|
|
@@ -8457,24 +8688,24 @@ function manifestBmadModules(repoRoot) {
|
|
|
8457
8688
|
};
|
|
8458
8689
|
}
|
|
8459
8690
|
}
|
|
8460
|
-
function
|
|
8461
|
-
const raw = safeReadText2(
|
|
8691
|
+
function configuredBmadModuleSelection(repoRoot) {
|
|
8692
|
+
const raw = safeReadText2(join7(repoRoot, "_bmad", "config.toml"));
|
|
8462
8693
|
if (raw === null) return void 0;
|
|
8463
8694
|
const modules = [...raw.matchAll(/^\[modules\.([A-Za-z0-9][A-Za-z0-9_-]*)\]\s*$/gm)].map((match) => match[1]);
|
|
8464
|
-
return Array.from(new Set(modules));
|
|
8695
|
+
return { modules: Array.from(new Set(modules)).filter((name) => name !== "g33"), hasG33: modules.includes("g33") };
|
|
8465
8696
|
}
|
|
8466
8697
|
function selectedBmadModules(repoRoot) {
|
|
8467
8698
|
const manifest = manifestBmadModules(repoRoot);
|
|
8468
8699
|
if (manifest.status === "valid") return manifest.modules;
|
|
8469
8700
|
if (manifest.status === "invalid") throw new Error(manifest.error);
|
|
8470
|
-
return
|
|
8701
|
+
return configuredBmadModuleSelection(repoRoot)?.modules ?? [...DEFAULT_BMAD_MODULES];
|
|
8471
8702
|
}
|
|
8472
8703
|
function requiredBmadSentinels(repoRoot, modules = selectedBmadModules(repoRoot)) {
|
|
8473
8704
|
return [
|
|
8474
|
-
|
|
8475
|
-
|
|
8476
|
-
|
|
8477
|
-
...modules.map((module) =>
|
|
8705
|
+
join7("core", "config.yaml"),
|
|
8706
|
+
join7("config.toml"),
|
|
8707
|
+
join7("_config", "manifest.yaml"),
|
|
8708
|
+
...modules.map((module) => join7(module, "config.yaml"))
|
|
8478
8709
|
];
|
|
8479
8710
|
}
|
|
8480
8711
|
function canonicalBmadProjectName(repoRoot) {
|
|
@@ -8486,7 +8717,7 @@ function bmadProjectNameIssues(repoRoot) {
|
|
|
8486
8717
|
const expected = canonicalBmadProjectName(repoRoot);
|
|
8487
8718
|
const paths = [];
|
|
8488
8719
|
const details = [];
|
|
8489
|
-
const configToml =
|
|
8720
|
+
const configToml = join7(repoRoot, "_bmad", "config.toml");
|
|
8490
8721
|
const tomlText = safeReadText2(configToml);
|
|
8491
8722
|
const tomlMatch = tomlText?.match(/^project_name\s*=\s*"((?:\\.|[^"\\])*)"\s*$/m);
|
|
8492
8723
|
let tomlName;
|
|
@@ -8501,16 +8732,16 @@ function bmadProjectNameIssues(repoRoot) {
|
|
|
8501
8732
|
paths.push(configToml);
|
|
8502
8733
|
details.push(`_bmad/config.toml project_name must be ${JSON.stringify(expected)}`);
|
|
8503
8734
|
}
|
|
8504
|
-
const bmadRoot =
|
|
8505
|
-
if (
|
|
8735
|
+
const bmadRoot = join7(repoRoot, "_bmad");
|
|
8736
|
+
if (existsSync4(bmadRoot)) {
|
|
8506
8737
|
let declared;
|
|
8507
8738
|
try {
|
|
8508
8739
|
declared = selectedBmadModules(repoRoot);
|
|
8509
8740
|
} catch {
|
|
8510
|
-
declared =
|
|
8741
|
+
declared = readdirSync4(bmadRoot);
|
|
8511
8742
|
}
|
|
8512
8743
|
for (const name of new Set(declared)) {
|
|
8513
|
-
const configPath =
|
|
8744
|
+
const configPath = join7(bmadRoot, name, "config.yaml");
|
|
8514
8745
|
const raw = safeReadText2(configPath);
|
|
8515
8746
|
if (raw === null) continue;
|
|
8516
8747
|
let actual;
|
|
@@ -8530,7 +8761,7 @@ function bmadProjectNameIssues(repoRoot) {
|
|
|
8530
8761
|
function isCurrentActivation(path, activations) {
|
|
8531
8762
|
if (activations.owned.has(path)) return true;
|
|
8532
8763
|
try {
|
|
8533
|
-
if (activations.owned.has(
|
|
8764
|
+
if (activations.owned.has(join7(realpathSync3(dirname5(path)), basename3(path)))) return true;
|
|
8534
8765
|
return activations.targets.has(realpathSync3(path));
|
|
8535
8766
|
} catch {
|
|
8536
8767
|
return false;
|
|
@@ -8543,25 +8774,25 @@ async function skillActivations(ctx) {
|
|
|
8543
8774
|
function evictLegacyBmadPackState(ctx, changedFiles, activations) {
|
|
8544
8775
|
const details = [];
|
|
8545
8776
|
const skillDirs = [
|
|
8546
|
-
|
|
8547
|
-
...SUPPORTED_CLI_ROOTS.map((root) =>
|
|
8777
|
+
join7(ctx.repoRoot, ".agents", "skills"),
|
|
8778
|
+
...SUPPORTED_CLI_ROOTS.map((root) => join7(ctx.repoRoot, root, "skills"))
|
|
8548
8779
|
];
|
|
8549
8780
|
for (const dir of skillDirs) {
|
|
8550
8781
|
const dirStat = lstatIfPresent(dir);
|
|
8551
8782
|
if (!dirStat || dirStat.isSymbolicLink() || !dirStat.isDirectory()) continue;
|
|
8552
8783
|
let names;
|
|
8553
8784
|
try {
|
|
8554
|
-
names =
|
|
8785
|
+
names = readdirSync4(dir);
|
|
8555
8786
|
} catch {
|
|
8556
8787
|
continue;
|
|
8557
8788
|
}
|
|
8558
8789
|
for (const name of names) {
|
|
8559
8790
|
if (!name.startsWith(BMAD_SKILL_NAME_PREFIX)) continue;
|
|
8560
|
-
const path =
|
|
8791
|
+
const path = join7(dir, name);
|
|
8561
8792
|
if (!lstatIfPresent(path)?.isSymbolicLink()) continue;
|
|
8562
8793
|
if (isCurrentActivation(path, activations)) continue;
|
|
8563
8794
|
changedFiles.push(path);
|
|
8564
|
-
details.push(`removed retired BMAD pack symlink ${
|
|
8795
|
+
details.push(`removed retired BMAD pack symlink ${relative5(ctx.repoRoot, path)}`);
|
|
8565
8796
|
if (!ctx.dryRun) unlinkSync3(path);
|
|
8566
8797
|
}
|
|
8567
8798
|
}
|
|
@@ -8569,7 +8800,7 @@ function evictLegacyBmadPackState(ctx, changedFiles, activations) {
|
|
|
8569
8800
|
}
|
|
8570
8801
|
function bmadInstallerInvocation(version = BMAD_INSTALLER_VERSION) {
|
|
8571
8802
|
const explicit = process.env.PJ_BMAD_INSTALLER?.trim();
|
|
8572
|
-
if (explicit) return { command:
|
|
8803
|
+
if (explicit) return { command: resolve6(explicit), prefixArgs: [] };
|
|
8573
8804
|
return {
|
|
8574
8805
|
command: "npx",
|
|
8575
8806
|
prefixArgs: ["-y", `${BMAD_NPM_PACKAGE}@${version}`]
|
|
@@ -8596,7 +8827,7 @@ function bmadInstallDisplay(repoRoot, modules = selectedBmadModules(repoRoot), v
|
|
|
8596
8827
|
}
|
|
8597
8828
|
function preflightBmadLifecycle(_ctx) {
|
|
8598
8829
|
const invocation = bmadInstallerInvocation();
|
|
8599
|
-
const probe =
|
|
8830
|
+
const probe = spawnSync4(invocation.command, [...invocation.prefixArgs, "--version"], {
|
|
8600
8831
|
encoding: "utf8",
|
|
8601
8832
|
timeout: 3e4
|
|
8602
8833
|
});
|
|
@@ -8618,16 +8849,26 @@ ${probe.stderr ?? ""}`;
|
|
|
8618
8849
|
}
|
|
8619
8850
|
return { ok: true };
|
|
8620
8851
|
}
|
|
8621
|
-
function runBmadInstall(repoRoot, modules = selectedBmadModules(repoRoot), version = BMAD_INSTALLER_VERSION) {
|
|
8852
|
+
async function runBmadInstall(repoRoot, modules = selectedBmadModules(repoRoot), version = BMAD_INSTALLER_VERSION) {
|
|
8853
|
+
const installedSelection = manifestBmadModules(repoRoot);
|
|
8854
|
+
const preservation = g33BmadUpdateBoundary(repoRoot, installedSelection.status === "valid" && installedSelection.hasG33 || configuredBmadModuleSelection(repoRoot)?.hasG33);
|
|
8855
|
+
if (preservation) return { ok: false, error: preservation.summary, companion: preservation };
|
|
8622
8856
|
const invocation = bmadInstallerInvocation(version);
|
|
8623
|
-
const result2 =
|
|
8857
|
+
const result2 = spawnSync4(invocation.command, [...invocation.prefixArgs, ...bmadInstallerArgs(repoRoot, modules)], { encoding: "utf8" });
|
|
8624
8858
|
if (result2.status !== 0) {
|
|
8625
|
-
|
|
8859
|
+
const stdoutTail = result2.stdout?.trim().split("\n").slice(-12).join("\n");
|
|
8860
|
+
return { ok: false, error: result2.stderr?.trim() || stdoutTail || result2.error?.message || `installer exited ${result2.status}` };
|
|
8626
8861
|
}
|
|
8627
|
-
return { ok: true };
|
|
8862
|
+
return { ok: true, companion: await runG33Companion(repoRoot, false, "bmad-install") };
|
|
8863
|
+
}
|
|
8864
|
+
function bmadChannelTarget(distTags) {
|
|
8865
|
+
const next = distTags?.[BMAD_TARGET_CHANNEL];
|
|
8866
|
+
const stable = distTags?.[BMAD_STABLE_CHANNEL];
|
|
8867
|
+
if (next && stable && compareBmadVersions(stable, next) > 0) return { version: stable, channel: BMAD_STABLE_CHANNEL };
|
|
8868
|
+
return next ? { version: next, channel: BMAD_TARGET_CHANNEL } : void 0;
|
|
8628
8869
|
}
|
|
8629
8870
|
function readInstalledBmadVersion(repoRoot) {
|
|
8630
|
-
const raw = safeReadText2(
|
|
8871
|
+
const raw = safeReadText2(join7(repoRoot, "_bmad", "_config", "manifest.yaml"));
|
|
8631
8872
|
if (!raw) return void 0;
|
|
8632
8873
|
try {
|
|
8633
8874
|
const parsed = YAML2.parse(raw);
|
|
@@ -8638,8 +8879,8 @@ function readInstalledBmadVersion(repoRoot) {
|
|
|
8638
8879
|
}
|
|
8639
8880
|
}
|
|
8640
8881
|
function bmadCachePath(homeDir) {
|
|
8641
|
-
const cacheRoot = process.env.XDG_CACHE_HOME?.trim() ||
|
|
8642
|
-
return
|
|
8882
|
+
const cacheRoot = process.env.XDG_CACHE_HOME?.trim() || join7(homeDir, ".cache");
|
|
8883
|
+
return join7(cacheRoot, "pjangler", "bmad-dist-tags.json");
|
|
8643
8884
|
}
|
|
8644
8885
|
function readBmadDistTagsCache(homeDir) {
|
|
8645
8886
|
const raw = safeReadText2(bmadCachePath(homeDir));
|
|
@@ -8654,7 +8895,7 @@ function readBmadDistTagsCache(homeDir) {
|
|
|
8654
8895
|
return void 0;
|
|
8655
8896
|
}
|
|
8656
8897
|
function fetchBmadDistTags() {
|
|
8657
|
-
const result2 =
|
|
8898
|
+
const result2 = spawnSync4("npm", ["view", BMAD_NPM_PACKAGE, "dist-tags", "--json"], {
|
|
8658
8899
|
encoding: "utf8",
|
|
8659
8900
|
timeout: 8e3
|
|
8660
8901
|
});
|
|
@@ -8806,8 +9047,8 @@ var UNSUPPORTED_BMAD_ROOTS = {
|
|
|
8806
9047
|
".zencoder": "zencoder"
|
|
8807
9048
|
};
|
|
8808
9049
|
function unsupportedRootAttestation(repoRoot, rootName) {
|
|
8809
|
-
const root =
|
|
8810
|
-
const stat =
|
|
9050
|
+
const root = join7(repoRoot, rootName);
|
|
9051
|
+
const stat = lstatSync6(root);
|
|
8811
9052
|
if (!stat.isDirectory() || stat.isSymbolicLink()) return { safe: false, reason: `${rootName} is not a regular generated directory` };
|
|
8812
9053
|
const installerTool = UNSUPPORTED_BMAD_ROOTS[rootName];
|
|
8813
9054
|
if (!installedBmadTools(repoRoot).has(installerTool)) {
|
|
@@ -8826,8 +9067,8 @@ var OPTIONAL_TEMPLATE_SCRIPTS = [
|
|
|
8826
9067
|
function optionalTemplateScriptIssues(ctx) {
|
|
8827
9068
|
const issues = [];
|
|
8828
9069
|
for (const rel of OPTIONAL_TEMPLATE_SCRIPTS) {
|
|
8829
|
-
const path =
|
|
8830
|
-
if (!
|
|
9070
|
+
const path = join7(ctx.repoRoot, rel);
|
|
9071
|
+
if (!existsSync4(path)) continue;
|
|
8831
9072
|
const expected = templateCommonProjectText(ctx, rel);
|
|
8832
9073
|
if (expected === void 0) continue;
|
|
8833
9074
|
if (safeReadText2(path) !== expected) issues.push(`${rel} is present but has drifted from the shipped template`);
|
|
@@ -8837,8 +9078,8 @@ function optionalTemplateScriptIssues(ctx) {
|
|
|
8837
9078
|
function refreshOptionalTemplateScripts(ctx) {
|
|
8838
9079
|
const changed = [];
|
|
8839
9080
|
for (const rel of OPTIONAL_TEMPLATE_SCRIPTS) {
|
|
8840
|
-
const path =
|
|
8841
|
-
if (!
|
|
9081
|
+
const path = join7(ctx.repoRoot, rel);
|
|
9082
|
+
if (!existsSync4(path)) continue;
|
|
8842
9083
|
const expected = templateCommonProjectText(ctx, rel);
|
|
8843
9084
|
if (expected === void 0 || safeReadText2(path) === expected) continue;
|
|
8844
9085
|
changed.push(path);
|
|
@@ -8855,14 +9096,14 @@ function createMiseChecks() {
|
|
|
8855
9096
|
id: "mise.config-root",
|
|
8856
9097
|
title: "mise config_root + AGENTS link hooks",
|
|
8857
9098
|
audit: (ctx) => {
|
|
8858
|
-
const misePath =
|
|
8859
|
-
if (!
|
|
9099
|
+
const misePath = join7(ctx.repoRoot, "mise.toml");
|
|
9100
|
+
if (!existsSync4(misePath)) {
|
|
8860
9101
|
return { id: "mise.config-root", title: "mise config_root + AGENTS link hooks", status: "fail", summary: "mise.toml missing", details: [], fixable: true };
|
|
8861
9102
|
}
|
|
8862
9103
|
const text = readText(misePath);
|
|
8863
9104
|
const details = [];
|
|
8864
|
-
const linkAgentfilesPath =
|
|
8865
|
-
if (!
|
|
9105
|
+
const linkAgentfilesPath = join7(ctx.repoRoot, ".mise", "scripts", "link-agentfiles.sh");
|
|
9106
|
+
if (!existsSync4(linkAgentfilesPath)) details.push(".mise/scripts/link-agentfiles.sh missing");
|
|
8866
9107
|
const toml = [];
|
|
8867
9108
|
const manual = [];
|
|
8868
9109
|
const parsed = parsedOrUndefined(text);
|
|
@@ -8909,10 +9150,10 @@ function createMiseChecks() {
|
|
|
8909
9150
|
};
|
|
8910
9151
|
},
|
|
8911
9152
|
migrate: (ctx, finding2) => {
|
|
8912
|
-
const path =
|
|
9153
|
+
const path = join7(ctx.repoRoot, "mise.toml");
|
|
8913
9154
|
const changedFiles = [];
|
|
8914
9155
|
const details = [];
|
|
8915
|
-
if (!
|
|
9156
|
+
if (!existsSync4(path)) {
|
|
8916
9157
|
if (ensureMiseTomlFromTemplate(ctx, changedFiles) === null) {
|
|
8917
9158
|
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: [] };
|
|
8918
9159
|
}
|
|
@@ -8928,7 +9169,7 @@ function createMiseChecks() {
|
|
|
8928
9169
|
if (!ctx.dryRun) writeText(path, plan.text);
|
|
8929
9170
|
text = plan.text;
|
|
8930
9171
|
}
|
|
8931
|
-
const linkAgentfilesPath =
|
|
9172
|
+
const linkAgentfilesPath = join7(ctx.repoRoot, ".mise", "scripts", "link-agentfiles.sh");
|
|
8932
9173
|
const expectedScript = templateLinkAgentfilesScript(ctx);
|
|
8933
9174
|
if (expectedScript === void 0) {
|
|
8934
9175
|
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: [] };
|
|
@@ -8966,13 +9207,13 @@ function createMiseChecks() {
|
|
|
8966
9207
|
title: "managed mise versioning block",
|
|
8967
9208
|
audit: (ctx) => {
|
|
8968
9209
|
const details = [];
|
|
8969
|
-
const misePath =
|
|
8970
|
-
const versioningPath =
|
|
8971
|
-
const manifestPath =
|
|
9210
|
+
const misePath = join7(ctx.repoRoot, "mise.toml");
|
|
9211
|
+
const versioningPath = join7(ctx.repoRoot, ".mise", "scripts", "versioning.sh");
|
|
9212
|
+
const manifestPath = join7(ctx.repoRoot, ".mise", "version-files.conf");
|
|
8972
9213
|
const text = safeReadText2(misePath);
|
|
8973
9214
|
if (!text?.includes("# >>> mise-versioning >>>")) details.push("mise versioning managed block missing");
|
|
8974
|
-
if (!
|
|
8975
|
-
if (!
|
|
9215
|
+
if (!existsSync4(versioningPath)) details.push(".mise/scripts/versioning.sh missing");
|
|
9216
|
+
if (!existsSync4(manifestPath)) details.push(".mise/version-files.conf missing");
|
|
8976
9217
|
const conf = safeReadText2(manifestPath);
|
|
8977
9218
|
const script = safeReadText2(versioningPath);
|
|
8978
9219
|
if (conf !== null && script !== null) {
|
|
@@ -8994,8 +9235,8 @@ function createMiseChecks() {
|
|
|
8994
9235
|
migrate: (ctx, finding2) => {
|
|
8995
9236
|
const changedFiles = [];
|
|
8996
9237
|
const details = [];
|
|
8997
|
-
const misePath =
|
|
8998
|
-
if (!
|
|
9238
|
+
const misePath = join7(ctx.repoRoot, "mise.toml");
|
|
9239
|
+
if (!existsSync4(misePath)) {
|
|
8999
9240
|
if (ensureMiseTomlFromTemplate(ctx, changedFiles) === null) {
|
|
9000
9241
|
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: [] };
|
|
9001
9242
|
}
|
|
@@ -9012,7 +9253,7 @@ function createMiseChecks() {
|
|
|
9012
9253
|
if (!changedFiles.includes(misePath)) changedFiles.push(misePath);
|
|
9013
9254
|
if (!ctx.dryRun) writeText(misePath, versioningPlan.text);
|
|
9014
9255
|
}
|
|
9015
|
-
const versioningPath =
|
|
9256
|
+
const versioningPath = join7(ctx.repoRoot, ".mise", "scripts", "versioning.sh");
|
|
9016
9257
|
const expectedScript = templateVersioningScript(ctx);
|
|
9017
9258
|
if (expectedScript === void 0) {
|
|
9018
9259
|
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: [] };
|
|
@@ -9024,7 +9265,7 @@ function createMiseChecks() {
|
|
|
9024
9265
|
chmodSync(versioningPath, 493);
|
|
9025
9266
|
}
|
|
9026
9267
|
}
|
|
9027
|
-
const manifestPath =
|
|
9268
|
+
const manifestPath = join7(ctx.repoRoot, ".mise", "version-files.conf");
|
|
9028
9269
|
const expectedManifest = templateVersionFilesConf(ctx, ctx.repoRoot);
|
|
9029
9270
|
if (safeReadText2(manifestPath) !== expectedManifest) {
|
|
9030
9271
|
changedFiles.push(manifestPath);
|
|
@@ -9054,7 +9295,7 @@ function planVersioningBlock(original) {
|
|
|
9054
9295
|
let replaced = [...VERSIONING_TASK_NAMES];
|
|
9055
9296
|
try {
|
|
9056
9297
|
if (oldBlock) {
|
|
9057
|
-
replaced = Object.keys(
|
|
9298
|
+
replaced = Object.keys(record2(record2(parseToml2(oldBlock)).tasks));
|
|
9058
9299
|
text = text.replace(blockPattern, VERSIONING_BLOCK);
|
|
9059
9300
|
} else {
|
|
9060
9301
|
const statements = scanToml(text);
|
|
@@ -9077,7 +9318,7 @@ function planVersioningBlock(original) {
|
|
|
9077
9318
|
const verdict = checkRewrite(expected, text);
|
|
9078
9319
|
return verdict.ok ? { text } : { text: original, refused: verdict.reason };
|
|
9079
9320
|
}
|
|
9080
|
-
var
|
|
9321
|
+
var record2 = (value) => isTable(value) ? value : {};
|
|
9081
9322
|
var RETIRED_SKILL_SCRIPTS = [
|
|
9082
9323
|
SYNC_SKILLS_SCRIPT_REL,
|
|
9083
9324
|
PROVISION_PACKS_SCRIPT_REL,
|
|
@@ -9086,7 +9327,7 @@ var RETIRED_SKILL_SCRIPTS = [
|
|
|
9086
9327
|
".mise/scripts/unlink-project-skills-from-clis.sh"
|
|
9087
9328
|
];
|
|
9088
9329
|
function retiredSkillsScripts(ctx) {
|
|
9089
|
-
return RETIRED_SKILL_SCRIPTS.map((path) =>
|
|
9330
|
+
return RETIRED_SKILL_SCRIPTS.map((path) => join7(ctx.repoRoot, path)).filter((path) => Boolean(lstatIfPresent(path)));
|
|
9090
9331
|
}
|
|
9091
9332
|
function skillsWiringFindings(text) {
|
|
9092
9333
|
if (text === null) return { fixable: ["mise.toml is missing; add the explicit skills:sync task"], manual: [] };
|
|
@@ -9099,26 +9340,26 @@ function skillsWiringFindings(text) {
|
|
|
9099
9340
|
parsed = baseline.parsed;
|
|
9100
9341
|
fixable.push("mise.toml repeats the close of a [hooks] array (an earlier pjangler run wrote it); migrate repairs it");
|
|
9101
9342
|
}
|
|
9102
|
-
const
|
|
9103
|
-
const tasks =
|
|
9343
|
+
const record3 = (value) => isTable(value) ? value : {};
|
|
9344
|
+
const tasks = record3(parsed.tasks), task = record3(tasks[SKILLS_SYNC_TASK]);
|
|
9104
9345
|
const run = Array.isArray(task.run) && task.run.length === 1 ? task.run[0] : task.run;
|
|
9105
9346
|
if (typeof run !== "string" || !/^skillex\s+sync\s+--scope\s+project\s+--project\s+(['"])\{\{config_root\}\}\1\s*$/.test(run)) {
|
|
9106
9347
|
fixable.push("skills:sync must explicitly target {{config_root}} with the Node CLI");
|
|
9107
9348
|
}
|
|
9108
|
-
const pin =
|
|
9109
|
-
const pinned = typeof pin === "string" ? pin :
|
|
9349
|
+
const pin = record3(task.tools)["npm:@delorenj/skillex"];
|
|
9350
|
+
const pinned = typeof pin === "string" ? pin : record3(pin).version;
|
|
9110
9351
|
if (pinned !== SKILLEX_TOOL_VERSION) {
|
|
9111
9352
|
fixable.push(`skills:sync must pin @delorenj/skillex ${SKILLEX_TOOL_VERSION} in its own tools table`);
|
|
9112
9353
|
} else if (typeof pin === "string") {
|
|
9113
9354
|
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 }`);
|
|
9114
|
-
} else if (
|
|
9355
|
+
} else if (record3(pin).allow_low_downloads !== true) {
|
|
9115
9356
|
fixable.push(`skills:sync must approve @delorenj/skillex ${SKILLEX_TOOL_VERSION} with allow_low_downloads = true (mise 2026.9 refuses to install it otherwise)`);
|
|
9116
9357
|
}
|
|
9117
9358
|
const retired = /sync-skills\.py|provision-(?:packs|bmad-skills)\.py|skills[:\-]provision/;
|
|
9118
9359
|
const renames = new Map(RETIRED_TASK_RENAMES);
|
|
9119
9360
|
const replaced = /* @__PURE__ */ new Set([...RETIRED_LINK_TASKS, LINK_AGENTFILES_TASK, SKILLS_SYNC_TASK]);
|
|
9120
9361
|
for (const [name, entry] of Object.entries(tasks)) {
|
|
9121
|
-
if (!retired.test(name) && !retired.test(JSON.stringify(
|
|
9362
|
+
if (!retired.test(name) && !retired.test(JSON.stringify(record3(entry).run ?? ""))) continue;
|
|
9122
9363
|
if (replaced.has(renames.get(name) ?? name)) {
|
|
9123
9364
|
if (!fixable.includes("Retire Python skill sync/provision tasks from mise.toml")) fixable.push("Retire Python skill sync/provision tasks from mise.toml");
|
|
9124
9365
|
} else {
|
|
@@ -9146,7 +9387,7 @@ function createAgentHooksChecks() {
|
|
|
9146
9387
|
title: "Skillex project skills",
|
|
9147
9388
|
audit: async (ctx) => {
|
|
9148
9389
|
const finding2 = await auditProjectSkills(ctx);
|
|
9149
|
-
const mise = safeReadText2(
|
|
9390
|
+
const mise = safeReadText2(join7(ctx.repoRoot, "mise.toml"));
|
|
9150
9391
|
const wiring = skillsWiringIssues(mise);
|
|
9151
9392
|
const retired = retiredSkillsScripts(ctx);
|
|
9152
9393
|
const unsafe = retired.filter((path) => lstatIfPresent(path)?.isDirectory());
|
|
@@ -9163,7 +9404,7 @@ function createAgentHooksChecks() {
|
|
|
9163
9404
|
const changedFiles = [];
|
|
9164
9405
|
const details = [];
|
|
9165
9406
|
const result2 = (status, summary) => ({ id: finding2.id, title: finding2.title, status, summary, changedFiles: [...new Set(changedFiles)], details });
|
|
9166
|
-
let currentMise = safeReadText2(
|
|
9407
|
+
let currentMise = safeReadText2(join7(ctx.repoRoot, "mise.toml"));
|
|
9167
9408
|
if (currentMise === null) {
|
|
9168
9409
|
const template = templateCommonProjectText(ctx, "mise.toml.jinja");
|
|
9169
9410
|
if (template === void 0) return result2("blocked", "Generated-project mise template is unavailable");
|
|
@@ -9180,7 +9421,7 @@ function createAgentHooksChecks() {
|
|
|
9180
9421
|
changedFiles.push(path);
|
|
9181
9422
|
if (!ctx.dryRun) unlinkSync3(path);
|
|
9182
9423
|
}
|
|
9183
|
-
const misePath =
|
|
9424
|
+
const misePath = join7(ctx.repoRoot, "mise.toml");
|
|
9184
9425
|
const nextMise = upsertLinkAgentfilesBlock(currentMise, ctx);
|
|
9185
9426
|
if (safeReadText2(misePath) !== nextMise) {
|
|
9186
9427
|
changedFiles.push(misePath);
|
|
@@ -9206,9 +9447,9 @@ function createAgentHooksChecks() {
|
|
|
9206
9447
|
id: "sot.agent-symlinks",
|
|
9207
9448
|
title: "AGENTS/CLAUDE/GEMINI symlink contract",
|
|
9208
9449
|
audit: (ctx) => {
|
|
9209
|
-
const agentsPath =
|
|
9210
|
-
if (!
|
|
9211
|
-
const fallbackSources = ["CLAUDE.md", "GEMINI.md", "README.md"].filter((file) =>
|
|
9450
|
+
const agentsPath = join7(ctx.repoRoot, "AGENTS.md");
|
|
9451
|
+
if (!existsSync4(agentsPath)) {
|
|
9452
|
+
const fallbackSources = ["CLAUDE.md", "GEMINI.md", "README.md"].filter((file) => existsSync4(join7(ctx.repoRoot, file)));
|
|
9212
9453
|
if (fallbackSources.length === 0) {
|
|
9213
9454
|
return { id: "sot.agent-symlinks", title: "AGENTS/CLAUDE/GEMINI symlink contract", status: "skip", summary: "AGENTS.md missing; symlink contract not applicable", details: [], fixable: false };
|
|
9214
9455
|
}
|
|
@@ -9223,7 +9464,7 @@ function createAgentHooksChecks() {
|
|
|
9223
9464
|
}
|
|
9224
9465
|
const details = [];
|
|
9225
9466
|
for (const file of ["CLAUDE.md", "GEMINI.md"]) {
|
|
9226
|
-
const full =
|
|
9467
|
+
const full = join7(ctx.repoRoot, file);
|
|
9227
9468
|
const target = readSymlinkTarget(full);
|
|
9228
9469
|
if (target !== "AGENTS.md") details.push(`${file} should be a symlink to AGENTS.md`);
|
|
9229
9470
|
}
|
|
@@ -9247,7 +9488,7 @@ function createAgentHooksChecks() {
|
|
|
9247
9488
|
return { id: finding2.id, title: finding2.title, status: "blocked", summary: "AGENTS.md missing; cannot derive canonical agent file", changedFiles, details: [bootstrap.blocked] };
|
|
9248
9489
|
}
|
|
9249
9490
|
for (const file of ["CLAUDE.md", "GEMINI.md"]) {
|
|
9250
|
-
const full =
|
|
9491
|
+
const full = join7(ctx.repoRoot, file);
|
|
9251
9492
|
const result2 = ensureSymlink(full, "AGENTS.md", ctx.dryRun);
|
|
9252
9493
|
if (result2.blocked) blockedDetails.push(result2.blocked);
|
|
9253
9494
|
if (result2.changed) changedFiles.push(full);
|
|
@@ -9274,7 +9515,7 @@ function createProjectJsonChecks() {
|
|
|
9274
9515
|
const changedFiles = [];
|
|
9275
9516
|
const blockedDetails = [];
|
|
9276
9517
|
const droppedDetails = [];
|
|
9277
|
-
const path =
|
|
9518
|
+
const path = join7(ctx.repoRoot, ".project.json");
|
|
9278
9519
|
const existing = readProjectJson(ctx) ?? {};
|
|
9279
9520
|
const canonical = canonicalProjectJson(ctx);
|
|
9280
9521
|
const preservedDetails = [];
|
|
@@ -9306,11 +9547,11 @@ function createProjectJsonChecks() {
|
|
|
9306
9547
|
changedFiles.push(path);
|
|
9307
9548
|
if (!ctx.dryRun) writeText(path, expected);
|
|
9308
9549
|
}
|
|
9309
|
-
const planeJson =
|
|
9310
|
-
if (
|
|
9550
|
+
const planeJson = join7(ctx.repoRoot, ".plane.json");
|
|
9551
|
+
if (existsSync4(planeJson)) {
|
|
9311
9552
|
const backup = `${planeJson}.migrated-backup`;
|
|
9312
|
-
if (
|
|
9313
|
-
blockedDetails.push(`cannot back up .plane.json because ${
|
|
9553
|
+
if (existsSync4(backup)) {
|
|
9554
|
+
blockedDetails.push(`cannot back up .plane.json because ${relative5(ctx.repoRoot, backup)} already exists`);
|
|
9314
9555
|
} else {
|
|
9315
9556
|
changedFiles.push(backup);
|
|
9316
9557
|
if (!ctx.dryRun) renameSync2(planeJson, backup);
|
|
@@ -9338,10 +9579,10 @@ function createMiseOpInjectChecks() {
|
|
|
9338
9579
|
const details = [];
|
|
9339
9580
|
let envOpNeedsHands = false;
|
|
9340
9581
|
const optedOut = envMaterializationOptedOut(ctx);
|
|
9341
|
-
const envOpPath =
|
|
9342
|
-
const envOpExists =
|
|
9582
|
+
const envOpPath = join7(ctx.repoRoot, ".env.op");
|
|
9583
|
+
const envOpExists = existsSync4(envOpPath);
|
|
9343
9584
|
const envOp = envOpExists ? readText(envOpPath) : void 0;
|
|
9344
|
-
const gitignore = safeReadText2(
|
|
9585
|
+
const gitignore = safeReadText2(join7(ctx.repoRoot, ".gitignore"));
|
|
9345
9586
|
if (!envOpExists) {
|
|
9346
9587
|
details.push(".env.op missing");
|
|
9347
9588
|
} else if (!envOp?.trim()) {
|
|
@@ -9361,7 +9602,7 @@ function createMiseOpInjectChecks() {
|
|
|
9361
9602
|
if (!gitignore?.includes(".env\n") && !gitignore?.includes(".env\r\n")) details.push(".gitignore should ignore .env");
|
|
9362
9603
|
if (!gitignore?.includes(".env.*")) details.push(".gitignore should ignore .env.*");
|
|
9363
9604
|
if (!gitignore?.includes("!.env.op")) details.push(".gitignore should unignore .env.op");
|
|
9364
|
-
const misePath =
|
|
9605
|
+
const misePath = join7(ctx.repoRoot, "mise.toml");
|
|
9365
9606
|
const miseText = safeReadText2(misePath);
|
|
9366
9607
|
if (!miseText) {
|
|
9367
9608
|
details.push("mise.toml missing for .env materialization hook");
|
|
@@ -9384,15 +9625,15 @@ function createMiseOpInjectChecks() {
|
|
|
9384
9625
|
else if (plan.text !== miseText && !mise.length) mise.push(describeRewrite(miseText, plan.text));
|
|
9385
9626
|
details.push(...mise);
|
|
9386
9627
|
}
|
|
9387
|
-
const materializePath =
|
|
9628
|
+
const materializePath = join7(ctx.repoRoot, MATERIALIZE_ENV_SCRIPT_REL);
|
|
9388
9629
|
const expectedMaterializer = templateMaterializeEnvScript(ctx);
|
|
9389
9630
|
if (optedOut) {
|
|
9390
|
-
if (
|
|
9631
|
+
if (existsSync4(materializePath)) details.push(`${MATERIALIZE_ENV_SCRIPT_REL} is present, but .project.json declares secrets.materialize_env: false`);
|
|
9391
9632
|
} else if (!expectedMaterializer) {
|
|
9392
9633
|
details.push("pjangler package is missing the managed materialize-env.sh source");
|
|
9393
9634
|
} else if (safeReadText2(materializePath) !== expectedMaterializer) {
|
|
9394
9635
|
details.push(`${MATERIALIZE_ENV_SCRIPT_REL} missing or drifted`);
|
|
9395
|
-
} else if ((
|
|
9636
|
+
} else if ((lstatSync6(materializePath).mode & 73) === 0) {
|
|
9396
9637
|
details.push(`${MATERIALIZE_ENV_SCRIPT_REL} is not executable`);
|
|
9397
9638
|
}
|
|
9398
9639
|
return {
|
|
@@ -9408,13 +9649,13 @@ function createMiseOpInjectChecks() {
|
|
|
9408
9649
|
const changedFiles = [];
|
|
9409
9650
|
const details = [];
|
|
9410
9651
|
let envOpNeedsHands = false;
|
|
9411
|
-
const envOpPath =
|
|
9412
|
-
const canonicalEnvOpPath =
|
|
9413
|
-
if (!
|
|
9652
|
+
const envOpPath = join7(ctx.repoRoot, ".env.op");
|
|
9653
|
+
const canonicalEnvOpPath = join7(ctx.pjanglerRoot, "templates", "commonproject", "template", ".env.op");
|
|
9654
|
+
if (!existsSync4(canonicalEnvOpPath)) {
|
|
9414
9655
|
return { id: finding2.id, title: finding2.title, status: "blocked", summary: "pjangler package is missing the neutral .env.op template", changedFiles: [], details: [] };
|
|
9415
9656
|
}
|
|
9416
9657
|
const canonicalEnvOp = readText(canonicalEnvOpPath);
|
|
9417
|
-
if (!
|
|
9658
|
+
if (!existsSync4(envOpPath) || !readText(envOpPath).trim()) {
|
|
9418
9659
|
changedFiles.push(envOpPath);
|
|
9419
9660
|
if (!ctx.dryRun) writeText(envOpPath, canonicalEnvOp);
|
|
9420
9661
|
} else {
|
|
@@ -9438,7 +9679,7 @@ function createMiseOpInjectChecks() {
|
|
|
9438
9679
|
}
|
|
9439
9680
|
}
|
|
9440
9681
|
}
|
|
9441
|
-
const gitignorePath =
|
|
9682
|
+
const gitignorePath = join7(ctx.repoRoot, ".gitignore");
|
|
9442
9683
|
const gitignore = safeReadText2(gitignorePath) ?? "";
|
|
9443
9684
|
const requiredBlock = `# Secrets \u2014 .env is materialized from .env.op by \`op inject\` on mise enter,
|
|
9444
9685
|
# staged through a mktemp file and moved into place only on success.
|
|
@@ -9451,7 +9692,7 @@ function createMiseOpInjectChecks() {
|
|
|
9451
9692
|
changedFiles.push(gitignorePath);
|
|
9452
9693
|
if (!ctx.dryRun) writeText(gitignorePath, `${gitignore.replace(/\s*$/, "")}${gitignore.trim() ? "\n\n" : ""}${requiredBlock}`);
|
|
9453
9694
|
}
|
|
9454
|
-
const misePath =
|
|
9695
|
+
const misePath = join7(ctx.repoRoot, "mise.toml");
|
|
9455
9696
|
let currentMise = safeReadText2(misePath);
|
|
9456
9697
|
if (currentMise === null) {
|
|
9457
9698
|
const initialized = ensureMiseTomlFromTemplate(ctx, changedFiles);
|
|
@@ -9468,16 +9709,16 @@ function createMiseOpInjectChecks() {
|
|
|
9468
9709
|
if (!changedFiles.includes(misePath)) changedFiles.push(misePath);
|
|
9469
9710
|
if (!ctx.dryRun) writeText(misePath, opPlan.text);
|
|
9470
9711
|
}
|
|
9471
|
-
const materializePath =
|
|
9712
|
+
const materializePath = join7(ctx.repoRoot, MATERIALIZE_ENV_SCRIPT_REL);
|
|
9472
9713
|
const expectedMaterializer = templateMaterializeEnvScript(ctx);
|
|
9473
9714
|
if (optedOut) {
|
|
9474
|
-
if (
|
|
9715
|
+
if (existsSync4(materializePath)) {
|
|
9475
9716
|
changedFiles.push(materializePath);
|
|
9476
9717
|
if (!ctx.dryRun) rmSync2(materializePath);
|
|
9477
9718
|
}
|
|
9478
9719
|
} else if (!expectedMaterializer) {
|
|
9479
9720
|
return { id: finding2.id, title: finding2.title, status: "blocked", summary: "pjangler package is missing materialize-env.sh", changedFiles: [], details: [] };
|
|
9480
|
-
} else if (safeReadText2(materializePath) !== expectedMaterializer ||
|
|
9721
|
+
} else if (safeReadText2(materializePath) !== expectedMaterializer || existsSync4(materializePath) && (lstatSync6(materializePath).mode & 73) === 0) {
|
|
9481
9722
|
changedFiles.push(materializePath);
|
|
9482
9723
|
if (!ctx.dryRun) {
|
|
9483
9724
|
writeText(materializePath, expectedMaterializer);
|
|
@@ -9539,7 +9780,7 @@ function createProjectProvenanceChecks() {
|
|
|
9539
9780
|
title: ".copier-answers.yml provenance + drift report",
|
|
9540
9781
|
audit: (ctx) => {
|
|
9541
9782
|
const details = [];
|
|
9542
|
-
const path =
|
|
9783
|
+
const path = join7(ctx.repoRoot, ".copier-answers.yml");
|
|
9543
9784
|
const text = safeReadText2(path);
|
|
9544
9785
|
const project = readProjectJson(ctx);
|
|
9545
9786
|
if (!text) {
|
|
@@ -9567,11 +9808,11 @@ function createProjectProvenanceChecks() {
|
|
|
9567
9808
|
migrate: (ctx, finding2) => {
|
|
9568
9809
|
const changedFiles = [];
|
|
9569
9810
|
const project = canonicalProjectJson(ctx);
|
|
9570
|
-
const path =
|
|
9811
|
+
const path = join7(ctx.repoRoot, ".copier-answers.yml");
|
|
9571
9812
|
const current = safeReadText2(path);
|
|
9572
9813
|
const existing = current ? YAML2.parseDocument(current) : void 0;
|
|
9573
9814
|
const document = existing && !existing.errors.length && YAML2.isMap(existing.contents) ? existing : new YAML2.Document({});
|
|
9574
|
-
document.set("_src_path",
|
|
9815
|
+
document.set("_src_path", join7(ctx.pjanglerRoot, "templates", "commonproject"));
|
|
9575
9816
|
document.set("project_description", String(project.project_description));
|
|
9576
9817
|
document.set("project_name", String(project.project_name));
|
|
9577
9818
|
document.set("ticket_provider", String(project.ticket_provider?.type ?? "plane"));
|
|
@@ -9596,9 +9837,9 @@ ${body}`;
|
|
|
9596
9837
|
}
|
|
9597
9838
|
function supportedCliProjectionIssues(repoRoot, plan) {
|
|
9598
9839
|
const issues = [];
|
|
9599
|
-
const managedSkills =
|
|
9840
|
+
const managedSkills = join7(repoRoot, ".agents", "skills");
|
|
9600
9841
|
for (const rootName of SUPPORTED_CLI_ROOTS) {
|
|
9601
|
-
const root =
|
|
9842
|
+
const root = join7(repoRoot, rootName);
|
|
9602
9843
|
const rootStat = lstatIfPresent(root);
|
|
9603
9844
|
if (!rootStat) {
|
|
9604
9845
|
issues.push(`${rootName} missing`);
|
|
@@ -9608,7 +9849,7 @@ function supportedCliProjectionIssues(repoRoot, plan) {
|
|
|
9608
9849
|
issues.push(`${rootName} must be a real configuration directory`);
|
|
9609
9850
|
continue;
|
|
9610
9851
|
}
|
|
9611
|
-
const skills =
|
|
9852
|
+
const skills = join7(root, "skills");
|
|
9612
9853
|
const skillsStat = lstatIfPresent(skills);
|
|
9613
9854
|
if (!skillsStat) {
|
|
9614
9855
|
issues.push(`${rootName}/skills missing`);
|
|
@@ -9617,7 +9858,7 @@ function supportedCliProjectionIssues(repoRoot, plan) {
|
|
|
9617
9858
|
let projectedSkills = skills;
|
|
9618
9859
|
if (skillsStat.isSymbolicLink()) {
|
|
9619
9860
|
try {
|
|
9620
|
-
|
|
9861
|
+
readlinkSync4(skills);
|
|
9621
9862
|
} catch {
|
|
9622
9863
|
issues.push(`${rootName}/skills is an unreadable symlink`);
|
|
9623
9864
|
continue;
|
|
@@ -9645,9 +9886,9 @@ function supportedCliProjectionIssues(repoRoot, plan) {
|
|
|
9645
9886
|
issues.push(`${rootName}/skills is not a directory`);
|
|
9646
9887
|
continue;
|
|
9647
9888
|
}
|
|
9648
|
-
const hasGeneratedSkill =
|
|
9649
|
-
const skillFile =
|
|
9650
|
-
return
|
|
9889
|
+
const hasGeneratedSkill = existsSync4(projectedSkills) && readdirSync4(projectedSkills).some((name) => {
|
|
9890
|
+
const skillFile = join7(projectedSkills, name, "SKILL.md");
|
|
9891
|
+
return existsSync4(skillFile) && lstatSync6(skillFile).isFile();
|
|
9651
9892
|
});
|
|
9652
9893
|
if (!hasGeneratedSkill) issues.push(`${rootName}/skills contains no BMAD skill configuration`);
|
|
9653
9894
|
}
|
|
@@ -9673,7 +9914,7 @@ var LEGACY_CLI_GITIGNORE_LINES = /* @__PURE__ */ new Set([
|
|
|
9673
9914
|
"# some CLIs get a real directory here and some get a symlink."
|
|
9674
9915
|
]);
|
|
9675
9916
|
function supportedCliGitignoreIssues(repoRoot) {
|
|
9676
|
-
const lines = (safeReadText2(
|
|
9917
|
+
const lines = (safeReadText2(join7(repoRoot, ".gitignore")) ?? "").split(/\r?\n/);
|
|
9677
9918
|
const legacy = lines.filter((line) => LEGACY_CLI_GITIGNORE_LINES.has(line.trim()));
|
|
9678
9919
|
return [
|
|
9679
9920
|
...legacy.length ? [`.gitignore contains ${legacy.length} legacy client-root override line(s); only .agents is canonical`] : [],
|
|
@@ -9681,7 +9922,7 @@ function supportedCliGitignoreIssues(repoRoot) {
|
|
|
9681
9922
|
];
|
|
9682
9923
|
}
|
|
9683
9924
|
function ensureSupportedCliGitignore(ctx) {
|
|
9684
|
-
const path =
|
|
9925
|
+
const path = join7(ctx.repoRoot, ".gitignore");
|
|
9685
9926
|
const current = safeReadText2(path) ?? "";
|
|
9686
9927
|
const kept = current.split(/\r?\n/).filter((line) => {
|
|
9687
9928
|
const trimmed2 = line.trim();
|
|
@@ -9719,10 +9960,10 @@ function createBmadChecks() {
|
|
|
9719
9960
|
fixable: false
|
|
9720
9961
|
};
|
|
9721
9962
|
}
|
|
9722
|
-
const targetRoot =
|
|
9723
|
-
const selectedModules = manifestSelection.status === "valid" ? manifestSelection.modules :
|
|
9963
|
+
const targetRoot = join7(ctx.repoRoot, "_bmad");
|
|
9964
|
+
const selectedModules = manifestSelection.status === "valid" ? manifestSelection.modules : configuredBmadModuleSelection(ctx.repoRoot)?.modules ?? [...DEFAULT_BMAD_MODULES];
|
|
9724
9965
|
const sentinels = requiredBmadSentinels(ctx.repoRoot, selectedModules);
|
|
9725
|
-
const missing = sentinels.filter((file) => !
|
|
9966
|
+
const missing = sentinels.filter((file) => !existsSync4(join7(targetRoot, file)));
|
|
9726
9967
|
const projectNameIssues = bmadProjectNameIssues(ctx.repoRoot);
|
|
9727
9968
|
const details = [
|
|
9728
9969
|
...missing.map((file) => `_bmad/${file}`),
|
|
@@ -9750,10 +9991,21 @@ function createBmadChecks() {
|
|
|
9750
9991
|
details: [manifestSelection.error]
|
|
9751
9992
|
};
|
|
9752
9993
|
}
|
|
9753
|
-
const selectedModules = manifestSelection.status === "valid" ? manifestSelection.modules :
|
|
9994
|
+
const selectedModules = manifestSelection.status === "valid" ? manifestSelection.modules : configuredBmadModuleSelection(ctx.repoRoot)?.modules ?? [...DEFAULT_BMAD_MODULES];
|
|
9995
|
+
const preservation = g33BmadUpdateBoundary(ctx.repoRoot, manifestSelection.status === "valid" && manifestSelection.hasG33 || configuredBmadModuleSelection(ctx.repoRoot)?.hasG33);
|
|
9996
|
+
if (preservation) return {
|
|
9997
|
+
id: finding2.id,
|
|
9998
|
+
title: finding2.title,
|
|
9999
|
+
status: "blocked",
|
|
10000
|
+
summary: preservation.summary,
|
|
10001
|
+
changedFiles: [],
|
|
10002
|
+
details: companionDetails(preservation),
|
|
10003
|
+
bmadCompanion: preservation
|
|
10004
|
+
};
|
|
9754
10005
|
if (ctx.dryRun) {
|
|
9755
10006
|
const sentinels = requiredBmadSentinels(ctx.repoRoot, selectedModules);
|
|
9756
|
-
|
|
10007
|
+
const companion = await runG33Companion(ctx.repoRoot, true, "bmad-install");
|
|
10008
|
+
changedFiles.push(...sentinels.map((file) => join7(ctx.repoRoot, "_bmad", file)).filter((path) => !existsSync4(path)));
|
|
9757
10009
|
changedFiles.push(...bmadProjectNameIssues(ctx.repoRoot).paths);
|
|
9758
10010
|
return {
|
|
9759
10011
|
id: finding2.id,
|
|
@@ -9762,16 +10014,18 @@ function createBmadChecks() {
|
|
|
9762
10014
|
summary: changedFiles.length ? "Would run non-interactive bmad-method install" : "No changes required",
|
|
9763
10015
|
changedFiles,
|
|
9764
10016
|
details: [
|
|
9765
|
-
`Would run: ${bmadInstallDisplay(ctx.repoRoot, selectedModules)}
|
|
9766
|
-
|
|
10017
|
+
`Would run: ${bmadInstallDisplay(ctx.repoRoot, selectedModules)}`,
|
|
10018
|
+
...companionDetails(companion)
|
|
10019
|
+
],
|
|
10020
|
+
bmadCompanion: companion
|
|
9767
10021
|
};
|
|
9768
10022
|
}
|
|
9769
10023
|
const expectedChangedPaths = [
|
|
9770
|
-
...requiredBmadSentinels(ctx.repoRoot, selectedModules).map((file) =>
|
|
10024
|
+
...requiredBmadSentinels(ctx.repoRoot, selectedModules).map((file) => join7(ctx.repoRoot, "_bmad", file)).filter((path) => !existsSync4(path)),
|
|
9771
10025
|
...bmadProjectNameIssues(ctx.repoRoot).paths
|
|
9772
10026
|
];
|
|
9773
10027
|
const evicted = evictLegacyBmadPackState(ctx, changedFiles, await skillActivations(ctx));
|
|
9774
|
-
const install = runBmadInstall(ctx.repoRoot, selectedModules);
|
|
10028
|
+
const install = await runBmadInstall(ctx.repoRoot, selectedModules);
|
|
9775
10029
|
if (!install.ok) {
|
|
9776
10030
|
return {
|
|
9777
10031
|
id: finding2.id,
|
|
@@ -9782,14 +10036,15 @@ function createBmadChecks() {
|
|
|
9782
10036
|
details: [...evicted, install.error ?? "Unknown error"]
|
|
9783
10037
|
};
|
|
9784
10038
|
}
|
|
9785
|
-
changedFiles.push(...expectedChangedPaths.filter(
|
|
10039
|
+
changedFiles.push(...expectedChangedPaths.filter(existsSync4), ...install.companion?.changedFiles ?? []);
|
|
9786
10040
|
return {
|
|
9787
10041
|
id: finding2.id,
|
|
9788
10042
|
title: finding2.title,
|
|
9789
10043
|
status: changedFiles.length ? "applied" : "noop",
|
|
9790
10044
|
summary: changedFiles.length ? "Installed BMAD scaffold via bmad-method" : "No changes required",
|
|
9791
10045
|
changedFiles,
|
|
9792
|
-
details: evicted
|
|
10046
|
+
details: [...evicted, ...install.companion ? companionDetails(install.companion) : []],
|
|
10047
|
+
bmadCompanion: install.companion
|
|
9793
10048
|
};
|
|
9794
10049
|
}
|
|
9795
10050
|
},
|
|
@@ -9803,14 +10058,16 @@ function createBmadChecks() {
|
|
|
9803
10058
|
id: "bmad.version",
|
|
9804
10059
|
title: "BMAD version currency",
|
|
9805
10060
|
status: "skip",
|
|
9806
|
-
summary:
|
|
10061
|
+
summary: existsSync4(join7(ctx.repoRoot, "_bmad")) ? "BMAD installed but version manifest unreadable" : "No BMAD install present",
|
|
9807
10062
|
details: [],
|
|
9808
10063
|
fixable: false
|
|
9809
10064
|
};
|
|
9810
10065
|
}
|
|
9811
10066
|
const pinned = ctx.bmadVersionPin?.trim();
|
|
9812
10067
|
const resolved = pinned ? void 0 : resolveBmadDistTags(ctx.homeDir);
|
|
9813
|
-
const
|
|
10068
|
+
const channelTarget = pinned ? void 0 : bmadChannelTarget(resolved?.distTags);
|
|
10069
|
+
const available = pinned ?? channelTarget?.version;
|
|
10070
|
+
const channel = channelTarget?.channel ?? BMAD_TARGET_CHANNEL;
|
|
9814
10071
|
if (!available) {
|
|
9815
10072
|
return {
|
|
9816
10073
|
id: "bmad.version",
|
|
@@ -9821,7 +10078,7 @@ function createBmadChecks() {
|
|
|
9821
10078
|
fixable: false
|
|
9822
10079
|
};
|
|
9823
10080
|
}
|
|
9824
|
-
const targetLabel = pinned ? `pinned ${available}` : `${
|
|
10081
|
+
const targetLabel = pinned ? `pinned ${available}` : `${channel} ${available}`;
|
|
9825
10082
|
const staleNote = resolved?.stale ? ` ${glyph.dot} cached` : "";
|
|
9826
10083
|
const comparison = compareBmadVersions(installed, available);
|
|
9827
10084
|
if (pinned ? comparison === 0 : comparison >= 0) {
|
|
@@ -9842,7 +10099,7 @@ function createBmadChecks() {
|
|
|
9842
10099
|
title: "BMAD version currency",
|
|
9843
10100
|
status: "skip",
|
|
9844
10101
|
summary: `BMAD ${installed} installed; stable currency floor unknown (${BMAD_NPM_PACKAGE} latest unresolved)`,
|
|
9845
|
-
details: [`installed: ${installed}`, `available: ${available} (${BMAD_NPM_PACKAGE}@${
|
|
10102
|
+
details: [`installed: ${installed}`, `available: ${available} (${BMAD_NPM_PACKAGE}@${channel})`],
|
|
9846
10103
|
fixable: false
|
|
9847
10104
|
};
|
|
9848
10105
|
}
|
|
@@ -9855,7 +10112,7 @@ function createBmadChecks() {
|
|
|
9855
10112
|
summary: `BMAD ${installed} is at or ahead of stable ${stable ?? "unknown"}; ${targetLabel} available${staleNote}`,
|
|
9856
10113
|
details: [
|
|
9857
10114
|
`installed: ${installed}`,
|
|
9858
|
-
`available: ${available} (${BMAD_NPM_PACKAGE}@${
|
|
10115
|
+
`available: ${available} (${BMAD_NPM_PACKAGE}@${channel})`,
|
|
9859
10116
|
stable ? `stable latest: ${stable}` : "",
|
|
9860
10117
|
"run `pj migrate bmad.version` to take the prerelease"
|
|
9861
10118
|
].filter(Boolean),
|
|
@@ -9869,7 +10126,7 @@ function createBmadChecks() {
|
|
|
9869
10126
|
summary: pinnedMismatch ? `BMAD ${installed} does not match ${targetLabel}` : `BMAD ${installed} is behind stable ${stable} \u2014 upgrade available`,
|
|
9870
10127
|
details: [
|
|
9871
10128
|
`installed: ${installed}`,
|
|
9872
|
-
pinned ? `required transaction pin: ${available}` : `available: ${available} (${BMAD_NPM_PACKAGE}@${
|
|
10129
|
+
pinned ? `required transaction pin: ${available}` : `available: ${available} (${BMAD_NPM_PACKAGE}@${channel})`,
|
|
9873
10130
|
!pinned && stable ? `stable latest: ${stable}` : "",
|
|
9874
10131
|
"run `pj migrate bmad.version` to upgrade"
|
|
9875
10132
|
].filter(Boolean),
|
|
@@ -9882,14 +10139,14 @@ function createBmadChecks() {
|
|
|
9882
10139
|
}
|
|
9883
10140
|
{
|
|
9884
10141
|
const current = readInstalledBmadVersion(ctx.repoRoot);
|
|
9885
|
-
const target = ctx.bmadVersionPin?.trim() ?? resolveBmadDistTags(ctx.homeDir)?.distTags?.
|
|
10142
|
+
const target = ctx.bmadVersionPin?.trim() ?? bmadChannelTarget(resolveBmadDistTags(ctx.homeDir)?.distTags)?.version;
|
|
9886
10143
|
if (!current || !target || compareBmadVersions(current, target) === 0) {
|
|
9887
10144
|
return { id: finding2.id, title: finding2.title, status: "noop", summary: "BMAD already current", changedFiles: [], details: [] };
|
|
9888
10145
|
}
|
|
9889
10146
|
}
|
|
9890
10147
|
const installed = readInstalledBmadVersion(ctx.repoRoot);
|
|
9891
|
-
const available = ctx.bmadVersionPin?.trim() ?? resolveBmadDistTags(ctx.homeDir)?.distTags?.
|
|
9892
|
-
const manifestPath =
|
|
10148
|
+
const available = ctx.bmadVersionPin?.trim() ?? bmadChannelTarget(resolveBmadDistTags(ctx.homeDir)?.distTags)?.version;
|
|
10149
|
+
const manifestPath = join7(ctx.repoRoot, "_bmad", "_config", "manifest.yaml");
|
|
9893
10150
|
const manifestSelection = manifestBmadModules(ctx.repoRoot);
|
|
9894
10151
|
if (manifestSelection.status === "invalid") {
|
|
9895
10152
|
return {
|
|
@@ -9901,8 +10158,19 @@ function createBmadChecks() {
|
|
|
9901
10158
|
details: [manifestSelection.error]
|
|
9902
10159
|
};
|
|
9903
10160
|
}
|
|
9904
|
-
const selectedModules = manifestSelection.status === "valid" ? manifestSelection.modules :
|
|
10161
|
+
const selectedModules = manifestSelection.status === "valid" ? manifestSelection.modules : configuredBmadModuleSelection(ctx.repoRoot)?.modules ?? [...DEFAULT_BMAD_MODULES];
|
|
10162
|
+
const preservation = g33BmadUpdateBoundary(ctx.repoRoot, manifestSelection.status === "valid" && manifestSelection.hasG33 || configuredBmadModuleSelection(ctx.repoRoot)?.hasG33);
|
|
10163
|
+
if (preservation) return {
|
|
10164
|
+
id: finding2.id,
|
|
10165
|
+
title: finding2.title,
|
|
10166
|
+
status: "blocked",
|
|
10167
|
+
summary: preservation.summary,
|
|
10168
|
+
changedFiles: [],
|
|
10169
|
+
details: companionDetails(preservation),
|
|
10170
|
+
bmadCompanion: preservation
|
|
10171
|
+
};
|
|
9905
10172
|
if (ctx.dryRun) {
|
|
10173
|
+
const companion = await runG33Companion(ctx.repoRoot, true, "bmad-install");
|
|
9906
10174
|
return {
|
|
9907
10175
|
id: finding2.id,
|
|
9908
10176
|
title: finding2.title,
|
|
@@ -9910,11 +10178,13 @@ function createBmadChecks() {
|
|
|
9910
10178
|
summary: `Would upgrade BMAD ${installed ?? "?"} -> ${available ?? BMAD_TARGET_CHANNEL}`,
|
|
9911
10179
|
changedFiles: [manifestPath],
|
|
9912
10180
|
details: [
|
|
9913
|
-
`Would run: ${bmadInstallDisplay(ctx.repoRoot, selectedModules, available ?? BMAD_TARGET_CHANNEL)}
|
|
9914
|
-
|
|
10181
|
+
`Would run: ${bmadInstallDisplay(ctx.repoRoot, selectedModules, available ?? BMAD_TARGET_CHANNEL)}`,
|
|
10182
|
+
...companionDetails(companion)
|
|
10183
|
+
],
|
|
10184
|
+
bmadCompanion: companion
|
|
9915
10185
|
};
|
|
9916
10186
|
}
|
|
9917
|
-
const install = runBmadInstall(ctx.repoRoot, selectedModules, available ?? BMAD_TARGET_CHANNEL);
|
|
10187
|
+
const install = await runBmadInstall(ctx.repoRoot, selectedModules, available ?? BMAD_TARGET_CHANNEL);
|
|
9918
10188
|
if (!install.ok) {
|
|
9919
10189
|
return {
|
|
9920
10190
|
id: finding2.id,
|
|
@@ -9931,7 +10201,8 @@ function createBmadChecks() {
|
|
|
9931
10201
|
const upgraded = Boolean(nowInstalled && installed && compareBmadVersions(nowInstalled, installed) > 0);
|
|
9932
10202
|
const changed = Array.from(/* @__PURE__ */ new Set([
|
|
9933
10203
|
...upgraded ? [manifestPath] : [],
|
|
9934
|
-
...evictedChanges
|
|
10204
|
+
...evictedChanges,
|
|
10205
|
+
...install.companion?.changedFiles ?? []
|
|
9935
10206
|
]));
|
|
9936
10207
|
return {
|
|
9937
10208
|
id: finding2.id,
|
|
@@ -9939,7 +10210,8 @@ function createBmadChecks() {
|
|
|
9939
10210
|
status: changed.length ? "applied" : "noop",
|
|
9940
10211
|
summary: upgraded ? `Upgraded BMAD ${installed} -> ${nowInstalled}` : `BMAD reinstalled (${nowInstalled ?? "?"})`,
|
|
9941
10212
|
changedFiles: changed,
|
|
9942
|
-
details: evicted
|
|
10213
|
+
details: [...evicted, ...install.companion ? companionDetails(install.companion) : []],
|
|
10214
|
+
bmadCompanion: install.companion
|
|
9943
10215
|
};
|
|
9944
10216
|
}
|
|
9945
10217
|
},
|
|
@@ -9948,7 +10220,7 @@ function createBmadChecks() {
|
|
|
9948
10220
|
title: "Supported BMAD CLI projection roots",
|
|
9949
10221
|
audit: (ctx) => {
|
|
9950
10222
|
const unsupportedNames = Object.keys(UNSUPPORTED_BMAD_ROOTS);
|
|
9951
|
-
const present = unsupportedNames.filter((name) =>
|
|
10223
|
+
const present = unsupportedNames.filter((name) => existsSync4(join7(ctx.repoRoot, name)));
|
|
9952
10224
|
const attestations = present.map((name) => ({ name, ...unsupportedRootAttestation(ctx.repoRoot, name) }));
|
|
9953
10225
|
const plan = planSkillRoots(ctx.repoRoot);
|
|
9954
10226
|
const supportedIssues = supportedCliProjectionIssues(ctx.repoRoot, plan);
|
|
@@ -9970,7 +10242,7 @@ function createBmadChecks() {
|
|
|
9970
10242
|
},
|
|
9971
10243
|
migrate: (ctx, finding2) => {
|
|
9972
10244
|
const unsupportedNames = Object.keys(UNSUPPORTED_BMAD_ROOTS);
|
|
9973
|
-
const present = unsupportedNames.filter((name) =>
|
|
10245
|
+
const present = unsupportedNames.filter((name) => existsSync4(join7(ctx.repoRoot, name)));
|
|
9974
10246
|
const attestations = present.map((name) => ({ name, ...unsupportedRootAttestation(ctx.repoRoot, name) }));
|
|
9975
10247
|
const blocked = attestations.filter((entry) => !entry.safe);
|
|
9976
10248
|
if (blocked.length) {
|
|
@@ -9995,7 +10267,7 @@ function createBmadChecks() {
|
|
|
9995
10267
|
};
|
|
9996
10268
|
}
|
|
9997
10269
|
const gitignoreChanges = ensureSupportedCliGitignore(ctx);
|
|
9998
|
-
const removedRoots = attestations.map((entry) =>
|
|
10270
|
+
const removedRoots = attestations.map((entry) => join7(ctx.repoRoot, entry.name));
|
|
9999
10271
|
if (!ctx.dryRun) for (const path of removedRoots) rmSync2(path, { recursive: true, force: true });
|
|
10000
10272
|
const changedFiles = [.../* @__PURE__ */ new Set([...projectionResult.changedFiles, ...gitignoreChanges, ...removedRoots])].sort();
|
|
10001
10273
|
return {
|
|
@@ -10054,11 +10326,11 @@ function linkedPlaneBoard(ctx) {
|
|
|
10054
10326
|
return { boardId, workspace };
|
|
10055
10327
|
}
|
|
10056
10328
|
function runPx(args, ctx) {
|
|
10057
|
-
const probe =
|
|
10329
|
+
const probe = spawnSync4("which", ["px"], { encoding: "utf8" });
|
|
10058
10330
|
if (probe.status !== 0) {
|
|
10059
10331
|
return { error: "px not found on PATH \u2014 install Pilot to manage board schemas" };
|
|
10060
10332
|
}
|
|
10061
|
-
const result2 =
|
|
10333
|
+
const result2 = spawnSync4("px", args, {
|
|
10062
10334
|
cwd: ctx.repoRoot,
|
|
10063
10335
|
encoding: "utf8",
|
|
10064
10336
|
shell: false,
|
|
@@ -10248,11 +10520,12 @@ var AgentHooksRecipe = class extends Recipe {
|
|
|
10248
10520
|
|
|
10249
10521
|
// src/recipes/BmadRecipe.ts
|
|
10250
10522
|
var BmadRecipe = class extends Recipe {
|
|
10251
|
-
|
|
10523
|
+
requiredChecks = createBmadChecks();
|
|
10524
|
+
checks = [...this.requiredChecks, createG33CompanionCheck()];
|
|
10252
10525
|
metadata = {
|
|
10253
10526
|
id: "bmad",
|
|
10254
10527
|
name: "bmad",
|
|
10255
|
-
description: "BMAD methodology and the
|
|
10528
|
+
description: "BMAD methodology, supported CLI projections, and the optional owner-provided g33 companion",
|
|
10256
10529
|
dependencies: ["agent-hooks"],
|
|
10257
10530
|
commands: [],
|
|
10258
10531
|
publicRuleIds: this.checks.map((check) => check.id)
|
|
@@ -10260,8 +10533,31 @@ var BmadRecipe = class extends Recipe {
|
|
|
10260
10533
|
constructor(context) {
|
|
10261
10534
|
super(context);
|
|
10262
10535
|
}
|
|
10263
|
-
init(ctx, _input) {
|
|
10264
|
-
|
|
10536
|
+
async init(ctx, _input) {
|
|
10537
|
+
const companions = [];
|
|
10538
|
+
const checks = this.requiredChecks.map((check) => ({
|
|
10539
|
+
...check,
|
|
10540
|
+
migrate: async (...args) => {
|
|
10541
|
+
const result2 = await check.migrate(...args);
|
|
10542
|
+
if (result2.bmadCompanion) companions.push(result2.bmadCompanion);
|
|
10543
|
+
return result2;
|
|
10544
|
+
}
|
|
10545
|
+
}));
|
|
10546
|
+
const required = await this.initializeOwnedChecks(ctx, checks);
|
|
10547
|
+
if (!required.ok) return { ...required, ...companions.length ? { bmadCompanion: companions.at(-1) } : {} };
|
|
10548
|
+
const companion = companions.at(-1) ?? await runG33Companion(ctx.repoRoot, ctx.dryRun);
|
|
10549
|
+
return {
|
|
10550
|
+
...required,
|
|
10551
|
+
bmadCompanion: companion,
|
|
10552
|
+
changedFiles: [.../* @__PURE__ */ new Set([...required.changedFiles, ...companion.changedFiles])].sort(),
|
|
10553
|
+
logs: [...required.logs, ...companionDetails(companion)],
|
|
10554
|
+
phases: [...required.phases, {
|
|
10555
|
+
id: "bmad.g33-companion",
|
|
10556
|
+
status: companion.status === "planned" ? "planned" : companion.status === "changed" ? "changed" : companion.status === "unchanged" ? "unchanged" : "skipped",
|
|
10557
|
+
message: `Optional g33 (${companion.status}): ${companion.summary}`,
|
|
10558
|
+
changedFiles: companion.changedFiles
|
|
10559
|
+
}]
|
|
10560
|
+
};
|
|
10265
10561
|
}
|
|
10266
10562
|
printNextSteps() {
|
|
10267
10563
|
console.log(`BMAD lifecycle initialized for the ${SUPPORTED_CLIS.length} supported CLIs.`);
|
|
@@ -10585,24 +10881,24 @@ var NodeRecipe = class extends Recipe {
|
|
|
10585
10881
|
|
|
10586
10882
|
// src/recipes/ProjectRecipe.ts
|
|
10587
10883
|
init_project();
|
|
10588
|
-
import { spawnSync as
|
|
10589
|
-
import { existsSync as existsSync14, lstatSync as
|
|
10590
|
-
import { dirname as dirname14, isAbsolute as
|
|
10884
|
+
import { spawnSync as spawnSync12 } from "node:child_process";
|
|
10885
|
+
import { existsSync as existsSync14, lstatSync as lstatSync13, readFileSync as readFileSync15, rmSync as rmSync5 } from "node:fs";
|
|
10886
|
+
import { dirname as dirname14, isAbsolute as isAbsolute9, join as join19, relative as relativePath, resolve as resolve18 } from "node:path";
|
|
10591
10887
|
init_tree_diff();
|
|
10592
10888
|
|
|
10593
10889
|
// src/recipes/NotebookRecipe.ts
|
|
10594
10890
|
init_project();
|
|
10595
|
-
import { existsSync as existsSync13, readFileSync as
|
|
10596
|
-
import { join as
|
|
10891
|
+
import { existsSync as existsSync13, readFileSync as readFileSync14 } from "node:fs";
|
|
10892
|
+
import { join as join18 } from "node:path";
|
|
10597
10893
|
|
|
10598
10894
|
// src/notebook/checks.ts
|
|
10599
10895
|
init_config();
|
|
10600
10896
|
init_notes();
|
|
10601
10897
|
init_output();
|
|
10602
10898
|
init_state();
|
|
10603
|
-
import { spawnSync as
|
|
10604
|
-
import { existsSync as existsSync10, readFileSync as
|
|
10605
|
-
import { join as
|
|
10899
|
+
import { spawnSync as spawnSync6 } from "node:child_process";
|
|
10900
|
+
import { existsSync as existsSync10, readFileSync as readFileSync12 } from "node:fs";
|
|
10901
|
+
import { join as join14 } from "node:path";
|
|
10606
10902
|
var NOTEBOOK_RULE_IDS = [
|
|
10607
10903
|
"notebook.configuration",
|
|
10608
10904
|
"notebook.binding",
|
|
@@ -10619,10 +10915,10 @@ function result(check, status, summary, changedFiles = [], details = []) {
|
|
|
10619
10915
|
return { id: check.id, title: check.title, status, summary, changedFiles, details };
|
|
10620
10916
|
}
|
|
10621
10917
|
function manifestNotebook(repo) {
|
|
10622
|
-
const path =
|
|
10918
|
+
const path = join14(repo, ".project.json");
|
|
10623
10919
|
if (!existsSync10(path)) return null;
|
|
10624
10920
|
try {
|
|
10625
|
-
const parsed = JSON.parse(
|
|
10921
|
+
const parsed = JSON.parse(readFileSync12(path, "utf8"));
|
|
10626
10922
|
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return null;
|
|
10627
10923
|
const notebook = parsed.notebook;
|
|
10628
10924
|
return notebook && typeof notebook === "object" && !Array.isArray(notebook) ? notebook : null;
|
|
@@ -10851,10 +11147,10 @@ function captureAudit(ctx) {
|
|
|
10851
11147
|
function overviewMigrate(ctx, current) {
|
|
10852
11148
|
if (ctx.dryRun) return result(current, "applied", "Would reconcile drifted Project Overview note");
|
|
10853
11149
|
if (!ctx.live) return result(current, "blocked", "Reconciling the remote Overview note requires --live");
|
|
10854
|
-
const pjanglerEntry = ctx.pjanglerRoot ?
|
|
11150
|
+
const pjanglerEntry = ctx.pjanglerRoot ? join14(ctx.pjanglerRoot, "dist", "index.js") : null;
|
|
10855
11151
|
const entry = pjanglerEntry && existsSync10(pjanglerEntry) ? pjanglerEntry : "pj";
|
|
10856
11152
|
const cmd = entry === "pj" ? ["pj"] : [process.execPath, entry];
|
|
10857
|
-
const spawned =
|
|
11153
|
+
const spawned = spawnSync6(cmd[0], [...cmd.slice(1), "notebook", "overview", ctx.repoRoot], {
|
|
10858
11154
|
encoding: "utf8",
|
|
10859
11155
|
timeout: 1e4,
|
|
10860
11156
|
env: { ...process.env, ...ctx.registryPath ? { PJ_PROJECT_REGISTRY: ctx.registryPath } : {} }
|
|
@@ -10899,7 +11195,7 @@ init_project();
|
|
|
10899
11195
|
init_config();
|
|
10900
11196
|
init_open_notebook_client();
|
|
10901
11197
|
init_notes();
|
|
10902
|
-
import { resolve as
|
|
11198
|
+
import { resolve as resolve17 } from "node:path";
|
|
10903
11199
|
|
|
10904
11200
|
// src/notebook/journal-key.ts
|
|
10905
11201
|
init_types();
|
|
@@ -10933,18 +11229,18 @@ function journalNoteOperationId(period, key) {
|
|
|
10933
11229
|
init_notes();
|
|
10934
11230
|
init_git_evidence();
|
|
10935
11231
|
init_types();
|
|
10936
|
-
import { spawnSync as
|
|
11232
|
+
import { spawnSync as spawnSync8 } from "node:child_process";
|
|
10937
11233
|
import { realpathSync as realpathSync8 } from "node:fs";
|
|
10938
|
-
import { relative as relative10, resolve as
|
|
11234
|
+
import { relative as relative10, resolve as resolve14, sep as sep5 } from "node:path";
|
|
10939
11235
|
var DEFAULT_OVERVIEW_REFERENCES = [".project.json", "README.md", "AGENTS.md", "CLAUDE.md", "docs/architecture.md"];
|
|
10940
11236
|
function git2(repo, args, timeout) {
|
|
10941
|
-
const result2 =
|
|
11237
|
+
const result2 = spawnSync8("git", args, { cwd: repo, encoding: "utf8", maxBuffer: 1024 * 1024, timeout, shell: false });
|
|
10942
11238
|
return { ok: result2.status === 0, stdout: result2.stdout?.trim() ?? "" };
|
|
10943
11239
|
}
|
|
10944
11240
|
function normalizedReference(repo, value) {
|
|
10945
11241
|
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}`);
|
|
10946
11242
|
const root = realpathSync8(repo);
|
|
10947
|
-
const candidate =
|
|
11243
|
+
const candidate = resolve14(root, value);
|
|
10948
11244
|
const rel = relative10(root, candidate).split(sep5).join("/");
|
|
10949
11245
|
if (!rel || rel === ".." || rel.startsWith("../")) throw new NotebookError("INVALID_INPUT", `Overview reference escapes the repository: ${value}`);
|
|
10950
11246
|
return rel.normalize("NFC");
|
|
@@ -11077,21 +11373,21 @@ init_git_evidence();
|
|
|
11077
11373
|
init_notes();
|
|
11078
11374
|
init_state();
|
|
11079
11375
|
init_types();
|
|
11080
|
-
import { spawn, spawnSync as
|
|
11376
|
+
import { spawn, spawnSync as spawnSync9 } from "node:child_process";
|
|
11081
11377
|
import { createHash as createHash8, randomUUID as randomUUID5 } from "node:crypto";
|
|
11082
|
-
import { chmodSync as chmodSync4, closeSync as closeSync5, constants as constants4, copyFileSync as copyFileSync3, existsSync as existsSync11, fstatSync as fstatSync3, fsyncSync as fsyncSync4, lstatSync as
|
|
11083
|
-
import { basename as basename8, dirname as dirname13, isAbsolute as
|
|
11378
|
+
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";
|
|
11379
|
+
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";
|
|
11084
11380
|
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
|
11085
11381
|
function bundledSkillCandidates() {
|
|
11086
11382
|
const candidates = [];
|
|
11087
11383
|
let cursor = dirname13(fileURLToPath3(import.meta.url));
|
|
11088
11384
|
for (let depth = 0; depth < 8; depth++) {
|
|
11089
|
-
candidates.push(
|
|
11385
|
+
candidates.push(join17(cursor, "dist", "assets", "project-notebook-skill"), join17(cursor, "assets", "project-notebook-skill"));
|
|
11090
11386
|
const parent = dirname13(cursor);
|
|
11091
11387
|
if (parent === cursor) break;
|
|
11092
11388
|
cursor = parent;
|
|
11093
11389
|
}
|
|
11094
|
-
return [...new Set(candidates.map((candidate) =>
|
|
11390
|
+
return [...new Set(candidates.map((candidate) => resolve15(candidate)))];
|
|
11095
11391
|
}
|
|
11096
11392
|
function safeSkillRelativePath(value) {
|
|
11097
11393
|
if (!value || value.startsWith("/") || value.includes("\0")) return false;
|
|
@@ -11104,14 +11400,14 @@ function assertOwnedSkillTree(source) {
|
|
|
11104
11400
|
assertNoSymlinkComponents(source);
|
|
11105
11401
|
const uid = typeof process.getuid === "function" ? process.getuid() : void 0;
|
|
11106
11402
|
const walk = (directory) => {
|
|
11107
|
-
const directoryStat =
|
|
11403
|
+
const directoryStat = lstatSync12(directory);
|
|
11108
11404
|
if (!directoryStat.isDirectory() || directoryStat.isSymbolicLink()) throw new NotebookError("CONFLICT", "Project Notebook skill source contains a non-directory component");
|
|
11109
11405
|
if (uid !== void 0 && directoryStat.uid !== uid) throw new NotebookError("CONFLICT", "Project Notebook skill source is not owned by the current user");
|
|
11110
11406
|
if (directoryStat.mode & 3586) throw new NotebookError("CONFLICT", "Project Notebook skill source has unsafe directory mode bits");
|
|
11111
11407
|
for (const entry of readdirSync7(directory, { withFileTypes: true })) {
|
|
11112
11408
|
if (entry.isDirectory() && (entry.name === "__pycache__" || entry.name === ".git" || entry.name === "node_modules")) continue;
|
|
11113
|
-
const path =
|
|
11114
|
-
const stat =
|
|
11409
|
+
const path = join17(directory, entry.name);
|
|
11410
|
+
const stat = lstatSync12(path);
|
|
11115
11411
|
if (stat.isSymbolicLink()) throw new NotebookError("CONFLICT", "Project Notebook skill source contains a symlink");
|
|
11116
11412
|
if (entry.isDirectory()) walk(path);
|
|
11117
11413
|
else if (!entry.isFile()) throw new NotebookError("CONFLICT", "Project Notebook skill source contains a non-regular entry");
|
|
@@ -11128,16 +11424,16 @@ function enumerateSkillPayload(source) {
|
|
|
11128
11424
|
const walk = (directory) => {
|
|
11129
11425
|
for (const entry of readdirSync7(directory, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name, "en"))) {
|
|
11130
11426
|
if (entry.isDirectory() && (entry.name === "__pycache__" || entry.name === ".git" || entry.name === "node_modules")) continue;
|
|
11131
|
-
const path =
|
|
11427
|
+
const path = join17(directory, entry.name);
|
|
11132
11428
|
const rel = relative11(source, path).split(sep6).join("/");
|
|
11133
11429
|
if (!rel.includes("/") && (rel === "export-manifest.json" || rel === "SHA256SUMS" || rel === ".source.yaml")) continue;
|
|
11134
11430
|
if (!safeSkillRelativePath(rel)) throw new NotebookError("CONFLICT", `Project Notebook skill export path is unsafe: ${rel}`);
|
|
11135
|
-
const stat =
|
|
11431
|
+
const stat = lstatSync12(path);
|
|
11136
11432
|
if (stat.isSymbolicLink()) throw new NotebookError("CONFLICT", "Project Notebook skill export contains a symlink");
|
|
11137
11433
|
if (entry.isDirectory()) walk(path);
|
|
11138
11434
|
else if (entry.isFile()) {
|
|
11139
11435
|
const expectedMode = rel.endsWith(".sh") || rel.startsWith("scripts/") ? "0755" : "0644";
|
|
11140
|
-
result2.push({ path: rel, sha256: createHash8("sha256").update(
|
|
11436
|
+
result2.push({ path: rel, sha256: createHash8("sha256").update(readFileSync13(path)).digest("hex"), mode: expectedMode });
|
|
11141
11437
|
} else throw new NotebookError("CONFLICT", "Project Notebook skill export contains a non-regular entry");
|
|
11142
11438
|
}
|
|
11143
11439
|
};
|
|
@@ -11145,9 +11441,9 @@ function enumerateSkillPayload(source) {
|
|
|
11145
11441
|
return result2.sort((a, b) => a.path.localeCompare(b.path, "en"));
|
|
11146
11442
|
}
|
|
11147
11443
|
function parsePackedManifest(source) {
|
|
11148
|
-
const manifestPath =
|
|
11444
|
+
const manifestPath = join17(source, "export-manifest.json");
|
|
11149
11445
|
if (!existsSync11(manifestPath)) return null;
|
|
11150
|
-
const value = JSON.parse(
|
|
11446
|
+
const value = JSON.parse(readFileSync13(manifestPath, "utf8"));
|
|
11151
11447
|
if (!value || typeof value !== "object" || Array.isArray(value)) throw new NotebookError("CONFLICT", "Project Notebook skill export manifest is invalid");
|
|
11152
11448
|
const manifest = value;
|
|
11153
11449
|
if (manifest.schema_version !== 1 || manifest.skill !== "project-notebook" || !Array.isArray(manifest.files)) throw new NotebookError("CONFLICT", "Project Notebook skill export manifest is incompatible");
|
|
@@ -11156,10 +11452,10 @@ function parsePackedManifest(source) {
|
|
|
11156
11452
|
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");
|
|
11157
11453
|
if (!safeSkillRelativePath(entry.path) || paths.has(entry.path)) throw new NotebookError("CONFLICT", "Project Notebook skill export path is unsafe or duplicated");
|
|
11158
11454
|
paths.add(entry.path);
|
|
11159
|
-
const path =
|
|
11160
|
-
const stat =
|
|
11455
|
+
const path = join17(source, ...entry.path.split("/"));
|
|
11456
|
+
const stat = lstatSync12(path);
|
|
11161
11457
|
if (!stat.isFile() || stat.isSymbolicLink()) throw new NotebookError("CONFLICT", "Project Notebook skill export contains a non-regular entry");
|
|
11162
|
-
const actual = createHash8("sha256").update(
|
|
11458
|
+
const actual = createHash8("sha256").update(readFileSync13(path)).digest("hex");
|
|
11163
11459
|
if (actual !== entry.sha256) throw new NotebookError("CONFLICT", `Project Notebook skill digest mismatch: ${entry.path}`);
|
|
11164
11460
|
const actualMode = stat.mode & 511;
|
|
11165
11461
|
const executable = entry.mode === "0755";
|
|
@@ -11171,12 +11467,12 @@ function parsePackedManifest(source) {
|
|
|
11171
11467
|
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");
|
|
11172
11468
|
const sums = `${manifest.files.map((entry) => `${entry.sha256} ${entry.path}`).join("\n")}
|
|
11173
11469
|
`;
|
|
11174
|
-
if (!existsSync11(
|
|
11470
|
+
if (!existsSync11(join17(source, "SHA256SUMS")) || readFileSync13(join17(source, "SHA256SUMS"), "utf8") !== sums) throw new NotebookError("CONFLICT", "Project Notebook skill SHA256SUMS is missing or stale");
|
|
11175
11471
|
return manifest;
|
|
11176
11472
|
}
|
|
11177
11473
|
function expectedPackedSkill() {
|
|
11178
11474
|
for (const candidate of bundledSkillCandidates()) {
|
|
11179
|
-
if (!existsSync11(
|
|
11475
|
+
if (!existsSync11(join17(candidate, "export-manifest.json"))) continue;
|
|
11180
11476
|
assertOwnedSkillTree(candidate);
|
|
11181
11477
|
const manifest = parsePackedManifest(candidate);
|
|
11182
11478
|
if (!manifest) continue;
|
|
@@ -11186,7 +11482,7 @@ function expectedPackedSkill() {
|
|
|
11186
11482
|
throw new NotebookError("NOT_CONFIGURED", "PJ\xE1ngler package has no verified Project Notebook skill export");
|
|
11187
11483
|
}
|
|
11188
11484
|
function isCanonicalSkillexProjectionPath(path) {
|
|
11189
|
-
return
|
|
11485
|
+
return resolve15(path).split(sep6).join("/").endsWith("/all-skills/project-notebook");
|
|
11190
11486
|
}
|
|
11191
11487
|
function isVerifiedCanonicalSkillexProjection(path) {
|
|
11192
11488
|
if (!isCanonicalSkillexProjectionPath(path)) return false;
|
|
@@ -11206,7 +11502,7 @@ function describeProjectNotebookSkillDrift(source) {
|
|
|
11206
11502
|
}
|
|
11207
11503
|
let actual;
|
|
11208
11504
|
try {
|
|
11209
|
-
const absolute =
|
|
11505
|
+
const absolute = resolve15(source);
|
|
11210
11506
|
const packed = parsePackedManifest(absolute);
|
|
11211
11507
|
actual = packed?.files ?? enumerateSkillPayload(absolute);
|
|
11212
11508
|
} catch (error) {
|
|
@@ -11230,7 +11526,7 @@ function describeProjectNotebookSkillDrift(source) {
|
|
|
11230
11526
|
return drift.length ? drift.slice(0, 20) : ["no per-file drift detected; the export enumeration itself differs"];
|
|
11231
11527
|
}
|
|
11232
11528
|
function verifyProjectNotebookSkillExport(source) {
|
|
11233
|
-
const absolute =
|
|
11529
|
+
const absolute = resolve15(source);
|
|
11234
11530
|
assertOwnedSkillTree(absolute);
|
|
11235
11531
|
const packed = parsePackedManifest(absolute);
|
|
11236
11532
|
const actual = packed ?? { schema_version: 1, skill: "project-notebook", files: enumerateSkillPayload(absolute) };
|
|
@@ -11242,14 +11538,14 @@ function verifyProjectNotebookSkillExport(source) {
|
|
|
11242
11538
|
}
|
|
11243
11539
|
function resolveProjectNotebookSkillSource(env2 = process.env) {
|
|
11244
11540
|
if (env2.PJ_PROJECT_NOTEBOOK_SKILL_ROOT) {
|
|
11245
|
-
const explicit =
|
|
11246
|
-
if (!existsSync11(
|
|
11541
|
+
const explicit = resolve15(env2.PJ_PROJECT_NOTEBOOK_SKILL_ROOT);
|
|
11542
|
+
if (!existsSync11(join17(explicit, "SKILL.md"))) throw new NotebookError("NOT_CONFIGURED", "Configured Project Notebook skill source is unavailable");
|
|
11247
11543
|
verifyProjectNotebookSkillExport(explicit);
|
|
11248
11544
|
return explicit;
|
|
11249
11545
|
}
|
|
11250
11546
|
if (env2.PJ_SKILLS_REGISTRY_ROOT) {
|
|
11251
|
-
const canonical =
|
|
11252
|
-
if (existsSync11(
|
|
11547
|
+
const canonical = resolve15(env2.PJ_SKILLS_REGISTRY_ROOT, "all-skills", "project-notebook");
|
|
11548
|
+
if (existsSync11(join17(canonical, "SKILL.md"))) {
|
|
11253
11549
|
verifyProjectNotebookSkillExport(canonical);
|
|
11254
11550
|
return canonical;
|
|
11255
11551
|
}
|
|
@@ -11264,7 +11560,7 @@ var REPAIR_COMMAND = "pj notebook skill --apply";
|
|
|
11264
11560
|
function probeCanonicalSkillexRootProjection(skillsRoot, link) {
|
|
11265
11561
|
let rootLink;
|
|
11266
11562
|
try {
|
|
11267
|
-
rootLink =
|
|
11563
|
+
rootLink = lstatSync12(skillsRoot);
|
|
11268
11564
|
} catch (error) {
|
|
11269
11565
|
if (error.code === "ENOENT") return { state: "absent" };
|
|
11270
11566
|
throw error;
|
|
@@ -11277,15 +11573,15 @@ function probeCanonicalSkillexRootProjection(skillsRoot, link) {
|
|
|
11277
11573
|
if (uid !== void 0 && rootLink.uid !== uid) throw new Error("owner");
|
|
11278
11574
|
const globalRoot = realpathSync9(skillsRoot);
|
|
11279
11575
|
assertNoSymlinkComponents(globalRoot);
|
|
11280
|
-
const globalStat =
|
|
11576
|
+
const globalStat = lstatSync12(globalRoot);
|
|
11281
11577
|
if (!globalStat.isDirectory() || globalStat.isSymbolicLink()) throw new Error("root-type");
|
|
11282
11578
|
if (uid !== void 0 && globalStat.uid !== uid) throw new Error("root-owner");
|
|
11283
11579
|
if (globalStat.mode & 3586) throw new Error("root-mode");
|
|
11284
11580
|
const skillSetsRoot = dirname13(globalRoot);
|
|
11285
11581
|
if (basename8(globalRoot) !== "global" || basename8(skillSetsRoot) !== "skill-sets") throw new Error("layout");
|
|
11286
11582
|
const checkoutRoot = dirname13(skillSetsRoot);
|
|
11287
|
-
const expectedSource =
|
|
11288
|
-
const linkStat =
|
|
11583
|
+
const expectedSource = join17(checkoutRoot, "all-skills", "project-notebook");
|
|
11584
|
+
const linkStat = lstatSync12(link);
|
|
11289
11585
|
if (!linkStat.isSymbolicLink()) throw new Error("projection-type");
|
|
11290
11586
|
if (uid !== void 0 && linkStat.uid !== uid) throw new Error("projection-owner");
|
|
11291
11587
|
const projectedSource = realpathSync9(link);
|
|
@@ -11321,28 +11617,28 @@ function installPackagedProjectNotebookSkill(input = {}) {
|
|
|
11321
11617
|
const manifest = verifyProjectNotebookSkillExport(source);
|
|
11322
11618
|
const digest = createHash8("sha256").update(JSON.stringify(manifest)).digest("hex");
|
|
11323
11619
|
const home = env2.HOME;
|
|
11324
|
-
if (!home || !
|
|
11325
|
-
const skillsRoot =
|
|
11326
|
-
const link =
|
|
11620
|
+
if (!home || !resolve15(home).startsWith("/")) throw new NotebookError("NOT_CONFIGURED", "A trusted HOME is required to install the Project Notebook skill");
|
|
11621
|
+
const skillsRoot = join17(home, ".agents", "skills");
|
|
11622
|
+
const link = join17(skillsRoot, "project-notebook");
|
|
11327
11623
|
const probe = probeCanonicalSkillexRootProjection(skillsRoot, link);
|
|
11328
11624
|
if (probe.state === "adopted") return { installed: false, path: link, digest };
|
|
11329
11625
|
if (probe.state === "declined") return { installed: false, path: link, digest, blocked: probe.block };
|
|
11330
|
-
const dataRoot =
|
|
11331
|
-
const payload =
|
|
11626
|
+
const dataRoot = resolve15(env2.XDG_DATA_HOME || join17(home, ".local", "share"), "pjangler", "skills", "project-notebook");
|
|
11627
|
+
const payload = join17(dataRoot, `${PJANGLER_VERSION}-${digest}`);
|
|
11332
11628
|
assertNoSymlinkComponents(dirname13(dataRoot), true);
|
|
11333
11629
|
mkdirSync6(dataRoot, { recursive: true, mode: 448 });
|
|
11334
11630
|
assertNoSymlinkComponents(dataRoot);
|
|
11335
|
-
const dataStat =
|
|
11631
|
+
const dataStat = lstatSync12(dataRoot);
|
|
11336
11632
|
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");
|
|
11337
11633
|
chmodSync4(dataRoot, 448);
|
|
11338
11634
|
if (!existsSync11(payload)) {
|
|
11339
|
-
const staging =
|
|
11635
|
+
const staging = join17(dataRoot, `.staging-${randomUUID5()}`);
|
|
11340
11636
|
mkdirSync6(staging, { recursive: false, mode: 448 });
|
|
11341
11637
|
try {
|
|
11342
11638
|
for (const entry of manifest.files) {
|
|
11343
|
-
const destination =
|
|
11639
|
+
const destination = join17(staging, ...entry.path.split("/"));
|
|
11344
11640
|
mkdirSync6(dirname13(destination), { recursive: true, mode: 493 });
|
|
11345
|
-
copyFileSync3(
|
|
11641
|
+
copyFileSync3(join17(source, ...entry.path.split("/")), destination);
|
|
11346
11642
|
const mode = entry.mode === "0755" ? 493 : 420;
|
|
11347
11643
|
chmodSync4(destination, mode);
|
|
11348
11644
|
const fd = openSync5(destination, constants4.O_RDONLY | (constants4.O_NOFOLLOW ?? 0));
|
|
@@ -11354,9 +11650,9 @@ function installPackagedProjectNotebookSkill(input = {}) {
|
|
|
11354
11650
|
closeSync5(fd);
|
|
11355
11651
|
}
|
|
11356
11652
|
}
|
|
11357
|
-
writeFileSync7(
|
|
11653
|
+
writeFileSync7(join17(staging, "export-manifest.json"), `${JSON.stringify(manifest, null, 2)}
|
|
11358
11654
|
`, { mode: 420, flag: "wx" });
|
|
11359
|
-
writeFileSync7(
|
|
11655
|
+
writeFileSync7(join17(staging, "SHA256SUMS"), `${manifest.files.map((entry) => `${entry.sha256} ${entry.path}`).join("\n")}
|
|
11360
11656
|
`, { mode: 420, flag: "wx" });
|
|
11361
11657
|
verifyProjectNotebookSkillExport(staging);
|
|
11362
11658
|
renameSync6(staging, payload);
|
|
@@ -11364,7 +11660,7 @@ function installPackagedProjectNotebookSkill(input = {}) {
|
|
|
11364
11660
|
if (existsSync11(staging)) rmSync4(staging, { recursive: true, force: true });
|
|
11365
11661
|
}
|
|
11366
11662
|
} else {
|
|
11367
|
-
const stat =
|
|
11663
|
+
const stat = lstatSync12(payload);
|
|
11368
11664
|
if (!stat.isDirectory() || stat.isSymbolicLink()) throw new NotebookError("CONFLICT", "Installed Project Notebook payload is not a real directory");
|
|
11369
11665
|
verifyProjectNotebookSkillExport(payload);
|
|
11370
11666
|
}
|
|
@@ -11374,7 +11670,7 @@ function installPackagedProjectNotebookSkill(input = {}) {
|
|
|
11374
11670
|
let linkExists = existsSync11(link);
|
|
11375
11671
|
if (!linkExists) {
|
|
11376
11672
|
try {
|
|
11377
|
-
|
|
11673
|
+
lstatSync12(link);
|
|
11378
11674
|
linkExists = true;
|
|
11379
11675
|
} catch {
|
|
11380
11676
|
}
|
|
@@ -11391,7 +11687,7 @@ function installPackagedProjectNotebookSkill(input = {}) {
|
|
|
11391
11687
|
repair: REPAIR_COMMAND
|
|
11392
11688
|
}
|
|
11393
11689
|
});
|
|
11394
|
-
const stat =
|
|
11690
|
+
const stat = lstatSync12(link);
|
|
11395
11691
|
if (!stat.isSymbolicLink()) return foreign(["the path is a real file or directory, not a PJ\xE1ngler-owned link"]);
|
|
11396
11692
|
const target = realpathSync9(link);
|
|
11397
11693
|
if (target !== realpathSync9(payload) && !isVerifiedCanonicalSkillexProjection(target)) {
|
|
@@ -11425,9 +11721,9 @@ function collectSupersededPayloads(dataRoot, keep) {
|
|
|
11425
11721
|
}
|
|
11426
11722
|
for (const name of entries) {
|
|
11427
11723
|
if (!/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?-[0-9a-f]{64}$/u.test(name)) continue;
|
|
11428
|
-
const candidate =
|
|
11724
|
+
const candidate = join17(dataRoot, name);
|
|
11429
11725
|
try {
|
|
11430
|
-
const stat =
|
|
11726
|
+
const stat = lstatSync12(candidate);
|
|
11431
11727
|
if (!stat.isDirectory() || stat.isSymbolicLink()) continue;
|
|
11432
11728
|
if (kept.has(realpathSync9(candidate))) continue;
|
|
11433
11729
|
rmSync4(candidate, { recursive: true, force: true });
|
|
@@ -11440,9 +11736,9 @@ function collectSupersededPayloads(dataRoot, keep) {
|
|
|
11440
11736
|
function repairProjectNotebookSkillProjection(input = {}) {
|
|
11441
11737
|
const env2 = input.env ?? process.env;
|
|
11442
11738
|
const home = env2.HOME;
|
|
11443
|
-
if (!home || !
|
|
11444
|
-
const skillsRoot =
|
|
11445
|
-
const link =
|
|
11739
|
+
if (!home || !resolve15(home).startsWith("/")) throw new NotebookError("NOT_CONFIGURED", "A trusted HOME is required to repair the Project Notebook skill projection");
|
|
11740
|
+
const skillsRoot = join17(home, ".agents", "skills");
|
|
11741
|
+
const link = join17(skillsRoot, "project-notebook");
|
|
11446
11742
|
const probe = probeCanonicalSkillexRootProjection(skillsRoot, link);
|
|
11447
11743
|
if (probe.state === "adopted") return { status: "clean", summary: "Canonical Skillex projection already matches the version-pinned export", source: probe.source, drift: [], changed_files: [] };
|
|
11448
11744
|
if (probe.state !== "declined") {
|
|
@@ -11456,7 +11752,7 @@ function repairProjectNotebookSkillProjection(input = {}) {
|
|
|
11456
11752
|
const present = [];
|
|
11457
11753
|
const walk = (directory) => {
|
|
11458
11754
|
for (const entry of readdirSync7(directory, { withFileTypes: true })) {
|
|
11459
|
-
const path =
|
|
11755
|
+
const path = join17(directory, entry.name);
|
|
11460
11756
|
const rel = relative11(source, path).split(sep6).join("/");
|
|
11461
11757
|
if (entry.isDirectory()) {
|
|
11462
11758
|
walk(path);
|
|
@@ -11470,11 +11766,11 @@ function repairProjectNotebookSkillProjection(input = {}) {
|
|
|
11470
11766
|
walk(source);
|
|
11471
11767
|
const stale = present.filter((rel) => !wanted.has(rel)).sort();
|
|
11472
11768
|
const changed = expected.manifest.files.filter((entry) => {
|
|
11473
|
-
const path =
|
|
11769
|
+
const path = join17(source, ...entry.path.split("/"));
|
|
11474
11770
|
if (!existsSync11(path)) return true;
|
|
11475
|
-
const stat =
|
|
11771
|
+
const stat = lstatSync12(path);
|
|
11476
11772
|
if (!stat.isFile()) return true;
|
|
11477
|
-
if (createHash8("sha256").update(
|
|
11773
|
+
if (createHash8("sha256").update(readFileSync13(path)).digest("hex") !== entry.sha256) return true;
|
|
11478
11774
|
const mode = stat.mode & 511;
|
|
11479
11775
|
const executable = entry.mode === "0755";
|
|
11480
11776
|
return executable && (mode & 64) === 0 || !executable && (mode & 73) !== 0;
|
|
@@ -11489,15 +11785,15 @@ function repairProjectNotebookSkillProjection(input = {}) {
|
|
|
11489
11785
|
summary: `Would restore ${affected.length} file(s) in ${source} from the version-pinned export`,
|
|
11490
11786
|
source,
|
|
11491
11787
|
drift: probe.block.details,
|
|
11492
|
-
changed_files: affected.map((rel) =>
|
|
11788
|
+
changed_files: affected.map((rel) => join17(source, ...rel.split("/")))
|
|
11493
11789
|
};
|
|
11494
11790
|
}
|
|
11495
|
-
for (const rel of stale) rmSync4(
|
|
11791
|
+
for (const rel of stale) rmSync4(join17(source, ...rel.split("/")), { force: true });
|
|
11496
11792
|
for (const rel of changed) {
|
|
11497
|
-
const destination =
|
|
11793
|
+
const destination = join17(source, ...rel.split("/"));
|
|
11498
11794
|
const entry = expected.manifest.files.find((item) => item.path === rel);
|
|
11499
11795
|
mkdirSync6(dirname13(destination), { recursive: true, mode: 493 });
|
|
11500
|
-
copyFileSync3(
|
|
11796
|
+
copyFileSync3(join17(expected.source, ...rel.split("/")), destination);
|
|
11501
11797
|
chmodSync4(destination, entry.mode === "0755" ? 493 : 420);
|
|
11502
11798
|
}
|
|
11503
11799
|
verifyProjectNotebookSkillExport(source);
|
|
@@ -11506,21 +11802,21 @@ function repairProjectNotebookSkillProjection(input = {}) {
|
|
|
11506
11802
|
summary: `Restored ${affected.length} file(s) in ${source} from the version-pinned export; commit the change in the Skillex checkout`,
|
|
11507
11803
|
source,
|
|
11508
11804
|
drift: probe.block.details,
|
|
11509
|
-
changed_files: affected.map((rel) =>
|
|
11805
|
+
changed_files: affected.map((rel) => join17(source, ...rel.split("/")))
|
|
11510
11806
|
};
|
|
11511
11807
|
}
|
|
11512
11808
|
function inspectProjectNotebookIntegration(env2 = process.env) {
|
|
11513
11809
|
const home = env2.HOME;
|
|
11514
11810
|
if (!home) return { skill_installed: false, hooks_projected: false, details: ["HOME is unavailable"] };
|
|
11515
|
-
const skillsRoot =
|
|
11516
|
-
const link =
|
|
11811
|
+
const skillsRoot = join17(home, ".agents", "skills");
|
|
11812
|
+
const link = join17(skillsRoot, "project-notebook");
|
|
11517
11813
|
try {
|
|
11518
11814
|
const expected = expectedPackedSkill();
|
|
11519
|
-
const dataRoot =
|
|
11520
|
-
const expectedPayload =
|
|
11815
|
+
const dataRoot = resolve15(env2.XDG_DATA_HOME || join17(home, ".local", "share"), "pjangler", "skills", "project-notebook");
|
|
11816
|
+
const expectedPayload = join17(dataRoot, `${PJANGLER_VERSION}-${expected.digest}`);
|
|
11521
11817
|
const probe = probeCanonicalSkillexRootProjection(skillsRoot, link);
|
|
11522
11818
|
if (probe.state === "declined") return { skill_installed: false, hooks_projected: false, details: probe.block.details, blocked: probe.block };
|
|
11523
|
-
const stat =
|
|
11819
|
+
const stat = lstatSync12(link);
|
|
11524
11820
|
if (!stat.isSymbolicLink()) {
|
|
11525
11821
|
return {
|
|
11526
11822
|
skill_installed: false,
|
|
@@ -11557,8 +11853,8 @@ function inspectProjectNotebookIntegration(env2 = process.env) {
|
|
|
11557
11853
|
}
|
|
11558
11854
|
}
|
|
11559
11855
|
function projectorArguments(source, command, input) {
|
|
11560
|
-
const script =
|
|
11561
|
-
const args = [script, command, "--master",
|
|
11856
|
+
const script = join17(source, "scripts", "project-hooks.py");
|
|
11857
|
+
const args = [script, command, "--master", join17(source, "hooks", "hooks.master.json"), "--fragment", join17(source, "hooks", "claude.settings.json"), "--target", input.target];
|
|
11562
11858
|
if (command === "check") args.push("--json");
|
|
11563
11859
|
else if (input.stateHome) args.push("--state-home", input.stateHome);
|
|
11564
11860
|
return args;
|
|
@@ -11580,8 +11876,8 @@ function checkProjectNotebookHooks(input = {}) {
|
|
|
11580
11876
|
verifyProjectNotebookSkillExport(source);
|
|
11581
11877
|
const home = env2.HOME;
|
|
11582
11878
|
if (!home) throw new NotebookError("NOT_CONFIGURED", "HOME is required to check Project Notebook hooks");
|
|
11583
|
-
const target =
|
|
11584
|
-
const result2 =
|
|
11879
|
+
const target = resolve15(input.target ?? env2.PJ_PROJECT_NOTEBOOK_CLAUDE_SETTINGS ?? join17(home, ".claude", "settings.json"));
|
|
11880
|
+
const result2 = spawnSync9("/usr/bin/python3", ["-I", ...projectorArguments(source, "check", { target })], { encoding: "utf8", env: projectorEnvironment(env2), timeout: 5e3, maxBuffer: 1048576 });
|
|
11585
11881
|
if (result2.status !== 0 && result2.status !== 1) throw new NotebookError("CONFLICT", (result2.stderr || "Project Notebook projector check failed").trim().slice(0, 512));
|
|
11586
11882
|
try {
|
|
11587
11883
|
const parsed = JSON.parse(result2.stdout);
|
|
@@ -11598,13 +11894,13 @@ function installProjectNotebookIntegration(input = {}) {
|
|
|
11598
11894
|
const source = realpathSync9(skill.path);
|
|
11599
11895
|
const home = env2.HOME;
|
|
11600
11896
|
if (!home) throw new NotebookError("NOT_CONFIGURED", "HOME is required to install Project Notebook hooks");
|
|
11601
|
-
const target =
|
|
11602
|
-
const stateHome =
|
|
11603
|
-
const result2 =
|
|
11897
|
+
const target = resolve15(input.target ?? env2.PJ_PROJECT_NOTEBOOK_CLAUDE_SETTINGS ?? join17(home, ".claude", "settings.json"));
|
|
11898
|
+
const stateHome = resolve15(env2.XDG_STATE_HOME || join17(home, ".local", "state"));
|
|
11899
|
+
const result2 = spawnSync9("/usr/bin/python3", ["-I", ...projectorArguments(source, "install", { target, stateHome })], { encoding: "utf8", env: projectorEnvironment(env2), timeout: 5e3, maxBuffer: 1048576 });
|
|
11604
11900
|
if (result2.status !== 0) {
|
|
11605
11901
|
if (skill.installed) {
|
|
11606
11902
|
try {
|
|
11607
|
-
const stat =
|
|
11903
|
+
const stat = lstatSync12(skill.path);
|
|
11608
11904
|
if (stat.isSymbolicLink() && realpathSync9(skill.path) === source) unlinkSync6(skill.path);
|
|
11609
11905
|
} catch {
|
|
11610
11906
|
}
|
|
@@ -11618,14 +11914,14 @@ function boundedDiagnostic(value, max = 512) {
|
|
|
11618
11914
|
return message.replace(/[\u0000-\u001f\u007f]/gu, " ").replace(/\s+/gu, " ").slice(0, max);
|
|
11619
11915
|
}
|
|
11620
11916
|
function assertNoSymlinkComponents(path, allowMissing = false) {
|
|
11621
|
-
const absolute =
|
|
11917
|
+
const absolute = resolve15(path);
|
|
11622
11918
|
const root = parse3(absolute).root;
|
|
11623
11919
|
let cursor = root;
|
|
11624
11920
|
for (const component of absolute.slice(root.length).split(sep6).filter(Boolean)) {
|
|
11625
|
-
cursor =
|
|
11921
|
+
cursor = join17(cursor, component);
|
|
11626
11922
|
let stat;
|
|
11627
11923
|
try {
|
|
11628
|
-
stat =
|
|
11924
|
+
stat = lstatSync12(cursor);
|
|
11629
11925
|
} catch (error) {
|
|
11630
11926
|
if (allowMissing && error.code === "ENOENT") return;
|
|
11631
11927
|
throw error;
|
|
@@ -11825,7 +12121,7 @@ var NotebookModule = class {
|
|
|
11825
12121
|
return {
|
|
11826
12122
|
changedFiles: [
|
|
11827
12123
|
...installed.skill.installed ? [installed.skill.path] : [],
|
|
11828
|
-
...installed.hooksChanged ? [
|
|
12124
|
+
...installed.hooksChanged ? [resolve17(env2.PJ_PROJECT_NOTEBOOK_CLAUDE_SETTINGS ?? `${home}/.claude/settings.json`)] : []
|
|
11829
12125
|
],
|
|
11830
12126
|
...installed.blocked ? { blocked: installed.blocked } : {}
|
|
11831
12127
|
};
|
|
@@ -12231,17 +12527,17 @@ init_remote_mutation_journal();
|
|
|
12231
12527
|
function integrationEnvironment(module, ctx) {
|
|
12232
12528
|
const env2 = { ...module.environment, HOME: ctx.homeDir };
|
|
12233
12529
|
if (module.environment.HOME !== ctx.homeDir) {
|
|
12234
|
-
env2.XDG_DATA_HOME =
|
|
12235
|
-
env2.XDG_STATE_HOME =
|
|
12236
|
-
env2.PJ_PROJECT_NOTEBOOK_CLAUDE_SETTINGS =
|
|
12530
|
+
env2.XDG_DATA_HOME = join18(ctx.homeDir, ".local", "share");
|
|
12531
|
+
env2.XDG_STATE_HOME = join18(ctx.homeDir, ".local", "state");
|
|
12532
|
+
env2.PJ_PROJECT_NOTEBOOK_CLAUDE_SETTINGS = join18(ctx.homeDir, ".claude", "settings.json");
|
|
12237
12533
|
}
|
|
12238
12534
|
return env2;
|
|
12239
12535
|
}
|
|
12240
12536
|
function resolvedForPlan(plan) {
|
|
12241
|
-
const manifestPath =
|
|
12537
|
+
const manifestPath = join18(plan.project.repo_path, ".project.json");
|
|
12242
12538
|
let manifest = plan.manifest;
|
|
12243
12539
|
if (existsSync13(manifestPath)) {
|
|
12244
|
-
const parsed = JSON.parse(
|
|
12540
|
+
const parsed = JSON.parse(readFileSync14(manifestPath, "utf8"));
|
|
12245
12541
|
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error(`${manifestPath} must contain a JSON object`);
|
|
12246
12542
|
manifest = parsed;
|
|
12247
12543
|
}
|
|
@@ -12382,7 +12678,7 @@ var PRODUCTION_RUNTIME = {
|
|
|
12382
12678
|
executePlan: executeProjectInitPlan,
|
|
12383
12679
|
preflightBmad: preflightBmadLifecycle,
|
|
12384
12680
|
runGit(cwd, args, options) {
|
|
12385
|
-
const result2 =
|
|
12681
|
+
const result2 = spawnSync12("git", [...args], {
|
|
12386
12682
|
cwd,
|
|
12387
12683
|
encoding: "utf8",
|
|
12388
12684
|
env: options?.env ? { ...process.env, ...options.env } : process.env
|
|
@@ -12396,13 +12692,13 @@ var PRODUCTION_RUNTIME = {
|
|
|
12396
12692
|
}
|
|
12397
12693
|
};
|
|
12398
12694
|
function unsafeToRemove(targetDir) {
|
|
12399
|
-
const absolute =
|
|
12695
|
+
const absolute = resolve18(targetDir);
|
|
12400
12696
|
let cursor = absolute;
|
|
12401
12697
|
const seen = /* @__PURE__ */ new Set();
|
|
12402
12698
|
while (!seen.has(cursor)) {
|
|
12403
12699
|
seen.add(cursor);
|
|
12404
12700
|
try {
|
|
12405
|
-
const stat =
|
|
12701
|
+
const stat = lstatSync13(cursor);
|
|
12406
12702
|
if (stat.isSymbolicLink()) {
|
|
12407
12703
|
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`;
|
|
12408
12704
|
}
|
|
@@ -12430,12 +12726,12 @@ function publicMigration(report) {
|
|
|
12430
12726
|
};
|
|
12431
12727
|
}
|
|
12432
12728
|
function hasGitRepository(runtime, targetDir) {
|
|
12433
|
-
if (!existsSync14(
|
|
12729
|
+
if (!existsSync14(join19(targetDir, ".git"))) return false;
|
|
12434
12730
|
return runtime.runGit(targetDir, ["rev-parse", "--is-inside-work-tree"]).status === 0;
|
|
12435
12731
|
}
|
|
12436
12732
|
function refreshPlanFromCanonicalManifest(plan) {
|
|
12437
|
-
const manifestPath =
|
|
12438
|
-
const manifest = JSON.parse(
|
|
12733
|
+
const manifestPath = join19(plan.project.repo_path, ".project.json");
|
|
12734
|
+
const manifest = JSON.parse(readFileSync15(manifestPath, "utf8"));
|
|
12439
12735
|
if (!manifest || typeof manifest !== "object" || Array.isArray(manifest)) {
|
|
12440
12736
|
throw new Error(`${manifestPath} must contain a JSON object`);
|
|
12441
12737
|
}
|
|
@@ -12527,6 +12823,8 @@ var ProjectRecipe = class extends Recipe {
|
|
|
12527
12823
|
repoRoot: targetDir,
|
|
12528
12824
|
bmadVersionPin: mode === "create" ? BMAD_INSTALLER_VERSION : ctx.bmadVersionPin
|
|
12529
12825
|
};
|
|
12826
|
+
let bmadCompanion;
|
|
12827
|
+
let g33DeferredForEnrollment = false;
|
|
12530
12828
|
let migrationReport;
|
|
12531
12829
|
let audit;
|
|
12532
12830
|
let notebookPlan;
|
|
@@ -12567,6 +12865,10 @@ var ProjectRecipe = class extends Recipe {
|
|
|
12567
12865
|
message: planBlocked ? "Project plan skipped after failed preflight" : executed.ok ? "Project plan executed" : executed.errors.join("; ")
|
|
12568
12866
|
});
|
|
12569
12867
|
if (errors.length === 0 && executed.ok && mode === "create") {
|
|
12868
|
+
try {
|
|
12869
|
+
g33DeferredForEnrollment = !g33CompanionEnabled(targetDir);
|
|
12870
|
+
} catch {
|
|
12871
|
+
}
|
|
12570
12872
|
const dependencyResult = await this.registry.initDependencies(this.metadata.id, transactionContext, normalized, ["notebook"]);
|
|
12571
12873
|
logs.push(...dependencyResult.logs);
|
|
12572
12874
|
errors.push(...dependencyResult.errors);
|
|
@@ -12647,7 +12949,7 @@ var ProjectRecipe = class extends Recipe {
|
|
|
12647
12949
|
if (hasGitRepository(this.runtime, targetDir)) {
|
|
12648
12950
|
phases.push({ id: "project.git", status: "unchanged", changedFiles: [], message: "Git repository already initialized" });
|
|
12649
12951
|
} else {
|
|
12650
|
-
const gitPath =
|
|
12952
|
+
const gitPath = join19(targetDir, ".git");
|
|
12651
12953
|
for (const { args, label, options } of [
|
|
12652
12954
|
{ args: ["init", "--initial-branch=main"], label: "git init" },
|
|
12653
12955
|
{ args: ["add", "-A"], label: "git add" },
|
|
@@ -12747,6 +13049,17 @@ var ProjectRecipe = class extends Recipe {
|
|
|
12747
13049
|
errors.push(`project manifest refresh after external effects failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
12748
13050
|
}
|
|
12749
13051
|
}
|
|
13052
|
+
if (errors.length === 0 && registryFinalizerEligible && mode === "create" && g33DeferredForEnrollment && plan.project.ticket_provider.state === "linked") {
|
|
13053
|
+
bmadCompanion = await runG33Companion(targetDir, ctx.dryRun);
|
|
13054
|
+
logs.push(...companionDetails(bmadCompanion));
|
|
13055
|
+
changedFiles.push(...bmadCompanion.changedFiles);
|
|
13056
|
+
phases.push({
|
|
13057
|
+
id: "project.optional:g33-companion",
|
|
13058
|
+
status: bmadCompanion.status === "changed" ? "changed" : bmadCompanion.status === "planned" ? "planned" : bmadCompanion.status === "unchanged" ? "unchanged" : "skipped",
|
|
13059
|
+
changedFiles: bmadCompanion.changedFiles,
|
|
13060
|
+
message: `Optional g33 (${bmadCompanion.status}): ${bmadCompanion.summary}`
|
|
13061
|
+
});
|
|
13062
|
+
}
|
|
12750
13063
|
if (registryFinalizerEligible && registryActions.length) {
|
|
12751
13064
|
rollbackEligible = false;
|
|
12752
13065
|
const registryPlan = { ...plan, actions: registryActions };
|
|
@@ -12800,8 +13113,8 @@ var ProjectRecipe = class extends Recipe {
|
|
|
12800
13113
|
} else try {
|
|
12801
13114
|
rmSync5(targetDir, { recursive: true, force: true });
|
|
12802
13115
|
const insideTarget = (path) => {
|
|
12803
|
-
const relative12 = relativePath(
|
|
12804
|
-
return relative12 === "" || !relative12.startsWith("..") && !
|
|
13116
|
+
const relative12 = relativePath(resolve18(targetDir), resolve18(path));
|
|
13117
|
+
return relative12 === "" || !relative12.startsWith("..") && !isAbsolute9(relative12);
|
|
12805
13118
|
};
|
|
12806
13119
|
const orphaned = [...new Set(changedFiles.filter((path) => !insideTarget(path)))].sort();
|
|
12807
13120
|
changedFiles.length = 0;
|
|
@@ -12837,7 +13150,8 @@ var ProjectRecipe = class extends Recipe {
|
|
|
12837
13150
|
audit,
|
|
12838
13151
|
selectedOperations: normalized.selectedOperations ?? [],
|
|
12839
13152
|
selectedParityRules: normalized.selectedRuleIds ?? [],
|
|
12840
|
-
migrationReport
|
|
13153
|
+
migrationReport,
|
|
13154
|
+
...bmadCompanion ? { bmadCompanion } : {}
|
|
12841
13155
|
};
|
|
12842
13156
|
}
|
|
12843
13157
|
printNextSteps() {
|
|
@@ -12845,12 +13159,12 @@ var ProjectRecipe = class extends Recipe {
|
|
|
12845
13159
|
};
|
|
12846
13160
|
|
|
12847
13161
|
// src/recipes/registry.ts
|
|
12848
|
-
import { readFileSync as
|
|
12849
|
-
import { join as
|
|
13162
|
+
import { readFileSync as readFileSync16 } from "node:fs";
|
|
13163
|
+
import { join as join20 } from "node:path";
|
|
12850
13164
|
function projectWaivers(repoRoot) {
|
|
12851
13165
|
let waive;
|
|
12852
13166
|
try {
|
|
12853
|
-
waive = JSON.parse(
|
|
13167
|
+
waive = JSON.parse(readFileSync16(join20(repoRoot, ".project.json"), "utf8"))?.parity?.waive;
|
|
12854
13168
|
} catch {
|
|
12855
13169
|
return /* @__PURE__ */ new Map();
|
|
12856
13170
|
}
|
|
@@ -13191,8 +13505,8 @@ var recipeRegistry = new RecipeRegistry([
|
|
|
13191
13505
|
|
|
13192
13506
|
// src/commands/AgentHooksCommands.ts
|
|
13193
13507
|
import { homedir as homedir6 } from "node:os";
|
|
13194
|
-
import { join as
|
|
13195
|
-
import { existsSync as existsSync15, cpSync, mkdirSync as mkdirSync7, readFileSync as
|
|
13508
|
+
import { join as join21, dirname as dirname15 } from "node:path";
|
|
13509
|
+
import { existsSync as existsSync15, cpSync, mkdirSync as mkdirSync7, readFileSync as readFileSync17, writeFileSync as writeFileSync8 } from "node:fs";
|
|
13196
13510
|
import { fileURLToPath as fileURLToPath4 } from "node:url";
|
|
13197
13511
|
init_project();
|
|
13198
13512
|
import { parse as parseToml3 } from "smol-toml";
|
|
@@ -13205,16 +13519,16 @@ function resolveTemplateRoot() {
|
|
|
13205
13519
|
try {
|
|
13206
13520
|
let dir = dirname15(fileURLToPath4(import.meta.url));
|
|
13207
13521
|
for (let i = 0; i < 8; i++) {
|
|
13208
|
-
candidates.push(
|
|
13522
|
+
candidates.push(join21(dir, "templates", "commonproject", "template"));
|
|
13209
13523
|
const parent = dirname15(dir);
|
|
13210
13524
|
if (parent === dir) break;
|
|
13211
13525
|
dir = parent;
|
|
13212
13526
|
}
|
|
13213
13527
|
} catch {
|
|
13214
13528
|
}
|
|
13215
|
-
candidates.push(
|
|
13529
|
+
candidates.push(join21(homedir6(), "code", "pjangler", "templates", "commonproject", "template"));
|
|
13216
13530
|
for (const c of candidates) {
|
|
13217
|
-
if (existsSync15(
|
|
13531
|
+
if (existsSync15(join21(c, ".agents", "hooks", "hooks.master.json"))) return c;
|
|
13218
13532
|
}
|
|
13219
13533
|
throw new Error(
|
|
13220
13534
|
"Could not locate the CommonProject template. Set PJANGLER_COMMONPROJECT_TEMPLATE to <repo>/templates/commonproject/template."
|
|
@@ -13240,8 +13554,8 @@ var CopyAgentHooksTree = class extends Command {
|
|
|
13240
13554
|
const created = [];
|
|
13241
13555
|
const skipped = [];
|
|
13242
13556
|
for (const { rel, dir } of items) {
|
|
13243
|
-
const src =
|
|
13244
|
-
const dest =
|
|
13557
|
+
const src = join21(templateRoot, rel);
|
|
13558
|
+
const dest = join21(this.context.targetDir, rel);
|
|
13245
13559
|
if (!existsSync15(src)) continue;
|
|
13246
13560
|
if (existsSync15(dest) && !this.context.force) {
|
|
13247
13561
|
skipped.push(rel);
|
|
@@ -13269,14 +13583,14 @@ var WireMiseAgentHooks = class _WireMiseAgentHooks extends Command {
|
|
|
13269
13583
|
if (!resolveAgentHooksLayer2()) {
|
|
13270
13584
|
return { success: true, message: this.formatMessage(AGENT_HOOKS_SKIP_MESSAGE) };
|
|
13271
13585
|
}
|
|
13272
|
-
const misePath =
|
|
13586
|
+
const misePath = join21(this.context.targetDir, "mise.toml");
|
|
13273
13587
|
if (!existsSync15(misePath)) {
|
|
13274
13588
|
return {
|
|
13275
13589
|
success: false,
|
|
13276
13590
|
message: "\u26A0\uFE0F No mise.toml found \u2014 run `pj add mise` first, then re-run."
|
|
13277
13591
|
};
|
|
13278
13592
|
}
|
|
13279
|
-
let content =
|
|
13593
|
+
let content = readFileSync17(misePath, "utf8");
|
|
13280
13594
|
if (content.includes(_WireMiseAgentHooks.MARKER)) {
|
|
13281
13595
|
return { success: true, message: this.formatMessage("\u2713 mise.toml already wired for agent-hooks") };
|
|
13282
13596
|
}
|
|
@@ -13499,7 +13813,7 @@ if __name__ == "__main__":
|
|
|
13499
13813
|
|
|
13500
13814
|
// src/commands/AddMiseCodegraphScript.ts
|
|
13501
13815
|
import { chmodSync as chmodSync5 } from "fs";
|
|
13502
|
-
import { join as
|
|
13816
|
+
import { join as join22 } from "path";
|
|
13503
13817
|
var AddMiseCodegraphScript = class extends Command {
|
|
13504
13818
|
async invoke() {
|
|
13505
13819
|
const filePath = ".mise/scripts/codegraph.sh";
|
|
@@ -13553,7 +13867,7 @@ fi
|
|
|
13553
13867
|
`;
|
|
13554
13868
|
this.writeFile(filePath, content);
|
|
13555
13869
|
if (!this.context.dryRun) {
|
|
13556
|
-
chmodSync5(
|
|
13870
|
+
chmodSync5(join22(this.context.targetDir, filePath), 493);
|
|
13557
13871
|
}
|
|
13558
13872
|
return {
|
|
13559
13873
|
success: true,
|
|
@@ -13591,18 +13905,18 @@ SECRET_KEY=""
|
|
|
13591
13905
|
// src/parity/index.ts
|
|
13592
13906
|
import { existsSync as existsSync16 } from "node:fs";
|
|
13593
13907
|
import { homedir as homedir7 } from "node:os";
|
|
13594
|
-
import { dirname as dirname16, join as
|
|
13908
|
+
import { dirname as dirname16, join as join23, resolve as resolve19 } from "node:path";
|
|
13595
13909
|
import { fileURLToPath as fileURLToPath5 } from "node:url";
|
|
13596
13910
|
function resolvePjanglerRoot2() {
|
|
13597
13911
|
let dir = dirname16(fileURLToPath5(import.meta.url));
|
|
13598
13912
|
while (dir !== dirname16(dir)) {
|
|
13599
|
-
if (existsSync16(
|
|
13913
|
+
if (existsSync16(join23(dir, "package.json")) && existsSync16(join23(dir, "templates", "commonproject", "copier.yml"))) return dir;
|
|
13600
13914
|
dir = dirname16(dir);
|
|
13601
13915
|
}
|
|
13602
|
-
return
|
|
13916
|
+
return resolve19(process.cwd());
|
|
13603
13917
|
}
|
|
13604
13918
|
function lifecycleContext(repoArg, dryRun, acceptRegistryMatches = false, overrides = {}) {
|
|
13605
|
-
const repoRoot =
|
|
13919
|
+
const repoRoot = resolve19(repoArg ?? process.cwd());
|
|
13606
13920
|
return {
|
|
13607
13921
|
...overrides,
|
|
13608
13922
|
targetDir: repoRoot,
|
|
@@ -13771,13 +14085,13 @@ init_version();
|
|
|
13771
14085
|
// src/project/info.ts
|
|
13772
14086
|
init_boardUrl();
|
|
13773
14087
|
init_project();
|
|
13774
|
-
import { readFileSync as
|
|
13775
|
-
import { join as
|
|
14088
|
+
import { readFileSync as readFileSync18 } from "node:fs";
|
|
14089
|
+
import { join as join24, resolve as resolve20 } from "node:path";
|
|
13776
14090
|
function readManifest2(root) {
|
|
13777
|
-
const path =
|
|
14091
|
+
const path = join24(root, ".project.json");
|
|
13778
14092
|
let manifest;
|
|
13779
14093
|
try {
|
|
13780
|
-
const value = JSON.parse(
|
|
14094
|
+
const value = JSON.parse(readFileSync18(path, "utf8"));
|
|
13781
14095
|
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("expected a JSON object");
|
|
13782
14096
|
manifest = value;
|
|
13783
14097
|
} catch (error) {
|
|
@@ -13817,8 +14131,8 @@ function readProjectInfo(input = {}) {
|
|
|
13817
14131
|
const info = {
|
|
13818
14132
|
ok: true,
|
|
13819
14133
|
project_id: projectId,
|
|
13820
|
-
repo_path:
|
|
13821
|
-
manifest_path:
|
|
14134
|
+
repo_path: resolve20(root),
|
|
14135
|
+
manifest_path: join24(resolve20(root), ".project.json"),
|
|
13822
14136
|
manifest,
|
|
13823
14137
|
registry: { location, status: "indexed" }
|
|
13824
14138
|
};
|
|
@@ -13839,27 +14153,27 @@ function readProjectInfo(input = {}) {
|
|
|
13839
14153
|
info.registry = { location, status: "not-indexed", message: "This project is not indexed. Run pj init --apply from its repository." };
|
|
13840
14154
|
return info;
|
|
13841
14155
|
}
|
|
13842
|
-
if (
|
|
14156
|
+
if (resolve20(indexed.repo_path) !== resolve20(root)) {
|
|
13843
14157
|
info.registry = { location, status: "stale", message: `The registry points to ${indexed.repo_path}; this manifest is at ${root}. Reindex the repository.` };
|
|
13844
14158
|
}
|
|
13845
14159
|
return info;
|
|
13846
14160
|
}
|
|
13847
14161
|
|
|
13848
14162
|
// src/describe/index.ts
|
|
13849
|
-
import { existsSync as existsSync17, readFileSync as
|
|
13850
|
-
import { join as
|
|
14163
|
+
import { existsSync as existsSync17, readFileSync as readFileSync19, readdirSync as readdirSync8, statSync as statSync6 } from "node:fs";
|
|
14164
|
+
import { join as join26, resolve as resolve21 } from "node:path";
|
|
13851
14165
|
init_project();
|
|
13852
14166
|
|
|
13853
14167
|
// src/describe/activity.ts
|
|
13854
|
-
import { spawn as spawn2, spawnSync as
|
|
14168
|
+
import { spawn as spawn2, spawnSync as spawnSync13 } from "node:child_process";
|
|
13855
14169
|
import { statSync as statSync5 } from "node:fs";
|
|
13856
|
-
import { join as
|
|
14170
|
+
import { join as join25 } from "node:path";
|
|
13857
14171
|
var ACTIVE_WINDOW_SECONDS = 24 * 60 * 60;
|
|
13858
14172
|
var MAX_DIRTY_STATS = 500;
|
|
13859
14173
|
var GIT_TIMEOUT_MS = 5e3;
|
|
13860
14174
|
var GIT_MAX_BUFFER = 16 * 1024 * 1024;
|
|
13861
14175
|
function git3(repo, args) {
|
|
13862
|
-
const result2 =
|
|
14176
|
+
const result2 = spawnSync13("git", ["-C", repo, ...args], {
|
|
13863
14177
|
encoding: "utf8",
|
|
13864
14178
|
timeout: GIT_TIMEOUT_MS,
|
|
13865
14179
|
maxBuffer: GIT_MAX_BUFFER
|
|
@@ -13982,7 +14296,7 @@ function uncommittedSource(repo, paths) {
|
|
|
13982
14296
|
let newest = 0;
|
|
13983
14297
|
for (const path of paths.slice(0, MAX_DIRTY_STATS)) {
|
|
13984
14298
|
try {
|
|
13985
|
-
const mtime = Math.floor(statSync5(
|
|
14299
|
+
const mtime = Math.floor(statSync5(join25(repo, path)).mtimeMs / 1e3);
|
|
13986
14300
|
if (mtime > newest) newest = mtime;
|
|
13987
14301
|
} catch {
|
|
13988
14302
|
}
|
|
@@ -14091,7 +14405,7 @@ var CONFIG_FILES = [
|
|
|
14091
14405
|
];
|
|
14092
14406
|
function readJson(path) {
|
|
14093
14407
|
try {
|
|
14094
|
-
const parsed = JSON.parse(
|
|
14408
|
+
const parsed = JSON.parse(readFileSync19(path, "utf8"));
|
|
14095
14409
|
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : void 0;
|
|
14096
14410
|
} catch {
|
|
14097
14411
|
return void 0;
|
|
@@ -14113,7 +14427,7 @@ function describeType(repo) {
|
|
|
14113
14427
|
const evidence = [];
|
|
14114
14428
|
const note = (signal, file) => evidence.push(`${signal} (${file})`);
|
|
14115
14429
|
for (const marker of LANGUAGE_MARKERS) {
|
|
14116
|
-
if (!existsSync17(
|
|
14430
|
+
if (!existsSync17(join26(repo, marker.file))) continue;
|
|
14117
14431
|
if (!languages.includes(marker.language)) {
|
|
14118
14432
|
languages.push(marker.language);
|
|
14119
14433
|
note(marker.language, marker.file);
|
|
@@ -14127,9 +14441,9 @@ function describeType(repo) {
|
|
|
14127
14441
|
}
|
|
14128
14442
|
} catch {
|
|
14129
14443
|
}
|
|
14130
|
-
const pkg = readJson(
|
|
14444
|
+
const pkg = readJson(join26(repo, "package.json"));
|
|
14131
14445
|
if (pkg) {
|
|
14132
|
-
if (existsSync17(
|
|
14446
|
+
if (existsSync17(join26(repo, "tsconfig.json"))) {
|
|
14133
14447
|
const index = languages.indexOf("javascript");
|
|
14134
14448
|
if (index >= 0) languages.splice(index, 1);
|
|
14135
14449
|
if (!languages.includes("typescript")) {
|
|
@@ -14163,26 +14477,26 @@ function describeType(repo) {
|
|
|
14163
14477
|
["hermes-fleet-host", "agents/hermes"]
|
|
14164
14478
|
];
|
|
14165
14479
|
for (const [role, marker] of roleMarkers) {
|
|
14166
|
-
if (!existsSync17(
|
|
14480
|
+
if (!existsSync17(join26(repo, marker))) continue;
|
|
14167
14481
|
roles.push(role);
|
|
14168
14482
|
note(role, marker);
|
|
14169
14483
|
}
|
|
14170
14484
|
return { primaryLanguage: languages[0], languages, roles, evidence };
|
|
14171
14485
|
}
|
|
14172
14486
|
function describeIdentity(repo, registryPath) {
|
|
14173
|
-
const manifestPath =
|
|
14487
|
+
const manifestPath = join26(repo, ".project.json");
|
|
14174
14488
|
const manifest = readJson(manifestPath);
|
|
14175
14489
|
const drift = [];
|
|
14176
|
-
let
|
|
14490
|
+
let record3;
|
|
14177
14491
|
let registryReadable = true;
|
|
14178
14492
|
try {
|
|
14179
14493
|
const registry = loadProjectRegistry(registryPath);
|
|
14180
14494
|
const identity = manifest?.project_id ?? manifest?.project_slug;
|
|
14181
14495
|
const slug = typeof identity === "string" ? identity.toLowerCase() : void 0;
|
|
14182
|
-
const resolved =
|
|
14183
|
-
|
|
14496
|
+
const resolved = resolve21(repo);
|
|
14497
|
+
record3 = (slug ? registry.projects[slug] : void 0) ?? Object.values(registry.projects).find((project) => resolve21(project.repo_path) === resolved);
|
|
14184
14498
|
const statuses = registry.__registry_status;
|
|
14185
|
-
const status =
|
|
14499
|
+
const status = record3 && statuses?.[record3.slug];
|
|
14186
14500
|
if (status && status.status !== "ok") {
|
|
14187
14501
|
drift.push({ note: `canonical manifest is ${status.status}; displaying the last indexed record. ${status.error ?? ""}`, command: "pjangler doctor" });
|
|
14188
14502
|
}
|
|
@@ -14190,14 +14504,14 @@ function describeIdentity(repo, registryPath) {
|
|
|
14190
14504
|
registryReadable = false;
|
|
14191
14505
|
drift.push({ note: `registry unreadable: ${err instanceof Error ? err.message : String(err)}` });
|
|
14192
14506
|
}
|
|
14193
|
-
if (manifest && !
|
|
14507
|
+
if (manifest && !record3 && registryReadable) {
|
|
14194
14508
|
drift.push({ note: ".project.json exists but this repo is not in the pjangler registry" });
|
|
14195
14509
|
}
|
|
14196
|
-
if (
|
|
14197
|
-
drift.push({ note: `registered as ${
|
|
14510
|
+
if (record3 && !manifest) {
|
|
14511
|
+
drift.push({ note: `registered as ${record3.slug} but .project.json is missing`, command: "pjangler doctor" });
|
|
14198
14512
|
}
|
|
14199
|
-
if (
|
|
14200
|
-
drift.push({ note: `registry repo_path points elsewhere: ${
|
|
14513
|
+
if (record3 && resolve21(record3.repo_path) !== resolve21(repo)) {
|
|
14514
|
+
drift.push({ note: `registry repo_path points elsewhere: ${record3.repo_path}`, command: "pjangler doctor" });
|
|
14201
14515
|
}
|
|
14202
14516
|
const manifestProvider = manifest?.ticket_provider;
|
|
14203
14517
|
const provider = manifestProvider ? {
|
|
@@ -14206,13 +14520,13 @@ function describeIdentity(repo, registryPath) {
|
|
|
14206
14520
|
identifier: manifestProvider.identifier,
|
|
14207
14521
|
board_id: manifestProvider.board_id,
|
|
14208
14522
|
state: manifestProvider.state
|
|
14209
|
-
} :
|
|
14210
|
-
if (manifestProvider &&
|
|
14523
|
+
} : record3?.ticket_provider;
|
|
14524
|
+
if (manifestProvider && record3) {
|
|
14211
14525
|
const fields = [
|
|
14212
|
-
["type", provider?.type,
|
|
14213
|
-
["workspace", provider?.workspace,
|
|
14214
|
-
["identifier", provider?.identifier,
|
|
14215
|
-
["board_id", provider?.board_id,
|
|
14526
|
+
["type", provider?.type, record3.ticket_provider.type],
|
|
14527
|
+
["workspace", provider?.workspace, record3.ticket_provider.workspace],
|
|
14528
|
+
["identifier", provider?.identifier, record3.ticket_provider.identifier],
|
|
14529
|
+
["board_id", provider?.board_id, record3.ticket_provider.board_id]
|
|
14216
14530
|
];
|
|
14217
14531
|
for (const [field2, fromManifest, fromRegistry] of fields) {
|
|
14218
14532
|
if ((fromManifest ?? "") === (fromRegistry ?? "")) continue;
|
|
@@ -14222,7 +14536,7 @@ function describeIdentity(repo, registryPath) {
|
|
|
14222
14536
|
}
|
|
14223
14537
|
}
|
|
14224
14538
|
const manifestAgents = manifest?.agents ?? {};
|
|
14225
|
-
const agents =
|
|
14539
|
+
const agents = record3 ? Object.entries(record3.agents).map(([name, agent]) => ({
|
|
14226
14540
|
name,
|
|
14227
14541
|
role: agent.role,
|
|
14228
14542
|
provisioningState: agent.provisioning_state,
|
|
@@ -14235,11 +14549,11 @@ function describeIdentity(repo, registryPath) {
|
|
|
14235
14549
|
}));
|
|
14236
14550
|
return {
|
|
14237
14551
|
manifest: Boolean(manifest),
|
|
14238
|
-
registered: Boolean(
|
|
14552
|
+
registered: Boolean(record3),
|
|
14239
14553
|
registryPath,
|
|
14240
|
-
slug:
|
|
14241
|
-
name:
|
|
14242
|
-
description:
|
|
14554
|
+
slug: record3?.slug ?? (manifest?.project_id ?? manifest?.project_slug),
|
|
14555
|
+
name: record3?.name ?? manifest?.project_name,
|
|
14556
|
+
description: record3?.description ?? manifest?.project_description,
|
|
14243
14557
|
ticketProvider: provider ? {
|
|
14244
14558
|
type: provider.type,
|
|
14245
14559
|
workspace: provider.workspace,
|
|
@@ -14263,12 +14577,12 @@ function describeSubsystems(repo, findings) {
|
|
|
14263
14577
|
const markers = SUBSYSTEM_MARKERS[metadata.id] ?? [];
|
|
14264
14578
|
const evidence = metadata.id === "notebook" ? (() => {
|
|
14265
14579
|
try {
|
|
14266
|
-
const manifest = JSON.parse(
|
|
14580
|
+
const manifest = JSON.parse(readFileSync19(join26(repo, ".project.json"), "utf8"));
|
|
14267
14581
|
return manifest.notebook && typeof manifest.notebook === "object" ? [".project.json#notebook"] : [];
|
|
14268
14582
|
} catch {
|
|
14269
14583
|
return [];
|
|
14270
14584
|
}
|
|
14271
|
-
})() : markers.filter((marker) => existsSync17(
|
|
14585
|
+
})() : markers.filter((marker) => existsSync17(join26(repo, marker)));
|
|
14272
14586
|
const rules = (byRecipe.get(metadata.id) ?? []).map((finding2) => ({
|
|
14273
14587
|
id: finding2.id,
|
|
14274
14588
|
title: finding2.title,
|
|
@@ -14286,8 +14600,8 @@ function describeSubsystems(repo, findings) {
|
|
|
14286
14600
|
function describeNotebook(repo, registryPath) {
|
|
14287
14601
|
try {
|
|
14288
14602
|
const registry = loadProjectRegistry(registryPath);
|
|
14289
|
-
const project = Object.values(registry.projects).find((entry) =>
|
|
14290
|
-
const manifest = existsSync17(
|
|
14603
|
+
const project = Object.values(registry.projects).find((entry) => resolve21(entry.repo_path) === resolve21(repo));
|
|
14604
|
+
const manifest = existsSync17(join26(repo, ".project.json")) ? JSON.parse(readFileSync19(join26(repo, ".project.json"), "utf8")) : void 0;
|
|
14291
14605
|
const declared = Boolean(project?.notebook || manifest?.notebook && typeof manifest.notebook === "object");
|
|
14292
14606
|
if (!declared) return { declared: false, bindingState: null, notebookId: null, overviewNoteId: null, health: null, remoteCheck: "skip", captureAdmission: null };
|
|
14293
14607
|
const config = loadEffectiveNotebookConfig(repo, registryPath);
|
|
@@ -14305,7 +14619,7 @@ function describeNotebook(repo, registryPath) {
|
|
|
14305
14619
|
}
|
|
14306
14620
|
}
|
|
14307
14621
|
function describeConfigFiles(repo) {
|
|
14308
|
-
return CONFIG_FILES.filter((spec) => existsSync17(
|
|
14622
|
+
return CONFIG_FILES.filter((spec) => existsSync17(join26(repo, spec.path))).map((spec) => ({ path: spec.path, purpose: spec.purpose, subsystem: spec.subsystem }));
|
|
14309
14623
|
}
|
|
14310
14624
|
function describeNextSteps(description, findings) {
|
|
14311
14625
|
const steps = [];
|
|
@@ -14383,7 +14697,7 @@ function describeNextSteps(description, findings) {
|
|
|
14383
14697
|
return steps;
|
|
14384
14698
|
}
|
|
14385
14699
|
async function describeProject(input = {}) {
|
|
14386
|
-
const repo =
|
|
14700
|
+
const repo = resolve21(input.repoArg ?? process.cwd());
|
|
14387
14701
|
if (!existsSync17(repo)) throw new Error(`Path does not exist: ${repo}`);
|
|
14388
14702
|
if (!statSync6(repo).isDirectory()) throw new Error(`Not a directory: ${repo}`);
|
|
14389
14703
|
const registryPath = input.registryPath ?? projectRegistryPath();
|
|
@@ -14572,7 +14886,7 @@ function validateExternalEffectConsent(input, options) {
|
|
|
14572
14886
|
return selected;
|
|
14573
14887
|
}
|
|
14574
14888
|
function resolveTargetDir(targetDir) {
|
|
14575
|
-
const dir =
|
|
14889
|
+
const dir = resolve22(targetDir ?? process.cwd());
|
|
14576
14890
|
if (!existsSync18(dir)) {
|
|
14577
14891
|
throw new Error(`Target directory does not exist: ${dir}`);
|
|
14578
14892
|
}
|
|
@@ -14584,7 +14898,7 @@ function resolveTargetDir(targetDir) {
|
|
|
14584
14898
|
function resolvePjanglerRoot3() {
|
|
14585
14899
|
let dir = dirname17(fileURLToPath6(import.meta.url));
|
|
14586
14900
|
while (dir !== dirname17(dir)) {
|
|
14587
|
-
if (existsSync18(
|
|
14901
|
+
if (existsSync18(join27(dir, "package.json")) && existsSync18(join27(dir, "templates", "commonproject", "copier.yml"))) {
|
|
14588
14902
|
return dir;
|
|
14589
14903
|
}
|
|
14590
14904
|
dir = dirname17(dir);
|
|
@@ -14879,12 +15193,12 @@ server.registerTool(
|
|
|
14879
15193
|
);
|
|
14880
15194
|
const pjanglerRoot = resolvePjanglerRoot3();
|
|
14881
15195
|
const projectSlug = validateSafePathSegment(input.projectSlug ?? slugify(input.projectName), "Project slug");
|
|
14882
|
-
const explicitTargetDir = input.targetDir ?
|
|
14883
|
-
const parentDir =
|
|
15196
|
+
const explicitTargetDir = input.targetDir ? resolve22(input.targetDir) : void 0;
|
|
15197
|
+
const parentDir = resolve22(input.parentDir ?? (explicitTargetDir ? dirname17(explicitTargetDir) : process.cwd()));
|
|
14884
15198
|
if (!existsSync18(parentDir) || !statSync7(parentDir).isDirectory()) throw new Error(`Parent directory does not exist: ${parentDir}`);
|
|
14885
15199
|
const targetDir = resolveContainedPath(
|
|
14886
15200
|
parentDir,
|
|
14887
|
-
explicitTargetDir ??
|
|
15201
|
+
explicitTargetDir ?? join27(parentDir, projectSlug),
|
|
14888
15202
|
"Bootstrap target"
|
|
14889
15203
|
);
|
|
14890
15204
|
const overwrite = input.overwrite ?? input.force ?? false;
|
|
@@ -15007,7 +15321,7 @@ server.registerTool(
|
|
|
15007
15321
|
registryPath: input.registryPath,
|
|
15008
15322
|
force: input.force ?? false,
|
|
15009
15323
|
overwrite: input.force ?? false,
|
|
15010
|
-
scaffold: !(input.targetDir && existsSync18(
|
|
15324
|
+
scaffold: !(input.targetDir && existsSync18(join27(resolve22(input.targetDir), ".git")))
|
|
15011
15325
|
});
|
|
15012
15326
|
if (!input.apply) return asText(publicCompositeProjectResponse(publicProjectPlan(plan), plan));
|
|
15013
15327
|
const preflight = await preflightProjectApply(plan, resolvePjanglerRoot3());
|