@delorenj/pjangler 1.5.13 → 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 +1157 -850
- package/dist/mcp-server.js +1040 -736
- 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",
|
|
@@ -8434,7 +8662,7 @@ var BMAD_DIST_TAGS_TTL_MS = 60 * 60 * 1e3;
|
|
|
8434
8662
|
var DEFAULT_BMAD_MODULES = ["bmm", "bmb", "cis"];
|
|
8435
8663
|
var BMAD_INSTALL_TOOLS = SUPPORTED_BMAD_TOOLS;
|
|
8436
8664
|
function manifestBmadModules(repoRoot) {
|
|
8437
|
-
const manifestPath =
|
|
8665
|
+
const manifestPath = join7(repoRoot, "_bmad", "_config", "manifest.yaml");
|
|
8438
8666
|
const raw = safeReadText2(manifestPath);
|
|
8439
8667
|
if (raw === null) return { status: "absent" };
|
|
8440
8668
|
try {
|
|
@@ -8443,14 +8671,16 @@ function manifestBmadModules(repoRoot) {
|
|
|
8443
8671
|
return { status: "invalid", error: `${manifestPath} must define a modules array` };
|
|
8444
8672
|
}
|
|
8445
8673
|
const declared = [];
|
|
8674
|
+
let hasG33 = false;
|
|
8446
8675
|
for (const entry of parsed.modules) {
|
|
8447
8676
|
const name = typeof entry === "string" ? entry : entry && typeof entry === "object" ? entry.name : void 0;
|
|
8448
8677
|
if (typeof name !== "string" || !/^[A-Za-z0-9][A-Za-z0-9_-]*$/.test(name)) {
|
|
8449
8678
|
return { status: "invalid", error: `${manifestPath} contains an invalid module entry` };
|
|
8450
8679
|
}
|
|
8451
|
-
if (name
|
|
8680
|
+
if (name === "g33") hasG33 = true;
|
|
8681
|
+
if (name !== "core" && name !== "custom" && name !== "g33") declared.push(name);
|
|
8452
8682
|
}
|
|
8453
|
-
return { status: "valid", modules: Array.from(new Set(declared)) };
|
|
8683
|
+
return { status: "valid", modules: Array.from(new Set(declared)), hasG33 };
|
|
8454
8684
|
} catch (error) {
|
|
8455
8685
|
return {
|
|
8456
8686
|
status: "invalid",
|
|
@@ -8458,24 +8688,24 @@ function manifestBmadModules(repoRoot) {
|
|
|
8458
8688
|
};
|
|
8459
8689
|
}
|
|
8460
8690
|
}
|
|
8461
|
-
function
|
|
8462
|
-
const raw = safeReadText2(
|
|
8691
|
+
function configuredBmadModuleSelection(repoRoot) {
|
|
8692
|
+
const raw = safeReadText2(join7(repoRoot, "_bmad", "config.toml"));
|
|
8463
8693
|
if (raw === null) return void 0;
|
|
8464
8694
|
const modules = [...raw.matchAll(/^\[modules\.([A-Za-z0-9][A-Za-z0-9_-]*)\]\s*$/gm)].map((match) => match[1]);
|
|
8465
|
-
return Array.from(new Set(modules));
|
|
8695
|
+
return { modules: Array.from(new Set(modules)).filter((name) => name !== "g33"), hasG33: modules.includes("g33") };
|
|
8466
8696
|
}
|
|
8467
8697
|
function selectedBmadModules(repoRoot) {
|
|
8468
8698
|
const manifest = manifestBmadModules(repoRoot);
|
|
8469
8699
|
if (manifest.status === "valid") return manifest.modules;
|
|
8470
8700
|
if (manifest.status === "invalid") throw new Error(manifest.error);
|
|
8471
|
-
return
|
|
8701
|
+
return configuredBmadModuleSelection(repoRoot)?.modules ?? [...DEFAULT_BMAD_MODULES];
|
|
8472
8702
|
}
|
|
8473
8703
|
function requiredBmadSentinels(repoRoot, modules = selectedBmadModules(repoRoot)) {
|
|
8474
8704
|
return [
|
|
8475
|
-
|
|
8476
|
-
|
|
8477
|
-
|
|
8478
|
-
...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"))
|
|
8479
8709
|
];
|
|
8480
8710
|
}
|
|
8481
8711
|
function canonicalBmadProjectName(repoRoot) {
|
|
@@ -8487,7 +8717,7 @@ function bmadProjectNameIssues(repoRoot) {
|
|
|
8487
8717
|
const expected = canonicalBmadProjectName(repoRoot);
|
|
8488
8718
|
const paths = [];
|
|
8489
8719
|
const details = [];
|
|
8490
|
-
const configToml =
|
|
8720
|
+
const configToml = join7(repoRoot, "_bmad", "config.toml");
|
|
8491
8721
|
const tomlText = safeReadText2(configToml);
|
|
8492
8722
|
const tomlMatch = tomlText?.match(/^project_name\s*=\s*"((?:\\.|[^"\\])*)"\s*$/m);
|
|
8493
8723
|
let tomlName;
|
|
@@ -8502,16 +8732,16 @@ function bmadProjectNameIssues(repoRoot) {
|
|
|
8502
8732
|
paths.push(configToml);
|
|
8503
8733
|
details.push(`_bmad/config.toml project_name must be ${JSON.stringify(expected)}`);
|
|
8504
8734
|
}
|
|
8505
|
-
const bmadRoot =
|
|
8506
|
-
if (
|
|
8735
|
+
const bmadRoot = join7(repoRoot, "_bmad");
|
|
8736
|
+
if (existsSync4(bmadRoot)) {
|
|
8507
8737
|
let declared;
|
|
8508
8738
|
try {
|
|
8509
8739
|
declared = selectedBmadModules(repoRoot);
|
|
8510
8740
|
} catch {
|
|
8511
|
-
declared =
|
|
8741
|
+
declared = readdirSync4(bmadRoot);
|
|
8512
8742
|
}
|
|
8513
8743
|
for (const name of new Set(declared)) {
|
|
8514
|
-
const configPath =
|
|
8744
|
+
const configPath = join7(bmadRoot, name, "config.yaml");
|
|
8515
8745
|
const raw = safeReadText2(configPath);
|
|
8516
8746
|
if (raw === null) continue;
|
|
8517
8747
|
let actual;
|
|
@@ -8531,7 +8761,7 @@ function bmadProjectNameIssues(repoRoot) {
|
|
|
8531
8761
|
function isCurrentActivation(path, activations) {
|
|
8532
8762
|
if (activations.owned.has(path)) return true;
|
|
8533
8763
|
try {
|
|
8534
|
-
if (activations.owned.has(
|
|
8764
|
+
if (activations.owned.has(join7(realpathSync3(dirname5(path)), basename3(path)))) return true;
|
|
8535
8765
|
return activations.targets.has(realpathSync3(path));
|
|
8536
8766
|
} catch {
|
|
8537
8767
|
return false;
|
|
@@ -8544,25 +8774,25 @@ async function skillActivations(ctx) {
|
|
|
8544
8774
|
function evictLegacyBmadPackState(ctx, changedFiles, activations) {
|
|
8545
8775
|
const details = [];
|
|
8546
8776
|
const skillDirs = [
|
|
8547
|
-
|
|
8548
|
-
...SUPPORTED_CLI_ROOTS.map((root) =>
|
|
8777
|
+
join7(ctx.repoRoot, ".agents", "skills"),
|
|
8778
|
+
...SUPPORTED_CLI_ROOTS.map((root) => join7(ctx.repoRoot, root, "skills"))
|
|
8549
8779
|
];
|
|
8550
8780
|
for (const dir of skillDirs) {
|
|
8551
8781
|
const dirStat = lstatIfPresent(dir);
|
|
8552
8782
|
if (!dirStat || dirStat.isSymbolicLink() || !dirStat.isDirectory()) continue;
|
|
8553
8783
|
let names;
|
|
8554
8784
|
try {
|
|
8555
|
-
names =
|
|
8785
|
+
names = readdirSync4(dir);
|
|
8556
8786
|
} catch {
|
|
8557
8787
|
continue;
|
|
8558
8788
|
}
|
|
8559
8789
|
for (const name of names) {
|
|
8560
8790
|
if (!name.startsWith(BMAD_SKILL_NAME_PREFIX)) continue;
|
|
8561
|
-
const path =
|
|
8791
|
+
const path = join7(dir, name);
|
|
8562
8792
|
if (!lstatIfPresent(path)?.isSymbolicLink()) continue;
|
|
8563
8793
|
if (isCurrentActivation(path, activations)) continue;
|
|
8564
8794
|
changedFiles.push(path);
|
|
8565
|
-
details.push(`removed retired BMAD pack symlink ${
|
|
8795
|
+
details.push(`removed retired BMAD pack symlink ${relative5(ctx.repoRoot, path)}`);
|
|
8566
8796
|
if (!ctx.dryRun) unlinkSync3(path);
|
|
8567
8797
|
}
|
|
8568
8798
|
}
|
|
@@ -8570,7 +8800,7 @@ function evictLegacyBmadPackState(ctx, changedFiles, activations) {
|
|
|
8570
8800
|
}
|
|
8571
8801
|
function bmadInstallerInvocation(version = BMAD_INSTALLER_VERSION) {
|
|
8572
8802
|
const explicit = process.env.PJ_BMAD_INSTALLER?.trim();
|
|
8573
|
-
if (explicit) return { command:
|
|
8803
|
+
if (explicit) return { command: resolve6(explicit), prefixArgs: [] };
|
|
8574
8804
|
return {
|
|
8575
8805
|
command: "npx",
|
|
8576
8806
|
prefixArgs: ["-y", `${BMAD_NPM_PACKAGE}@${version}`]
|
|
@@ -8597,7 +8827,7 @@ function bmadInstallDisplay(repoRoot, modules = selectedBmadModules(repoRoot), v
|
|
|
8597
8827
|
}
|
|
8598
8828
|
function preflightBmadLifecycle(_ctx) {
|
|
8599
8829
|
const invocation = bmadInstallerInvocation();
|
|
8600
|
-
const probe =
|
|
8830
|
+
const probe = spawnSync4(invocation.command, [...invocation.prefixArgs, "--version"], {
|
|
8601
8831
|
encoding: "utf8",
|
|
8602
8832
|
timeout: 3e4
|
|
8603
8833
|
});
|
|
@@ -8619,14 +8849,17 @@ ${probe.stderr ?? ""}`;
|
|
|
8619
8849
|
}
|
|
8620
8850
|
return { ok: true };
|
|
8621
8851
|
}
|
|
8622
|
-
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 };
|
|
8623
8856
|
const invocation = bmadInstallerInvocation(version);
|
|
8624
|
-
const result2 =
|
|
8857
|
+
const result2 = spawnSync4(invocation.command, [...invocation.prefixArgs, ...bmadInstallerArgs(repoRoot, modules)], { encoding: "utf8" });
|
|
8625
8858
|
if (result2.status !== 0) {
|
|
8626
8859
|
const stdoutTail = result2.stdout?.trim().split("\n").slice(-12).join("\n");
|
|
8627
8860
|
return { ok: false, error: result2.stderr?.trim() || stdoutTail || result2.error?.message || `installer exited ${result2.status}` };
|
|
8628
8861
|
}
|
|
8629
|
-
return { ok: true };
|
|
8862
|
+
return { ok: true, companion: await runG33Companion(repoRoot, false, "bmad-install") };
|
|
8630
8863
|
}
|
|
8631
8864
|
function bmadChannelTarget(distTags) {
|
|
8632
8865
|
const next = distTags?.[BMAD_TARGET_CHANNEL];
|
|
@@ -8635,7 +8868,7 @@ function bmadChannelTarget(distTags) {
|
|
|
8635
8868
|
return next ? { version: next, channel: BMAD_TARGET_CHANNEL } : void 0;
|
|
8636
8869
|
}
|
|
8637
8870
|
function readInstalledBmadVersion(repoRoot) {
|
|
8638
|
-
const raw = safeReadText2(
|
|
8871
|
+
const raw = safeReadText2(join7(repoRoot, "_bmad", "_config", "manifest.yaml"));
|
|
8639
8872
|
if (!raw) return void 0;
|
|
8640
8873
|
try {
|
|
8641
8874
|
const parsed = YAML2.parse(raw);
|
|
@@ -8646,8 +8879,8 @@ function readInstalledBmadVersion(repoRoot) {
|
|
|
8646
8879
|
}
|
|
8647
8880
|
}
|
|
8648
8881
|
function bmadCachePath(homeDir) {
|
|
8649
|
-
const cacheRoot = process.env.XDG_CACHE_HOME?.trim() ||
|
|
8650
|
-
return
|
|
8882
|
+
const cacheRoot = process.env.XDG_CACHE_HOME?.trim() || join7(homeDir, ".cache");
|
|
8883
|
+
return join7(cacheRoot, "pjangler", "bmad-dist-tags.json");
|
|
8651
8884
|
}
|
|
8652
8885
|
function readBmadDistTagsCache(homeDir) {
|
|
8653
8886
|
const raw = safeReadText2(bmadCachePath(homeDir));
|
|
@@ -8662,7 +8895,7 @@ function readBmadDistTagsCache(homeDir) {
|
|
|
8662
8895
|
return void 0;
|
|
8663
8896
|
}
|
|
8664
8897
|
function fetchBmadDistTags() {
|
|
8665
|
-
const result2 =
|
|
8898
|
+
const result2 = spawnSync4("npm", ["view", BMAD_NPM_PACKAGE, "dist-tags", "--json"], {
|
|
8666
8899
|
encoding: "utf8",
|
|
8667
8900
|
timeout: 8e3
|
|
8668
8901
|
});
|
|
@@ -8814,8 +9047,8 @@ var UNSUPPORTED_BMAD_ROOTS = {
|
|
|
8814
9047
|
".zencoder": "zencoder"
|
|
8815
9048
|
};
|
|
8816
9049
|
function unsupportedRootAttestation(repoRoot, rootName) {
|
|
8817
|
-
const root =
|
|
8818
|
-
const stat =
|
|
9050
|
+
const root = join7(repoRoot, rootName);
|
|
9051
|
+
const stat = lstatSync6(root);
|
|
8819
9052
|
if (!stat.isDirectory() || stat.isSymbolicLink()) return { safe: false, reason: `${rootName} is not a regular generated directory` };
|
|
8820
9053
|
const installerTool = UNSUPPORTED_BMAD_ROOTS[rootName];
|
|
8821
9054
|
if (!installedBmadTools(repoRoot).has(installerTool)) {
|
|
@@ -8834,8 +9067,8 @@ var OPTIONAL_TEMPLATE_SCRIPTS = [
|
|
|
8834
9067
|
function optionalTemplateScriptIssues(ctx) {
|
|
8835
9068
|
const issues = [];
|
|
8836
9069
|
for (const rel of OPTIONAL_TEMPLATE_SCRIPTS) {
|
|
8837
|
-
const path =
|
|
8838
|
-
if (!
|
|
9070
|
+
const path = join7(ctx.repoRoot, rel);
|
|
9071
|
+
if (!existsSync4(path)) continue;
|
|
8839
9072
|
const expected = templateCommonProjectText(ctx, rel);
|
|
8840
9073
|
if (expected === void 0) continue;
|
|
8841
9074
|
if (safeReadText2(path) !== expected) issues.push(`${rel} is present but has drifted from the shipped template`);
|
|
@@ -8845,8 +9078,8 @@ function optionalTemplateScriptIssues(ctx) {
|
|
|
8845
9078
|
function refreshOptionalTemplateScripts(ctx) {
|
|
8846
9079
|
const changed = [];
|
|
8847
9080
|
for (const rel of OPTIONAL_TEMPLATE_SCRIPTS) {
|
|
8848
|
-
const path =
|
|
8849
|
-
if (!
|
|
9081
|
+
const path = join7(ctx.repoRoot, rel);
|
|
9082
|
+
if (!existsSync4(path)) continue;
|
|
8850
9083
|
const expected = templateCommonProjectText(ctx, rel);
|
|
8851
9084
|
if (expected === void 0 || safeReadText2(path) === expected) continue;
|
|
8852
9085
|
changed.push(path);
|
|
@@ -8863,14 +9096,14 @@ function createMiseChecks() {
|
|
|
8863
9096
|
id: "mise.config-root",
|
|
8864
9097
|
title: "mise config_root + AGENTS link hooks",
|
|
8865
9098
|
audit: (ctx) => {
|
|
8866
|
-
const misePath =
|
|
8867
|
-
if (!
|
|
9099
|
+
const misePath = join7(ctx.repoRoot, "mise.toml");
|
|
9100
|
+
if (!existsSync4(misePath)) {
|
|
8868
9101
|
return { id: "mise.config-root", title: "mise config_root + AGENTS link hooks", status: "fail", summary: "mise.toml missing", details: [], fixable: true };
|
|
8869
9102
|
}
|
|
8870
9103
|
const text = readText(misePath);
|
|
8871
9104
|
const details = [];
|
|
8872
|
-
const linkAgentfilesPath =
|
|
8873
|
-
if (!
|
|
9105
|
+
const linkAgentfilesPath = join7(ctx.repoRoot, ".mise", "scripts", "link-agentfiles.sh");
|
|
9106
|
+
if (!existsSync4(linkAgentfilesPath)) details.push(".mise/scripts/link-agentfiles.sh missing");
|
|
8874
9107
|
const toml = [];
|
|
8875
9108
|
const manual = [];
|
|
8876
9109
|
const parsed = parsedOrUndefined(text);
|
|
@@ -8917,10 +9150,10 @@ function createMiseChecks() {
|
|
|
8917
9150
|
};
|
|
8918
9151
|
},
|
|
8919
9152
|
migrate: (ctx, finding2) => {
|
|
8920
|
-
const path =
|
|
9153
|
+
const path = join7(ctx.repoRoot, "mise.toml");
|
|
8921
9154
|
const changedFiles = [];
|
|
8922
9155
|
const details = [];
|
|
8923
|
-
if (!
|
|
9156
|
+
if (!existsSync4(path)) {
|
|
8924
9157
|
if (ensureMiseTomlFromTemplate(ctx, changedFiles) === null) {
|
|
8925
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: [] };
|
|
8926
9159
|
}
|
|
@@ -8936,7 +9169,7 @@ function createMiseChecks() {
|
|
|
8936
9169
|
if (!ctx.dryRun) writeText(path, plan.text);
|
|
8937
9170
|
text = plan.text;
|
|
8938
9171
|
}
|
|
8939
|
-
const linkAgentfilesPath =
|
|
9172
|
+
const linkAgentfilesPath = join7(ctx.repoRoot, ".mise", "scripts", "link-agentfiles.sh");
|
|
8940
9173
|
const expectedScript = templateLinkAgentfilesScript(ctx);
|
|
8941
9174
|
if (expectedScript === void 0) {
|
|
8942
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: [] };
|
|
@@ -8974,13 +9207,13 @@ function createMiseChecks() {
|
|
|
8974
9207
|
title: "managed mise versioning block",
|
|
8975
9208
|
audit: (ctx) => {
|
|
8976
9209
|
const details = [];
|
|
8977
|
-
const misePath =
|
|
8978
|
-
const versioningPath =
|
|
8979
|
-
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");
|
|
8980
9213
|
const text = safeReadText2(misePath);
|
|
8981
9214
|
if (!text?.includes("# >>> mise-versioning >>>")) details.push("mise versioning managed block missing");
|
|
8982
|
-
if (!
|
|
8983
|
-
if (!
|
|
9215
|
+
if (!existsSync4(versioningPath)) details.push(".mise/scripts/versioning.sh missing");
|
|
9216
|
+
if (!existsSync4(manifestPath)) details.push(".mise/version-files.conf missing");
|
|
8984
9217
|
const conf = safeReadText2(manifestPath);
|
|
8985
9218
|
const script = safeReadText2(versioningPath);
|
|
8986
9219
|
if (conf !== null && script !== null) {
|
|
@@ -9002,8 +9235,8 @@ function createMiseChecks() {
|
|
|
9002
9235
|
migrate: (ctx, finding2) => {
|
|
9003
9236
|
const changedFiles = [];
|
|
9004
9237
|
const details = [];
|
|
9005
|
-
const misePath =
|
|
9006
|
-
if (!
|
|
9238
|
+
const misePath = join7(ctx.repoRoot, "mise.toml");
|
|
9239
|
+
if (!existsSync4(misePath)) {
|
|
9007
9240
|
if (ensureMiseTomlFromTemplate(ctx, changedFiles) === null) {
|
|
9008
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: [] };
|
|
9009
9242
|
}
|
|
@@ -9020,7 +9253,7 @@ function createMiseChecks() {
|
|
|
9020
9253
|
if (!changedFiles.includes(misePath)) changedFiles.push(misePath);
|
|
9021
9254
|
if (!ctx.dryRun) writeText(misePath, versioningPlan.text);
|
|
9022
9255
|
}
|
|
9023
|
-
const versioningPath =
|
|
9256
|
+
const versioningPath = join7(ctx.repoRoot, ".mise", "scripts", "versioning.sh");
|
|
9024
9257
|
const expectedScript = templateVersioningScript(ctx);
|
|
9025
9258
|
if (expectedScript === void 0) {
|
|
9026
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: [] };
|
|
@@ -9032,7 +9265,7 @@ function createMiseChecks() {
|
|
|
9032
9265
|
chmodSync(versioningPath, 493);
|
|
9033
9266
|
}
|
|
9034
9267
|
}
|
|
9035
|
-
const manifestPath =
|
|
9268
|
+
const manifestPath = join7(ctx.repoRoot, ".mise", "version-files.conf");
|
|
9036
9269
|
const expectedManifest = templateVersionFilesConf(ctx, ctx.repoRoot);
|
|
9037
9270
|
if (safeReadText2(manifestPath) !== expectedManifest) {
|
|
9038
9271
|
changedFiles.push(manifestPath);
|
|
@@ -9062,7 +9295,7 @@ function planVersioningBlock(original) {
|
|
|
9062
9295
|
let replaced = [...VERSIONING_TASK_NAMES];
|
|
9063
9296
|
try {
|
|
9064
9297
|
if (oldBlock) {
|
|
9065
|
-
replaced = Object.keys(
|
|
9298
|
+
replaced = Object.keys(record2(record2(parseToml2(oldBlock)).tasks));
|
|
9066
9299
|
text = text.replace(blockPattern, VERSIONING_BLOCK);
|
|
9067
9300
|
} else {
|
|
9068
9301
|
const statements = scanToml(text);
|
|
@@ -9085,7 +9318,7 @@ function planVersioningBlock(original) {
|
|
|
9085
9318
|
const verdict = checkRewrite(expected, text);
|
|
9086
9319
|
return verdict.ok ? { text } : { text: original, refused: verdict.reason };
|
|
9087
9320
|
}
|
|
9088
|
-
var
|
|
9321
|
+
var record2 = (value) => isTable(value) ? value : {};
|
|
9089
9322
|
var RETIRED_SKILL_SCRIPTS = [
|
|
9090
9323
|
SYNC_SKILLS_SCRIPT_REL,
|
|
9091
9324
|
PROVISION_PACKS_SCRIPT_REL,
|
|
@@ -9094,7 +9327,7 @@ var RETIRED_SKILL_SCRIPTS = [
|
|
|
9094
9327
|
".mise/scripts/unlink-project-skills-from-clis.sh"
|
|
9095
9328
|
];
|
|
9096
9329
|
function retiredSkillsScripts(ctx) {
|
|
9097
|
-
return RETIRED_SKILL_SCRIPTS.map((path) =>
|
|
9330
|
+
return RETIRED_SKILL_SCRIPTS.map((path) => join7(ctx.repoRoot, path)).filter((path) => Boolean(lstatIfPresent(path)));
|
|
9098
9331
|
}
|
|
9099
9332
|
function skillsWiringFindings(text) {
|
|
9100
9333
|
if (text === null) return { fixable: ["mise.toml is missing; add the explicit skills:sync task"], manual: [] };
|
|
@@ -9107,26 +9340,26 @@ function skillsWiringFindings(text) {
|
|
|
9107
9340
|
parsed = baseline.parsed;
|
|
9108
9341
|
fixable.push("mise.toml repeats the close of a [hooks] array (an earlier pjangler run wrote it); migrate repairs it");
|
|
9109
9342
|
}
|
|
9110
|
-
const
|
|
9111
|
-
const tasks =
|
|
9343
|
+
const record3 = (value) => isTable(value) ? value : {};
|
|
9344
|
+
const tasks = record3(parsed.tasks), task = record3(tasks[SKILLS_SYNC_TASK]);
|
|
9112
9345
|
const run = Array.isArray(task.run) && task.run.length === 1 ? task.run[0] : task.run;
|
|
9113
9346
|
if (typeof run !== "string" || !/^skillex\s+sync\s+--scope\s+project\s+--project\s+(['"])\{\{config_root\}\}\1\s*$/.test(run)) {
|
|
9114
9347
|
fixable.push("skills:sync must explicitly target {{config_root}} with the Node CLI");
|
|
9115
9348
|
}
|
|
9116
|
-
const pin =
|
|
9117
|
-
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;
|
|
9118
9351
|
if (pinned !== SKILLEX_TOOL_VERSION) {
|
|
9119
9352
|
fixable.push(`skills:sync must pin @delorenj/skillex ${SKILLEX_TOOL_VERSION} in its own tools table`);
|
|
9120
9353
|
} else if (typeof pin === "string") {
|
|
9121
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 }`);
|
|
9122
|
-
} else if (
|
|
9355
|
+
} else if (record3(pin).allow_low_downloads !== true) {
|
|
9123
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)`);
|
|
9124
9357
|
}
|
|
9125
9358
|
const retired = /sync-skills\.py|provision-(?:packs|bmad-skills)\.py|skills[:\-]provision/;
|
|
9126
9359
|
const renames = new Map(RETIRED_TASK_RENAMES);
|
|
9127
9360
|
const replaced = /* @__PURE__ */ new Set([...RETIRED_LINK_TASKS, LINK_AGENTFILES_TASK, SKILLS_SYNC_TASK]);
|
|
9128
9361
|
for (const [name, entry] of Object.entries(tasks)) {
|
|
9129
|
-
if (!retired.test(name) && !retired.test(JSON.stringify(
|
|
9362
|
+
if (!retired.test(name) && !retired.test(JSON.stringify(record3(entry).run ?? ""))) continue;
|
|
9130
9363
|
if (replaced.has(renames.get(name) ?? name)) {
|
|
9131
9364
|
if (!fixable.includes("Retire Python skill sync/provision tasks from mise.toml")) fixable.push("Retire Python skill sync/provision tasks from mise.toml");
|
|
9132
9365
|
} else {
|
|
@@ -9154,7 +9387,7 @@ function createAgentHooksChecks() {
|
|
|
9154
9387
|
title: "Skillex project skills",
|
|
9155
9388
|
audit: async (ctx) => {
|
|
9156
9389
|
const finding2 = await auditProjectSkills(ctx);
|
|
9157
|
-
const mise = safeReadText2(
|
|
9390
|
+
const mise = safeReadText2(join7(ctx.repoRoot, "mise.toml"));
|
|
9158
9391
|
const wiring = skillsWiringIssues(mise);
|
|
9159
9392
|
const retired = retiredSkillsScripts(ctx);
|
|
9160
9393
|
const unsafe = retired.filter((path) => lstatIfPresent(path)?.isDirectory());
|
|
@@ -9171,7 +9404,7 @@ function createAgentHooksChecks() {
|
|
|
9171
9404
|
const changedFiles = [];
|
|
9172
9405
|
const details = [];
|
|
9173
9406
|
const result2 = (status, summary) => ({ id: finding2.id, title: finding2.title, status, summary, changedFiles: [...new Set(changedFiles)], details });
|
|
9174
|
-
let currentMise = safeReadText2(
|
|
9407
|
+
let currentMise = safeReadText2(join7(ctx.repoRoot, "mise.toml"));
|
|
9175
9408
|
if (currentMise === null) {
|
|
9176
9409
|
const template = templateCommonProjectText(ctx, "mise.toml.jinja");
|
|
9177
9410
|
if (template === void 0) return result2("blocked", "Generated-project mise template is unavailable");
|
|
@@ -9188,7 +9421,7 @@ function createAgentHooksChecks() {
|
|
|
9188
9421
|
changedFiles.push(path);
|
|
9189
9422
|
if (!ctx.dryRun) unlinkSync3(path);
|
|
9190
9423
|
}
|
|
9191
|
-
const misePath =
|
|
9424
|
+
const misePath = join7(ctx.repoRoot, "mise.toml");
|
|
9192
9425
|
const nextMise = upsertLinkAgentfilesBlock(currentMise, ctx);
|
|
9193
9426
|
if (safeReadText2(misePath) !== nextMise) {
|
|
9194
9427
|
changedFiles.push(misePath);
|
|
@@ -9214,9 +9447,9 @@ function createAgentHooksChecks() {
|
|
|
9214
9447
|
id: "sot.agent-symlinks",
|
|
9215
9448
|
title: "AGENTS/CLAUDE/GEMINI symlink contract",
|
|
9216
9449
|
audit: (ctx) => {
|
|
9217
|
-
const agentsPath =
|
|
9218
|
-
if (!
|
|
9219
|
-
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)));
|
|
9220
9453
|
if (fallbackSources.length === 0) {
|
|
9221
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 };
|
|
9222
9455
|
}
|
|
@@ -9231,7 +9464,7 @@ function createAgentHooksChecks() {
|
|
|
9231
9464
|
}
|
|
9232
9465
|
const details = [];
|
|
9233
9466
|
for (const file of ["CLAUDE.md", "GEMINI.md"]) {
|
|
9234
|
-
const full =
|
|
9467
|
+
const full = join7(ctx.repoRoot, file);
|
|
9235
9468
|
const target = readSymlinkTarget(full);
|
|
9236
9469
|
if (target !== "AGENTS.md") details.push(`${file} should be a symlink to AGENTS.md`);
|
|
9237
9470
|
}
|
|
@@ -9255,7 +9488,7 @@ function createAgentHooksChecks() {
|
|
|
9255
9488
|
return { id: finding2.id, title: finding2.title, status: "blocked", summary: "AGENTS.md missing; cannot derive canonical agent file", changedFiles, details: [bootstrap.blocked] };
|
|
9256
9489
|
}
|
|
9257
9490
|
for (const file of ["CLAUDE.md", "GEMINI.md"]) {
|
|
9258
|
-
const full =
|
|
9491
|
+
const full = join7(ctx.repoRoot, file);
|
|
9259
9492
|
const result2 = ensureSymlink(full, "AGENTS.md", ctx.dryRun);
|
|
9260
9493
|
if (result2.blocked) blockedDetails.push(result2.blocked);
|
|
9261
9494
|
if (result2.changed) changedFiles.push(full);
|
|
@@ -9282,7 +9515,7 @@ function createProjectJsonChecks() {
|
|
|
9282
9515
|
const changedFiles = [];
|
|
9283
9516
|
const blockedDetails = [];
|
|
9284
9517
|
const droppedDetails = [];
|
|
9285
|
-
const path =
|
|
9518
|
+
const path = join7(ctx.repoRoot, ".project.json");
|
|
9286
9519
|
const existing = readProjectJson(ctx) ?? {};
|
|
9287
9520
|
const canonical = canonicalProjectJson(ctx);
|
|
9288
9521
|
const preservedDetails = [];
|
|
@@ -9314,11 +9547,11 @@ function createProjectJsonChecks() {
|
|
|
9314
9547
|
changedFiles.push(path);
|
|
9315
9548
|
if (!ctx.dryRun) writeText(path, expected);
|
|
9316
9549
|
}
|
|
9317
|
-
const planeJson =
|
|
9318
|
-
if (
|
|
9550
|
+
const planeJson = join7(ctx.repoRoot, ".plane.json");
|
|
9551
|
+
if (existsSync4(planeJson)) {
|
|
9319
9552
|
const backup = `${planeJson}.migrated-backup`;
|
|
9320
|
-
if (
|
|
9321
|
-
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`);
|
|
9322
9555
|
} else {
|
|
9323
9556
|
changedFiles.push(backup);
|
|
9324
9557
|
if (!ctx.dryRun) renameSync2(planeJson, backup);
|
|
@@ -9346,10 +9579,10 @@ function createMiseOpInjectChecks() {
|
|
|
9346
9579
|
const details = [];
|
|
9347
9580
|
let envOpNeedsHands = false;
|
|
9348
9581
|
const optedOut = envMaterializationOptedOut(ctx);
|
|
9349
|
-
const envOpPath =
|
|
9350
|
-
const envOpExists =
|
|
9582
|
+
const envOpPath = join7(ctx.repoRoot, ".env.op");
|
|
9583
|
+
const envOpExists = existsSync4(envOpPath);
|
|
9351
9584
|
const envOp = envOpExists ? readText(envOpPath) : void 0;
|
|
9352
|
-
const gitignore = safeReadText2(
|
|
9585
|
+
const gitignore = safeReadText2(join7(ctx.repoRoot, ".gitignore"));
|
|
9353
9586
|
if (!envOpExists) {
|
|
9354
9587
|
details.push(".env.op missing");
|
|
9355
9588
|
} else if (!envOp?.trim()) {
|
|
@@ -9369,7 +9602,7 @@ function createMiseOpInjectChecks() {
|
|
|
9369
9602
|
if (!gitignore?.includes(".env\n") && !gitignore?.includes(".env\r\n")) details.push(".gitignore should ignore .env");
|
|
9370
9603
|
if (!gitignore?.includes(".env.*")) details.push(".gitignore should ignore .env.*");
|
|
9371
9604
|
if (!gitignore?.includes("!.env.op")) details.push(".gitignore should unignore .env.op");
|
|
9372
|
-
const misePath =
|
|
9605
|
+
const misePath = join7(ctx.repoRoot, "mise.toml");
|
|
9373
9606
|
const miseText = safeReadText2(misePath);
|
|
9374
9607
|
if (!miseText) {
|
|
9375
9608
|
details.push("mise.toml missing for .env materialization hook");
|
|
@@ -9392,15 +9625,15 @@ function createMiseOpInjectChecks() {
|
|
|
9392
9625
|
else if (plan.text !== miseText && !mise.length) mise.push(describeRewrite(miseText, plan.text));
|
|
9393
9626
|
details.push(...mise);
|
|
9394
9627
|
}
|
|
9395
|
-
const materializePath =
|
|
9628
|
+
const materializePath = join7(ctx.repoRoot, MATERIALIZE_ENV_SCRIPT_REL);
|
|
9396
9629
|
const expectedMaterializer = templateMaterializeEnvScript(ctx);
|
|
9397
9630
|
if (optedOut) {
|
|
9398
|
-
if (
|
|
9631
|
+
if (existsSync4(materializePath)) details.push(`${MATERIALIZE_ENV_SCRIPT_REL} is present, but .project.json declares secrets.materialize_env: false`);
|
|
9399
9632
|
} else if (!expectedMaterializer) {
|
|
9400
9633
|
details.push("pjangler package is missing the managed materialize-env.sh source");
|
|
9401
9634
|
} else if (safeReadText2(materializePath) !== expectedMaterializer) {
|
|
9402
9635
|
details.push(`${MATERIALIZE_ENV_SCRIPT_REL} missing or drifted`);
|
|
9403
|
-
} else if ((
|
|
9636
|
+
} else if ((lstatSync6(materializePath).mode & 73) === 0) {
|
|
9404
9637
|
details.push(`${MATERIALIZE_ENV_SCRIPT_REL} is not executable`);
|
|
9405
9638
|
}
|
|
9406
9639
|
return {
|
|
@@ -9416,13 +9649,13 @@ function createMiseOpInjectChecks() {
|
|
|
9416
9649
|
const changedFiles = [];
|
|
9417
9650
|
const details = [];
|
|
9418
9651
|
let envOpNeedsHands = false;
|
|
9419
|
-
const envOpPath =
|
|
9420
|
-
const canonicalEnvOpPath =
|
|
9421
|
-
if (!
|
|
9652
|
+
const envOpPath = join7(ctx.repoRoot, ".env.op");
|
|
9653
|
+
const canonicalEnvOpPath = join7(ctx.pjanglerRoot, "templates", "commonproject", "template", ".env.op");
|
|
9654
|
+
if (!existsSync4(canonicalEnvOpPath)) {
|
|
9422
9655
|
return { id: finding2.id, title: finding2.title, status: "blocked", summary: "pjangler package is missing the neutral .env.op template", changedFiles: [], details: [] };
|
|
9423
9656
|
}
|
|
9424
9657
|
const canonicalEnvOp = readText(canonicalEnvOpPath);
|
|
9425
|
-
if (!
|
|
9658
|
+
if (!existsSync4(envOpPath) || !readText(envOpPath).trim()) {
|
|
9426
9659
|
changedFiles.push(envOpPath);
|
|
9427
9660
|
if (!ctx.dryRun) writeText(envOpPath, canonicalEnvOp);
|
|
9428
9661
|
} else {
|
|
@@ -9446,7 +9679,7 @@ function createMiseOpInjectChecks() {
|
|
|
9446
9679
|
}
|
|
9447
9680
|
}
|
|
9448
9681
|
}
|
|
9449
|
-
const gitignorePath =
|
|
9682
|
+
const gitignorePath = join7(ctx.repoRoot, ".gitignore");
|
|
9450
9683
|
const gitignore = safeReadText2(gitignorePath) ?? "";
|
|
9451
9684
|
const requiredBlock = `# Secrets \u2014 .env is materialized from .env.op by \`op inject\` on mise enter,
|
|
9452
9685
|
# staged through a mktemp file and moved into place only on success.
|
|
@@ -9459,7 +9692,7 @@ function createMiseOpInjectChecks() {
|
|
|
9459
9692
|
changedFiles.push(gitignorePath);
|
|
9460
9693
|
if (!ctx.dryRun) writeText(gitignorePath, `${gitignore.replace(/\s*$/, "")}${gitignore.trim() ? "\n\n" : ""}${requiredBlock}`);
|
|
9461
9694
|
}
|
|
9462
|
-
const misePath =
|
|
9695
|
+
const misePath = join7(ctx.repoRoot, "mise.toml");
|
|
9463
9696
|
let currentMise = safeReadText2(misePath);
|
|
9464
9697
|
if (currentMise === null) {
|
|
9465
9698
|
const initialized = ensureMiseTomlFromTemplate(ctx, changedFiles);
|
|
@@ -9476,16 +9709,16 @@ function createMiseOpInjectChecks() {
|
|
|
9476
9709
|
if (!changedFiles.includes(misePath)) changedFiles.push(misePath);
|
|
9477
9710
|
if (!ctx.dryRun) writeText(misePath, opPlan.text);
|
|
9478
9711
|
}
|
|
9479
|
-
const materializePath =
|
|
9712
|
+
const materializePath = join7(ctx.repoRoot, MATERIALIZE_ENV_SCRIPT_REL);
|
|
9480
9713
|
const expectedMaterializer = templateMaterializeEnvScript(ctx);
|
|
9481
9714
|
if (optedOut) {
|
|
9482
|
-
if (
|
|
9715
|
+
if (existsSync4(materializePath)) {
|
|
9483
9716
|
changedFiles.push(materializePath);
|
|
9484
9717
|
if (!ctx.dryRun) rmSync2(materializePath);
|
|
9485
9718
|
}
|
|
9486
9719
|
} else if (!expectedMaterializer) {
|
|
9487
9720
|
return { id: finding2.id, title: finding2.title, status: "blocked", summary: "pjangler package is missing materialize-env.sh", changedFiles: [], details: [] };
|
|
9488
|
-
} else if (safeReadText2(materializePath) !== expectedMaterializer ||
|
|
9721
|
+
} else if (safeReadText2(materializePath) !== expectedMaterializer || existsSync4(materializePath) && (lstatSync6(materializePath).mode & 73) === 0) {
|
|
9489
9722
|
changedFiles.push(materializePath);
|
|
9490
9723
|
if (!ctx.dryRun) {
|
|
9491
9724
|
writeText(materializePath, expectedMaterializer);
|
|
@@ -9547,7 +9780,7 @@ function createProjectProvenanceChecks() {
|
|
|
9547
9780
|
title: ".copier-answers.yml provenance + drift report",
|
|
9548
9781
|
audit: (ctx) => {
|
|
9549
9782
|
const details = [];
|
|
9550
|
-
const path =
|
|
9783
|
+
const path = join7(ctx.repoRoot, ".copier-answers.yml");
|
|
9551
9784
|
const text = safeReadText2(path);
|
|
9552
9785
|
const project = readProjectJson(ctx);
|
|
9553
9786
|
if (!text) {
|
|
@@ -9575,11 +9808,11 @@ function createProjectProvenanceChecks() {
|
|
|
9575
9808
|
migrate: (ctx, finding2) => {
|
|
9576
9809
|
const changedFiles = [];
|
|
9577
9810
|
const project = canonicalProjectJson(ctx);
|
|
9578
|
-
const path =
|
|
9811
|
+
const path = join7(ctx.repoRoot, ".copier-answers.yml");
|
|
9579
9812
|
const current = safeReadText2(path);
|
|
9580
9813
|
const existing = current ? YAML2.parseDocument(current) : void 0;
|
|
9581
9814
|
const document = existing && !existing.errors.length && YAML2.isMap(existing.contents) ? existing : new YAML2.Document({});
|
|
9582
|
-
document.set("_src_path",
|
|
9815
|
+
document.set("_src_path", join7(ctx.pjanglerRoot, "templates", "commonproject"));
|
|
9583
9816
|
document.set("project_description", String(project.project_description));
|
|
9584
9817
|
document.set("project_name", String(project.project_name));
|
|
9585
9818
|
document.set("ticket_provider", String(project.ticket_provider?.type ?? "plane"));
|
|
@@ -9604,9 +9837,9 @@ ${body}`;
|
|
|
9604
9837
|
}
|
|
9605
9838
|
function supportedCliProjectionIssues(repoRoot, plan) {
|
|
9606
9839
|
const issues = [];
|
|
9607
|
-
const managedSkills =
|
|
9840
|
+
const managedSkills = join7(repoRoot, ".agents", "skills");
|
|
9608
9841
|
for (const rootName of SUPPORTED_CLI_ROOTS) {
|
|
9609
|
-
const root =
|
|
9842
|
+
const root = join7(repoRoot, rootName);
|
|
9610
9843
|
const rootStat = lstatIfPresent(root);
|
|
9611
9844
|
if (!rootStat) {
|
|
9612
9845
|
issues.push(`${rootName} missing`);
|
|
@@ -9616,7 +9849,7 @@ function supportedCliProjectionIssues(repoRoot, plan) {
|
|
|
9616
9849
|
issues.push(`${rootName} must be a real configuration directory`);
|
|
9617
9850
|
continue;
|
|
9618
9851
|
}
|
|
9619
|
-
const skills =
|
|
9852
|
+
const skills = join7(root, "skills");
|
|
9620
9853
|
const skillsStat = lstatIfPresent(skills);
|
|
9621
9854
|
if (!skillsStat) {
|
|
9622
9855
|
issues.push(`${rootName}/skills missing`);
|
|
@@ -9625,7 +9858,7 @@ function supportedCliProjectionIssues(repoRoot, plan) {
|
|
|
9625
9858
|
let projectedSkills = skills;
|
|
9626
9859
|
if (skillsStat.isSymbolicLink()) {
|
|
9627
9860
|
try {
|
|
9628
|
-
|
|
9861
|
+
readlinkSync4(skills);
|
|
9629
9862
|
} catch {
|
|
9630
9863
|
issues.push(`${rootName}/skills is an unreadable symlink`);
|
|
9631
9864
|
continue;
|
|
@@ -9653,9 +9886,9 @@ function supportedCliProjectionIssues(repoRoot, plan) {
|
|
|
9653
9886
|
issues.push(`${rootName}/skills is not a directory`);
|
|
9654
9887
|
continue;
|
|
9655
9888
|
}
|
|
9656
|
-
const hasGeneratedSkill =
|
|
9657
|
-
const skillFile =
|
|
9658
|
-
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();
|
|
9659
9892
|
});
|
|
9660
9893
|
if (!hasGeneratedSkill) issues.push(`${rootName}/skills contains no BMAD skill configuration`);
|
|
9661
9894
|
}
|
|
@@ -9681,7 +9914,7 @@ var LEGACY_CLI_GITIGNORE_LINES = /* @__PURE__ */ new Set([
|
|
|
9681
9914
|
"# some CLIs get a real directory here and some get a symlink."
|
|
9682
9915
|
]);
|
|
9683
9916
|
function supportedCliGitignoreIssues(repoRoot) {
|
|
9684
|
-
const lines = (safeReadText2(
|
|
9917
|
+
const lines = (safeReadText2(join7(repoRoot, ".gitignore")) ?? "").split(/\r?\n/);
|
|
9685
9918
|
const legacy = lines.filter((line) => LEGACY_CLI_GITIGNORE_LINES.has(line.trim()));
|
|
9686
9919
|
return [
|
|
9687
9920
|
...legacy.length ? [`.gitignore contains ${legacy.length} legacy client-root override line(s); only .agents is canonical`] : [],
|
|
@@ -9689,7 +9922,7 @@ function supportedCliGitignoreIssues(repoRoot) {
|
|
|
9689
9922
|
];
|
|
9690
9923
|
}
|
|
9691
9924
|
function ensureSupportedCliGitignore(ctx) {
|
|
9692
|
-
const path =
|
|
9925
|
+
const path = join7(ctx.repoRoot, ".gitignore");
|
|
9693
9926
|
const current = safeReadText2(path) ?? "";
|
|
9694
9927
|
const kept = current.split(/\r?\n/).filter((line) => {
|
|
9695
9928
|
const trimmed2 = line.trim();
|
|
@@ -9727,10 +9960,10 @@ function createBmadChecks() {
|
|
|
9727
9960
|
fixable: false
|
|
9728
9961
|
};
|
|
9729
9962
|
}
|
|
9730
|
-
const targetRoot =
|
|
9731
|
-
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];
|
|
9732
9965
|
const sentinels = requiredBmadSentinels(ctx.repoRoot, selectedModules);
|
|
9733
|
-
const missing = sentinels.filter((file) => !
|
|
9966
|
+
const missing = sentinels.filter((file) => !existsSync4(join7(targetRoot, file)));
|
|
9734
9967
|
const projectNameIssues = bmadProjectNameIssues(ctx.repoRoot);
|
|
9735
9968
|
const details = [
|
|
9736
9969
|
...missing.map((file) => `_bmad/${file}`),
|
|
@@ -9758,10 +9991,21 @@ function createBmadChecks() {
|
|
|
9758
9991
|
details: [manifestSelection.error]
|
|
9759
9992
|
};
|
|
9760
9993
|
}
|
|
9761
|
-
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
|
+
};
|
|
9762
10005
|
if (ctx.dryRun) {
|
|
9763
10006
|
const sentinels = requiredBmadSentinels(ctx.repoRoot, selectedModules);
|
|
9764
|
-
|
|
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)));
|
|
9765
10009
|
changedFiles.push(...bmadProjectNameIssues(ctx.repoRoot).paths);
|
|
9766
10010
|
return {
|
|
9767
10011
|
id: finding2.id,
|
|
@@ -9770,16 +10014,18 @@ function createBmadChecks() {
|
|
|
9770
10014
|
summary: changedFiles.length ? "Would run non-interactive bmad-method install" : "No changes required",
|
|
9771
10015
|
changedFiles,
|
|
9772
10016
|
details: [
|
|
9773
|
-
`Would run: ${bmadInstallDisplay(ctx.repoRoot, selectedModules)}
|
|
9774
|
-
|
|
10017
|
+
`Would run: ${bmadInstallDisplay(ctx.repoRoot, selectedModules)}`,
|
|
10018
|
+
...companionDetails(companion)
|
|
10019
|
+
],
|
|
10020
|
+
bmadCompanion: companion
|
|
9775
10021
|
};
|
|
9776
10022
|
}
|
|
9777
10023
|
const expectedChangedPaths = [
|
|
9778
|
-
...requiredBmadSentinels(ctx.repoRoot, selectedModules).map((file) =>
|
|
10024
|
+
...requiredBmadSentinels(ctx.repoRoot, selectedModules).map((file) => join7(ctx.repoRoot, "_bmad", file)).filter((path) => !existsSync4(path)),
|
|
9779
10025
|
...bmadProjectNameIssues(ctx.repoRoot).paths
|
|
9780
10026
|
];
|
|
9781
10027
|
const evicted = evictLegacyBmadPackState(ctx, changedFiles, await skillActivations(ctx));
|
|
9782
|
-
const install = runBmadInstall(ctx.repoRoot, selectedModules);
|
|
10028
|
+
const install = await runBmadInstall(ctx.repoRoot, selectedModules);
|
|
9783
10029
|
if (!install.ok) {
|
|
9784
10030
|
return {
|
|
9785
10031
|
id: finding2.id,
|
|
@@ -9790,14 +10036,15 @@ function createBmadChecks() {
|
|
|
9790
10036
|
details: [...evicted, install.error ?? "Unknown error"]
|
|
9791
10037
|
};
|
|
9792
10038
|
}
|
|
9793
|
-
changedFiles.push(...expectedChangedPaths.filter(
|
|
10039
|
+
changedFiles.push(...expectedChangedPaths.filter(existsSync4), ...install.companion?.changedFiles ?? []);
|
|
9794
10040
|
return {
|
|
9795
10041
|
id: finding2.id,
|
|
9796
10042
|
title: finding2.title,
|
|
9797
10043
|
status: changedFiles.length ? "applied" : "noop",
|
|
9798
10044
|
summary: changedFiles.length ? "Installed BMAD scaffold via bmad-method" : "No changes required",
|
|
9799
10045
|
changedFiles,
|
|
9800
|
-
details: evicted
|
|
10046
|
+
details: [...evicted, ...install.companion ? companionDetails(install.companion) : []],
|
|
10047
|
+
bmadCompanion: install.companion
|
|
9801
10048
|
};
|
|
9802
10049
|
}
|
|
9803
10050
|
},
|
|
@@ -9811,7 +10058,7 @@ function createBmadChecks() {
|
|
|
9811
10058
|
id: "bmad.version",
|
|
9812
10059
|
title: "BMAD version currency",
|
|
9813
10060
|
status: "skip",
|
|
9814
|
-
summary:
|
|
10061
|
+
summary: existsSync4(join7(ctx.repoRoot, "_bmad")) ? "BMAD installed but version manifest unreadable" : "No BMAD install present",
|
|
9815
10062
|
details: [],
|
|
9816
10063
|
fixable: false
|
|
9817
10064
|
};
|
|
@@ -9899,7 +10146,7 @@ function createBmadChecks() {
|
|
|
9899
10146
|
}
|
|
9900
10147
|
const installed = readInstalledBmadVersion(ctx.repoRoot);
|
|
9901
10148
|
const available = ctx.bmadVersionPin?.trim() ?? bmadChannelTarget(resolveBmadDistTags(ctx.homeDir)?.distTags)?.version;
|
|
9902
|
-
const manifestPath =
|
|
10149
|
+
const manifestPath = join7(ctx.repoRoot, "_bmad", "_config", "manifest.yaml");
|
|
9903
10150
|
const manifestSelection = manifestBmadModules(ctx.repoRoot);
|
|
9904
10151
|
if (manifestSelection.status === "invalid") {
|
|
9905
10152
|
return {
|
|
@@ -9911,8 +10158,19 @@ function createBmadChecks() {
|
|
|
9911
10158
|
details: [manifestSelection.error]
|
|
9912
10159
|
};
|
|
9913
10160
|
}
|
|
9914
|
-
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
|
+
};
|
|
9915
10172
|
if (ctx.dryRun) {
|
|
10173
|
+
const companion = await runG33Companion(ctx.repoRoot, true, "bmad-install");
|
|
9916
10174
|
return {
|
|
9917
10175
|
id: finding2.id,
|
|
9918
10176
|
title: finding2.title,
|
|
@@ -9920,11 +10178,13 @@ function createBmadChecks() {
|
|
|
9920
10178
|
summary: `Would upgrade BMAD ${installed ?? "?"} -> ${available ?? BMAD_TARGET_CHANNEL}`,
|
|
9921
10179
|
changedFiles: [manifestPath],
|
|
9922
10180
|
details: [
|
|
9923
|
-
`Would run: ${bmadInstallDisplay(ctx.repoRoot, selectedModules, available ?? BMAD_TARGET_CHANNEL)}
|
|
9924
|
-
|
|
10181
|
+
`Would run: ${bmadInstallDisplay(ctx.repoRoot, selectedModules, available ?? BMAD_TARGET_CHANNEL)}`,
|
|
10182
|
+
...companionDetails(companion)
|
|
10183
|
+
],
|
|
10184
|
+
bmadCompanion: companion
|
|
9925
10185
|
};
|
|
9926
10186
|
}
|
|
9927
|
-
const install = runBmadInstall(ctx.repoRoot, selectedModules, available ?? BMAD_TARGET_CHANNEL);
|
|
10187
|
+
const install = await runBmadInstall(ctx.repoRoot, selectedModules, available ?? BMAD_TARGET_CHANNEL);
|
|
9928
10188
|
if (!install.ok) {
|
|
9929
10189
|
return {
|
|
9930
10190
|
id: finding2.id,
|
|
@@ -9941,7 +10201,8 @@ function createBmadChecks() {
|
|
|
9941
10201
|
const upgraded = Boolean(nowInstalled && installed && compareBmadVersions(nowInstalled, installed) > 0);
|
|
9942
10202
|
const changed = Array.from(/* @__PURE__ */ new Set([
|
|
9943
10203
|
...upgraded ? [manifestPath] : [],
|
|
9944
|
-
...evictedChanges
|
|
10204
|
+
...evictedChanges,
|
|
10205
|
+
...install.companion?.changedFiles ?? []
|
|
9945
10206
|
]));
|
|
9946
10207
|
return {
|
|
9947
10208
|
id: finding2.id,
|
|
@@ -9949,7 +10210,8 @@ function createBmadChecks() {
|
|
|
9949
10210
|
status: changed.length ? "applied" : "noop",
|
|
9950
10211
|
summary: upgraded ? `Upgraded BMAD ${installed} -> ${nowInstalled}` : `BMAD reinstalled (${nowInstalled ?? "?"})`,
|
|
9951
10212
|
changedFiles: changed,
|
|
9952
|
-
details: evicted
|
|
10213
|
+
details: [...evicted, ...install.companion ? companionDetails(install.companion) : []],
|
|
10214
|
+
bmadCompanion: install.companion
|
|
9953
10215
|
};
|
|
9954
10216
|
}
|
|
9955
10217
|
},
|
|
@@ -9958,7 +10220,7 @@ function createBmadChecks() {
|
|
|
9958
10220
|
title: "Supported BMAD CLI projection roots",
|
|
9959
10221
|
audit: (ctx) => {
|
|
9960
10222
|
const unsupportedNames = Object.keys(UNSUPPORTED_BMAD_ROOTS);
|
|
9961
|
-
const present = unsupportedNames.filter((name) =>
|
|
10223
|
+
const present = unsupportedNames.filter((name) => existsSync4(join7(ctx.repoRoot, name)));
|
|
9962
10224
|
const attestations = present.map((name) => ({ name, ...unsupportedRootAttestation(ctx.repoRoot, name) }));
|
|
9963
10225
|
const plan = planSkillRoots(ctx.repoRoot);
|
|
9964
10226
|
const supportedIssues = supportedCliProjectionIssues(ctx.repoRoot, plan);
|
|
@@ -9980,7 +10242,7 @@ function createBmadChecks() {
|
|
|
9980
10242
|
},
|
|
9981
10243
|
migrate: (ctx, finding2) => {
|
|
9982
10244
|
const unsupportedNames = Object.keys(UNSUPPORTED_BMAD_ROOTS);
|
|
9983
|
-
const present = unsupportedNames.filter((name) =>
|
|
10245
|
+
const present = unsupportedNames.filter((name) => existsSync4(join7(ctx.repoRoot, name)));
|
|
9984
10246
|
const attestations = present.map((name) => ({ name, ...unsupportedRootAttestation(ctx.repoRoot, name) }));
|
|
9985
10247
|
const blocked = attestations.filter((entry) => !entry.safe);
|
|
9986
10248
|
if (blocked.length) {
|
|
@@ -10005,7 +10267,7 @@ function createBmadChecks() {
|
|
|
10005
10267
|
};
|
|
10006
10268
|
}
|
|
10007
10269
|
const gitignoreChanges = ensureSupportedCliGitignore(ctx);
|
|
10008
|
-
const removedRoots = attestations.map((entry) =>
|
|
10270
|
+
const removedRoots = attestations.map((entry) => join7(ctx.repoRoot, entry.name));
|
|
10009
10271
|
if (!ctx.dryRun) for (const path of removedRoots) rmSync2(path, { recursive: true, force: true });
|
|
10010
10272
|
const changedFiles = [.../* @__PURE__ */ new Set([...projectionResult.changedFiles, ...gitignoreChanges, ...removedRoots])].sort();
|
|
10011
10273
|
return {
|
|
@@ -10064,11 +10326,11 @@ function linkedPlaneBoard(ctx) {
|
|
|
10064
10326
|
return { boardId, workspace };
|
|
10065
10327
|
}
|
|
10066
10328
|
function runPx(args, ctx) {
|
|
10067
|
-
const probe =
|
|
10329
|
+
const probe = spawnSync4("which", ["px"], { encoding: "utf8" });
|
|
10068
10330
|
if (probe.status !== 0) {
|
|
10069
10331
|
return { error: "px not found on PATH \u2014 install Pilot to manage board schemas" };
|
|
10070
10332
|
}
|
|
10071
|
-
const result2 =
|
|
10333
|
+
const result2 = spawnSync4("px", args, {
|
|
10072
10334
|
cwd: ctx.repoRoot,
|
|
10073
10335
|
encoding: "utf8",
|
|
10074
10336
|
shell: false,
|
|
@@ -10258,11 +10520,12 @@ var AgentHooksRecipe = class extends Recipe {
|
|
|
10258
10520
|
|
|
10259
10521
|
// src/recipes/BmadRecipe.ts
|
|
10260
10522
|
var BmadRecipe = class extends Recipe {
|
|
10261
|
-
|
|
10523
|
+
requiredChecks = createBmadChecks();
|
|
10524
|
+
checks = [...this.requiredChecks, createG33CompanionCheck()];
|
|
10262
10525
|
metadata = {
|
|
10263
10526
|
id: "bmad",
|
|
10264
10527
|
name: "bmad",
|
|
10265
|
-
description: "BMAD methodology and the
|
|
10528
|
+
description: "BMAD methodology, supported CLI projections, and the optional owner-provided g33 companion",
|
|
10266
10529
|
dependencies: ["agent-hooks"],
|
|
10267
10530
|
commands: [],
|
|
10268
10531
|
publicRuleIds: this.checks.map((check) => check.id)
|
|
@@ -10270,8 +10533,31 @@ var BmadRecipe = class extends Recipe {
|
|
|
10270
10533
|
constructor(context) {
|
|
10271
10534
|
super(context);
|
|
10272
10535
|
}
|
|
10273
|
-
init(ctx, _input) {
|
|
10274
|
-
|
|
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
|
+
};
|
|
10275
10561
|
}
|
|
10276
10562
|
printNextSteps() {
|
|
10277
10563
|
console.log(`BMAD lifecycle initialized for the ${SUPPORTED_CLIS.length} supported CLIs.`);
|
|
@@ -10595,24 +10881,24 @@ var NodeRecipe = class extends Recipe {
|
|
|
10595
10881
|
|
|
10596
10882
|
// src/recipes/ProjectRecipe.ts
|
|
10597
10883
|
init_project();
|
|
10598
|
-
import { spawnSync as
|
|
10599
|
-
import { existsSync as existsSync14, lstatSync as
|
|
10600
|
-
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";
|
|
10601
10887
|
init_tree_diff();
|
|
10602
10888
|
|
|
10603
10889
|
// src/recipes/NotebookRecipe.ts
|
|
10604
10890
|
init_project();
|
|
10605
|
-
import { existsSync as existsSync13, readFileSync as
|
|
10606
|
-
import { join as
|
|
10891
|
+
import { existsSync as existsSync13, readFileSync as readFileSync14 } from "node:fs";
|
|
10892
|
+
import { join as join18 } from "node:path";
|
|
10607
10893
|
|
|
10608
10894
|
// src/notebook/checks.ts
|
|
10609
10895
|
init_config();
|
|
10610
10896
|
init_notes();
|
|
10611
10897
|
init_output();
|
|
10612
10898
|
init_state();
|
|
10613
|
-
import { spawnSync as
|
|
10614
|
-
import { existsSync as existsSync10, readFileSync as
|
|
10615
|
-
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";
|
|
10616
10902
|
var NOTEBOOK_RULE_IDS = [
|
|
10617
10903
|
"notebook.configuration",
|
|
10618
10904
|
"notebook.binding",
|
|
@@ -10629,10 +10915,10 @@ function result(check, status, summary, changedFiles = [], details = []) {
|
|
|
10629
10915
|
return { id: check.id, title: check.title, status, summary, changedFiles, details };
|
|
10630
10916
|
}
|
|
10631
10917
|
function manifestNotebook(repo) {
|
|
10632
|
-
const path =
|
|
10918
|
+
const path = join14(repo, ".project.json");
|
|
10633
10919
|
if (!existsSync10(path)) return null;
|
|
10634
10920
|
try {
|
|
10635
|
-
const parsed = JSON.parse(
|
|
10921
|
+
const parsed = JSON.parse(readFileSync12(path, "utf8"));
|
|
10636
10922
|
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return null;
|
|
10637
10923
|
const notebook = parsed.notebook;
|
|
10638
10924
|
return notebook && typeof notebook === "object" && !Array.isArray(notebook) ? notebook : null;
|
|
@@ -10861,10 +11147,10 @@ function captureAudit(ctx) {
|
|
|
10861
11147
|
function overviewMigrate(ctx, current) {
|
|
10862
11148
|
if (ctx.dryRun) return result(current, "applied", "Would reconcile drifted Project Overview note");
|
|
10863
11149
|
if (!ctx.live) return result(current, "blocked", "Reconciling the remote Overview note requires --live");
|
|
10864
|
-
const pjanglerEntry = ctx.pjanglerRoot ?
|
|
11150
|
+
const pjanglerEntry = ctx.pjanglerRoot ? join14(ctx.pjanglerRoot, "dist", "index.js") : null;
|
|
10865
11151
|
const entry = pjanglerEntry && existsSync10(pjanglerEntry) ? pjanglerEntry : "pj";
|
|
10866
11152
|
const cmd = entry === "pj" ? ["pj"] : [process.execPath, entry];
|
|
10867
|
-
const spawned =
|
|
11153
|
+
const spawned = spawnSync6(cmd[0], [...cmd.slice(1), "notebook", "overview", ctx.repoRoot], {
|
|
10868
11154
|
encoding: "utf8",
|
|
10869
11155
|
timeout: 1e4,
|
|
10870
11156
|
env: { ...process.env, ...ctx.registryPath ? { PJ_PROJECT_REGISTRY: ctx.registryPath } : {} }
|
|
@@ -10909,7 +11195,7 @@ init_project();
|
|
|
10909
11195
|
init_config();
|
|
10910
11196
|
init_open_notebook_client();
|
|
10911
11197
|
init_notes();
|
|
10912
|
-
import { resolve as
|
|
11198
|
+
import { resolve as resolve17 } from "node:path";
|
|
10913
11199
|
|
|
10914
11200
|
// src/notebook/journal-key.ts
|
|
10915
11201
|
init_types();
|
|
@@ -10943,18 +11229,18 @@ function journalNoteOperationId(period, key) {
|
|
|
10943
11229
|
init_notes();
|
|
10944
11230
|
init_git_evidence();
|
|
10945
11231
|
init_types();
|
|
10946
|
-
import { spawnSync as
|
|
11232
|
+
import { spawnSync as spawnSync8 } from "node:child_process";
|
|
10947
11233
|
import { realpathSync as realpathSync8 } from "node:fs";
|
|
10948
|
-
import { relative as relative10, resolve as
|
|
11234
|
+
import { relative as relative10, resolve as resolve14, sep as sep5 } from "node:path";
|
|
10949
11235
|
var DEFAULT_OVERVIEW_REFERENCES = [".project.json", "README.md", "AGENTS.md", "CLAUDE.md", "docs/architecture.md"];
|
|
10950
11236
|
function git2(repo, args, timeout) {
|
|
10951
|
-
const result2 =
|
|
11237
|
+
const result2 = spawnSync8("git", args, { cwd: repo, encoding: "utf8", maxBuffer: 1024 * 1024, timeout, shell: false });
|
|
10952
11238
|
return { ok: result2.status === 0, stdout: result2.stdout?.trim() ?? "" };
|
|
10953
11239
|
}
|
|
10954
11240
|
function normalizedReference(repo, value) {
|
|
10955
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}`);
|
|
10956
11242
|
const root = realpathSync8(repo);
|
|
10957
|
-
const candidate =
|
|
11243
|
+
const candidate = resolve14(root, value);
|
|
10958
11244
|
const rel = relative10(root, candidate).split(sep5).join("/");
|
|
10959
11245
|
if (!rel || rel === ".." || rel.startsWith("../")) throw new NotebookError("INVALID_INPUT", `Overview reference escapes the repository: ${value}`);
|
|
10960
11246
|
return rel.normalize("NFC");
|
|
@@ -11087,21 +11373,21 @@ init_git_evidence();
|
|
|
11087
11373
|
init_notes();
|
|
11088
11374
|
init_state();
|
|
11089
11375
|
init_types();
|
|
11090
|
-
import { spawn, spawnSync as
|
|
11376
|
+
import { spawn, spawnSync as spawnSync9 } from "node:child_process";
|
|
11091
11377
|
import { createHash as createHash8, randomUUID as randomUUID5 } from "node:crypto";
|
|
11092
|
-
import { chmodSync as chmodSync4, closeSync as closeSync5, constants as constants4, copyFileSync as copyFileSync3, existsSync as existsSync11, fstatSync as fstatSync3, fsyncSync as fsyncSync4, lstatSync as
|
|
11093
|
-
import { basename as basename8, dirname as dirname13, isAbsolute as
|
|
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";
|
|
11094
11380
|
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
|
11095
11381
|
function bundledSkillCandidates() {
|
|
11096
11382
|
const candidates = [];
|
|
11097
11383
|
let cursor = dirname13(fileURLToPath3(import.meta.url));
|
|
11098
11384
|
for (let depth = 0; depth < 8; depth++) {
|
|
11099
|
-
candidates.push(
|
|
11385
|
+
candidates.push(join17(cursor, "dist", "assets", "project-notebook-skill"), join17(cursor, "assets", "project-notebook-skill"));
|
|
11100
11386
|
const parent = dirname13(cursor);
|
|
11101
11387
|
if (parent === cursor) break;
|
|
11102
11388
|
cursor = parent;
|
|
11103
11389
|
}
|
|
11104
|
-
return [...new Set(candidates.map((candidate) =>
|
|
11390
|
+
return [...new Set(candidates.map((candidate) => resolve15(candidate)))];
|
|
11105
11391
|
}
|
|
11106
11392
|
function safeSkillRelativePath(value) {
|
|
11107
11393
|
if (!value || value.startsWith("/") || value.includes("\0")) return false;
|
|
@@ -11114,14 +11400,14 @@ function assertOwnedSkillTree(source) {
|
|
|
11114
11400
|
assertNoSymlinkComponents(source);
|
|
11115
11401
|
const uid = typeof process.getuid === "function" ? process.getuid() : void 0;
|
|
11116
11402
|
const walk = (directory) => {
|
|
11117
|
-
const directoryStat =
|
|
11403
|
+
const directoryStat = lstatSync12(directory);
|
|
11118
11404
|
if (!directoryStat.isDirectory() || directoryStat.isSymbolicLink()) throw new NotebookError("CONFLICT", "Project Notebook skill source contains a non-directory component");
|
|
11119
11405
|
if (uid !== void 0 && directoryStat.uid !== uid) throw new NotebookError("CONFLICT", "Project Notebook skill source is not owned by the current user");
|
|
11120
11406
|
if (directoryStat.mode & 3586) throw new NotebookError("CONFLICT", "Project Notebook skill source has unsafe directory mode bits");
|
|
11121
11407
|
for (const entry of readdirSync7(directory, { withFileTypes: true })) {
|
|
11122
11408
|
if (entry.isDirectory() && (entry.name === "__pycache__" || entry.name === ".git" || entry.name === "node_modules")) continue;
|
|
11123
|
-
const path =
|
|
11124
|
-
const stat =
|
|
11409
|
+
const path = join17(directory, entry.name);
|
|
11410
|
+
const stat = lstatSync12(path);
|
|
11125
11411
|
if (stat.isSymbolicLink()) throw new NotebookError("CONFLICT", "Project Notebook skill source contains a symlink");
|
|
11126
11412
|
if (entry.isDirectory()) walk(path);
|
|
11127
11413
|
else if (!entry.isFile()) throw new NotebookError("CONFLICT", "Project Notebook skill source contains a non-regular entry");
|
|
@@ -11138,16 +11424,16 @@ function enumerateSkillPayload(source) {
|
|
|
11138
11424
|
const walk = (directory) => {
|
|
11139
11425
|
for (const entry of readdirSync7(directory, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name, "en"))) {
|
|
11140
11426
|
if (entry.isDirectory() && (entry.name === "__pycache__" || entry.name === ".git" || entry.name === "node_modules")) continue;
|
|
11141
|
-
const path =
|
|
11427
|
+
const path = join17(directory, entry.name);
|
|
11142
11428
|
const rel = relative11(source, path).split(sep6).join("/");
|
|
11143
11429
|
if (!rel.includes("/") && (rel === "export-manifest.json" || rel === "SHA256SUMS" || rel === ".source.yaml")) continue;
|
|
11144
11430
|
if (!safeSkillRelativePath(rel)) throw new NotebookError("CONFLICT", `Project Notebook skill export path is unsafe: ${rel}`);
|
|
11145
|
-
const stat =
|
|
11431
|
+
const stat = lstatSync12(path);
|
|
11146
11432
|
if (stat.isSymbolicLink()) throw new NotebookError("CONFLICT", "Project Notebook skill export contains a symlink");
|
|
11147
11433
|
if (entry.isDirectory()) walk(path);
|
|
11148
11434
|
else if (entry.isFile()) {
|
|
11149
11435
|
const expectedMode = rel.endsWith(".sh") || rel.startsWith("scripts/") ? "0755" : "0644";
|
|
11150
|
-
result2.push({ path: rel, sha256: createHash8("sha256").update(
|
|
11436
|
+
result2.push({ path: rel, sha256: createHash8("sha256").update(readFileSync13(path)).digest("hex"), mode: expectedMode });
|
|
11151
11437
|
} else throw new NotebookError("CONFLICT", "Project Notebook skill export contains a non-regular entry");
|
|
11152
11438
|
}
|
|
11153
11439
|
};
|
|
@@ -11155,9 +11441,9 @@ function enumerateSkillPayload(source) {
|
|
|
11155
11441
|
return result2.sort((a, b) => a.path.localeCompare(b.path, "en"));
|
|
11156
11442
|
}
|
|
11157
11443
|
function parsePackedManifest(source) {
|
|
11158
|
-
const manifestPath =
|
|
11444
|
+
const manifestPath = join17(source, "export-manifest.json");
|
|
11159
11445
|
if (!existsSync11(manifestPath)) return null;
|
|
11160
|
-
const value = JSON.parse(
|
|
11446
|
+
const value = JSON.parse(readFileSync13(manifestPath, "utf8"));
|
|
11161
11447
|
if (!value || typeof value !== "object" || Array.isArray(value)) throw new NotebookError("CONFLICT", "Project Notebook skill export manifest is invalid");
|
|
11162
11448
|
const manifest = value;
|
|
11163
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");
|
|
@@ -11166,10 +11452,10 @@ function parsePackedManifest(source) {
|
|
|
11166
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");
|
|
11167
11453
|
if (!safeSkillRelativePath(entry.path) || paths.has(entry.path)) throw new NotebookError("CONFLICT", "Project Notebook skill export path is unsafe or duplicated");
|
|
11168
11454
|
paths.add(entry.path);
|
|
11169
|
-
const path =
|
|
11170
|
-
const stat =
|
|
11455
|
+
const path = join17(source, ...entry.path.split("/"));
|
|
11456
|
+
const stat = lstatSync12(path);
|
|
11171
11457
|
if (!stat.isFile() || stat.isSymbolicLink()) throw new NotebookError("CONFLICT", "Project Notebook skill export contains a non-regular entry");
|
|
11172
|
-
const actual = createHash8("sha256").update(
|
|
11458
|
+
const actual = createHash8("sha256").update(readFileSync13(path)).digest("hex");
|
|
11173
11459
|
if (actual !== entry.sha256) throw new NotebookError("CONFLICT", `Project Notebook skill digest mismatch: ${entry.path}`);
|
|
11174
11460
|
const actualMode = stat.mode & 511;
|
|
11175
11461
|
const executable = entry.mode === "0755";
|
|
@@ -11181,12 +11467,12 @@ function parsePackedManifest(source) {
|
|
|
11181
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");
|
|
11182
11468
|
const sums = `${manifest.files.map((entry) => `${entry.sha256} ${entry.path}`).join("\n")}
|
|
11183
11469
|
`;
|
|
11184
|
-
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");
|
|
11185
11471
|
return manifest;
|
|
11186
11472
|
}
|
|
11187
11473
|
function expectedPackedSkill() {
|
|
11188
11474
|
for (const candidate of bundledSkillCandidates()) {
|
|
11189
|
-
if (!existsSync11(
|
|
11475
|
+
if (!existsSync11(join17(candidate, "export-manifest.json"))) continue;
|
|
11190
11476
|
assertOwnedSkillTree(candidate);
|
|
11191
11477
|
const manifest = parsePackedManifest(candidate);
|
|
11192
11478
|
if (!manifest) continue;
|
|
@@ -11196,7 +11482,7 @@ function expectedPackedSkill() {
|
|
|
11196
11482
|
throw new NotebookError("NOT_CONFIGURED", "PJ\xE1ngler package has no verified Project Notebook skill export");
|
|
11197
11483
|
}
|
|
11198
11484
|
function isCanonicalSkillexProjectionPath(path) {
|
|
11199
|
-
return
|
|
11485
|
+
return resolve15(path).split(sep6).join("/").endsWith("/all-skills/project-notebook");
|
|
11200
11486
|
}
|
|
11201
11487
|
function isVerifiedCanonicalSkillexProjection(path) {
|
|
11202
11488
|
if (!isCanonicalSkillexProjectionPath(path)) return false;
|
|
@@ -11216,7 +11502,7 @@ function describeProjectNotebookSkillDrift(source) {
|
|
|
11216
11502
|
}
|
|
11217
11503
|
let actual;
|
|
11218
11504
|
try {
|
|
11219
|
-
const absolute =
|
|
11505
|
+
const absolute = resolve15(source);
|
|
11220
11506
|
const packed = parsePackedManifest(absolute);
|
|
11221
11507
|
actual = packed?.files ?? enumerateSkillPayload(absolute);
|
|
11222
11508
|
} catch (error) {
|
|
@@ -11240,7 +11526,7 @@ function describeProjectNotebookSkillDrift(source) {
|
|
|
11240
11526
|
return drift.length ? drift.slice(0, 20) : ["no per-file drift detected; the export enumeration itself differs"];
|
|
11241
11527
|
}
|
|
11242
11528
|
function verifyProjectNotebookSkillExport(source) {
|
|
11243
|
-
const absolute =
|
|
11529
|
+
const absolute = resolve15(source);
|
|
11244
11530
|
assertOwnedSkillTree(absolute);
|
|
11245
11531
|
const packed = parsePackedManifest(absolute);
|
|
11246
11532
|
const actual = packed ?? { schema_version: 1, skill: "project-notebook", files: enumerateSkillPayload(absolute) };
|
|
@@ -11252,14 +11538,14 @@ function verifyProjectNotebookSkillExport(source) {
|
|
|
11252
11538
|
}
|
|
11253
11539
|
function resolveProjectNotebookSkillSource(env2 = process.env) {
|
|
11254
11540
|
if (env2.PJ_PROJECT_NOTEBOOK_SKILL_ROOT) {
|
|
11255
|
-
const explicit =
|
|
11256
|
-
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");
|
|
11257
11543
|
verifyProjectNotebookSkillExport(explicit);
|
|
11258
11544
|
return explicit;
|
|
11259
11545
|
}
|
|
11260
11546
|
if (env2.PJ_SKILLS_REGISTRY_ROOT) {
|
|
11261
|
-
const canonical =
|
|
11262
|
-
if (existsSync11(
|
|
11547
|
+
const canonical = resolve15(env2.PJ_SKILLS_REGISTRY_ROOT, "all-skills", "project-notebook");
|
|
11548
|
+
if (existsSync11(join17(canonical, "SKILL.md"))) {
|
|
11263
11549
|
verifyProjectNotebookSkillExport(canonical);
|
|
11264
11550
|
return canonical;
|
|
11265
11551
|
}
|
|
@@ -11274,7 +11560,7 @@ var REPAIR_COMMAND = "pj notebook skill --apply";
|
|
|
11274
11560
|
function probeCanonicalSkillexRootProjection(skillsRoot, link) {
|
|
11275
11561
|
let rootLink;
|
|
11276
11562
|
try {
|
|
11277
|
-
rootLink =
|
|
11563
|
+
rootLink = lstatSync12(skillsRoot);
|
|
11278
11564
|
} catch (error) {
|
|
11279
11565
|
if (error.code === "ENOENT") return { state: "absent" };
|
|
11280
11566
|
throw error;
|
|
@@ -11287,15 +11573,15 @@ function probeCanonicalSkillexRootProjection(skillsRoot, link) {
|
|
|
11287
11573
|
if (uid !== void 0 && rootLink.uid !== uid) throw new Error("owner");
|
|
11288
11574
|
const globalRoot = realpathSync9(skillsRoot);
|
|
11289
11575
|
assertNoSymlinkComponents(globalRoot);
|
|
11290
|
-
const globalStat =
|
|
11576
|
+
const globalStat = lstatSync12(globalRoot);
|
|
11291
11577
|
if (!globalStat.isDirectory() || globalStat.isSymbolicLink()) throw new Error("root-type");
|
|
11292
11578
|
if (uid !== void 0 && globalStat.uid !== uid) throw new Error("root-owner");
|
|
11293
11579
|
if (globalStat.mode & 3586) throw new Error("root-mode");
|
|
11294
11580
|
const skillSetsRoot = dirname13(globalRoot);
|
|
11295
11581
|
if (basename8(globalRoot) !== "global" || basename8(skillSetsRoot) !== "skill-sets") throw new Error("layout");
|
|
11296
11582
|
const checkoutRoot = dirname13(skillSetsRoot);
|
|
11297
|
-
const expectedSource =
|
|
11298
|
-
const linkStat =
|
|
11583
|
+
const expectedSource = join17(checkoutRoot, "all-skills", "project-notebook");
|
|
11584
|
+
const linkStat = lstatSync12(link);
|
|
11299
11585
|
if (!linkStat.isSymbolicLink()) throw new Error("projection-type");
|
|
11300
11586
|
if (uid !== void 0 && linkStat.uid !== uid) throw new Error("projection-owner");
|
|
11301
11587
|
const projectedSource = realpathSync9(link);
|
|
@@ -11331,28 +11617,28 @@ function installPackagedProjectNotebookSkill(input = {}) {
|
|
|
11331
11617
|
const manifest = verifyProjectNotebookSkillExport(source);
|
|
11332
11618
|
const digest = createHash8("sha256").update(JSON.stringify(manifest)).digest("hex");
|
|
11333
11619
|
const home = env2.HOME;
|
|
11334
|
-
if (!home || !
|
|
11335
|
-
const skillsRoot =
|
|
11336
|
-
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");
|
|
11337
11623
|
const probe = probeCanonicalSkillexRootProjection(skillsRoot, link);
|
|
11338
11624
|
if (probe.state === "adopted") return { installed: false, path: link, digest };
|
|
11339
11625
|
if (probe.state === "declined") return { installed: false, path: link, digest, blocked: probe.block };
|
|
11340
|
-
const dataRoot =
|
|
11341
|
-
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}`);
|
|
11342
11628
|
assertNoSymlinkComponents(dirname13(dataRoot), true);
|
|
11343
11629
|
mkdirSync6(dataRoot, { recursive: true, mode: 448 });
|
|
11344
11630
|
assertNoSymlinkComponents(dataRoot);
|
|
11345
|
-
const dataStat =
|
|
11631
|
+
const dataStat = lstatSync12(dataRoot);
|
|
11346
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");
|
|
11347
11633
|
chmodSync4(dataRoot, 448);
|
|
11348
11634
|
if (!existsSync11(payload)) {
|
|
11349
|
-
const staging =
|
|
11635
|
+
const staging = join17(dataRoot, `.staging-${randomUUID5()}`);
|
|
11350
11636
|
mkdirSync6(staging, { recursive: false, mode: 448 });
|
|
11351
11637
|
try {
|
|
11352
11638
|
for (const entry of manifest.files) {
|
|
11353
|
-
const destination =
|
|
11639
|
+
const destination = join17(staging, ...entry.path.split("/"));
|
|
11354
11640
|
mkdirSync6(dirname13(destination), { recursive: true, mode: 493 });
|
|
11355
|
-
copyFileSync3(
|
|
11641
|
+
copyFileSync3(join17(source, ...entry.path.split("/")), destination);
|
|
11356
11642
|
const mode = entry.mode === "0755" ? 493 : 420;
|
|
11357
11643
|
chmodSync4(destination, mode);
|
|
11358
11644
|
const fd = openSync5(destination, constants4.O_RDONLY | (constants4.O_NOFOLLOW ?? 0));
|
|
@@ -11364,9 +11650,9 @@ function installPackagedProjectNotebookSkill(input = {}) {
|
|
|
11364
11650
|
closeSync5(fd);
|
|
11365
11651
|
}
|
|
11366
11652
|
}
|
|
11367
|
-
writeFileSync7(
|
|
11653
|
+
writeFileSync7(join17(staging, "export-manifest.json"), `${JSON.stringify(manifest, null, 2)}
|
|
11368
11654
|
`, { mode: 420, flag: "wx" });
|
|
11369
|
-
writeFileSync7(
|
|
11655
|
+
writeFileSync7(join17(staging, "SHA256SUMS"), `${manifest.files.map((entry) => `${entry.sha256} ${entry.path}`).join("\n")}
|
|
11370
11656
|
`, { mode: 420, flag: "wx" });
|
|
11371
11657
|
verifyProjectNotebookSkillExport(staging);
|
|
11372
11658
|
renameSync6(staging, payload);
|
|
@@ -11374,7 +11660,7 @@ function installPackagedProjectNotebookSkill(input = {}) {
|
|
|
11374
11660
|
if (existsSync11(staging)) rmSync4(staging, { recursive: true, force: true });
|
|
11375
11661
|
}
|
|
11376
11662
|
} else {
|
|
11377
|
-
const stat =
|
|
11663
|
+
const stat = lstatSync12(payload);
|
|
11378
11664
|
if (!stat.isDirectory() || stat.isSymbolicLink()) throw new NotebookError("CONFLICT", "Installed Project Notebook payload is not a real directory");
|
|
11379
11665
|
verifyProjectNotebookSkillExport(payload);
|
|
11380
11666
|
}
|
|
@@ -11384,7 +11670,7 @@ function installPackagedProjectNotebookSkill(input = {}) {
|
|
|
11384
11670
|
let linkExists = existsSync11(link);
|
|
11385
11671
|
if (!linkExists) {
|
|
11386
11672
|
try {
|
|
11387
|
-
|
|
11673
|
+
lstatSync12(link);
|
|
11388
11674
|
linkExists = true;
|
|
11389
11675
|
} catch {
|
|
11390
11676
|
}
|
|
@@ -11401,7 +11687,7 @@ function installPackagedProjectNotebookSkill(input = {}) {
|
|
|
11401
11687
|
repair: REPAIR_COMMAND
|
|
11402
11688
|
}
|
|
11403
11689
|
});
|
|
11404
|
-
const stat =
|
|
11690
|
+
const stat = lstatSync12(link);
|
|
11405
11691
|
if (!stat.isSymbolicLink()) return foreign(["the path is a real file or directory, not a PJ\xE1ngler-owned link"]);
|
|
11406
11692
|
const target = realpathSync9(link);
|
|
11407
11693
|
if (target !== realpathSync9(payload) && !isVerifiedCanonicalSkillexProjection(target)) {
|
|
@@ -11435,9 +11721,9 @@ function collectSupersededPayloads(dataRoot, keep) {
|
|
|
11435
11721
|
}
|
|
11436
11722
|
for (const name of entries) {
|
|
11437
11723
|
if (!/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?-[0-9a-f]{64}$/u.test(name)) continue;
|
|
11438
|
-
const candidate =
|
|
11724
|
+
const candidate = join17(dataRoot, name);
|
|
11439
11725
|
try {
|
|
11440
|
-
const stat =
|
|
11726
|
+
const stat = lstatSync12(candidate);
|
|
11441
11727
|
if (!stat.isDirectory() || stat.isSymbolicLink()) continue;
|
|
11442
11728
|
if (kept.has(realpathSync9(candidate))) continue;
|
|
11443
11729
|
rmSync4(candidate, { recursive: true, force: true });
|
|
@@ -11450,9 +11736,9 @@ function collectSupersededPayloads(dataRoot, keep) {
|
|
|
11450
11736
|
function repairProjectNotebookSkillProjection(input = {}) {
|
|
11451
11737
|
const env2 = input.env ?? process.env;
|
|
11452
11738
|
const home = env2.HOME;
|
|
11453
|
-
if (!home || !
|
|
11454
|
-
const skillsRoot =
|
|
11455
|
-
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");
|
|
11456
11742
|
const probe = probeCanonicalSkillexRootProjection(skillsRoot, link);
|
|
11457
11743
|
if (probe.state === "adopted") return { status: "clean", summary: "Canonical Skillex projection already matches the version-pinned export", source: probe.source, drift: [], changed_files: [] };
|
|
11458
11744
|
if (probe.state !== "declined") {
|
|
@@ -11466,7 +11752,7 @@ function repairProjectNotebookSkillProjection(input = {}) {
|
|
|
11466
11752
|
const present = [];
|
|
11467
11753
|
const walk = (directory) => {
|
|
11468
11754
|
for (const entry of readdirSync7(directory, { withFileTypes: true })) {
|
|
11469
|
-
const path =
|
|
11755
|
+
const path = join17(directory, entry.name);
|
|
11470
11756
|
const rel = relative11(source, path).split(sep6).join("/");
|
|
11471
11757
|
if (entry.isDirectory()) {
|
|
11472
11758
|
walk(path);
|
|
@@ -11480,11 +11766,11 @@ function repairProjectNotebookSkillProjection(input = {}) {
|
|
|
11480
11766
|
walk(source);
|
|
11481
11767
|
const stale = present.filter((rel) => !wanted.has(rel)).sort();
|
|
11482
11768
|
const changed = expected.manifest.files.filter((entry) => {
|
|
11483
|
-
const path =
|
|
11769
|
+
const path = join17(source, ...entry.path.split("/"));
|
|
11484
11770
|
if (!existsSync11(path)) return true;
|
|
11485
|
-
const stat =
|
|
11771
|
+
const stat = lstatSync12(path);
|
|
11486
11772
|
if (!stat.isFile()) return true;
|
|
11487
|
-
if (createHash8("sha256").update(
|
|
11773
|
+
if (createHash8("sha256").update(readFileSync13(path)).digest("hex") !== entry.sha256) return true;
|
|
11488
11774
|
const mode = stat.mode & 511;
|
|
11489
11775
|
const executable = entry.mode === "0755";
|
|
11490
11776
|
return executable && (mode & 64) === 0 || !executable && (mode & 73) !== 0;
|
|
@@ -11499,15 +11785,15 @@ function repairProjectNotebookSkillProjection(input = {}) {
|
|
|
11499
11785
|
summary: `Would restore ${affected.length} file(s) in ${source} from the version-pinned export`,
|
|
11500
11786
|
source,
|
|
11501
11787
|
drift: probe.block.details,
|
|
11502
|
-
changed_files: affected.map((rel) =>
|
|
11788
|
+
changed_files: affected.map((rel) => join17(source, ...rel.split("/")))
|
|
11503
11789
|
};
|
|
11504
11790
|
}
|
|
11505
|
-
for (const rel of stale) rmSync4(
|
|
11791
|
+
for (const rel of stale) rmSync4(join17(source, ...rel.split("/")), { force: true });
|
|
11506
11792
|
for (const rel of changed) {
|
|
11507
|
-
const destination =
|
|
11793
|
+
const destination = join17(source, ...rel.split("/"));
|
|
11508
11794
|
const entry = expected.manifest.files.find((item) => item.path === rel);
|
|
11509
11795
|
mkdirSync6(dirname13(destination), { recursive: true, mode: 493 });
|
|
11510
|
-
copyFileSync3(
|
|
11796
|
+
copyFileSync3(join17(expected.source, ...rel.split("/")), destination);
|
|
11511
11797
|
chmodSync4(destination, entry.mode === "0755" ? 493 : 420);
|
|
11512
11798
|
}
|
|
11513
11799
|
verifyProjectNotebookSkillExport(source);
|
|
@@ -11516,21 +11802,21 @@ function repairProjectNotebookSkillProjection(input = {}) {
|
|
|
11516
11802
|
summary: `Restored ${affected.length} file(s) in ${source} from the version-pinned export; commit the change in the Skillex checkout`,
|
|
11517
11803
|
source,
|
|
11518
11804
|
drift: probe.block.details,
|
|
11519
|
-
changed_files: affected.map((rel) =>
|
|
11805
|
+
changed_files: affected.map((rel) => join17(source, ...rel.split("/")))
|
|
11520
11806
|
};
|
|
11521
11807
|
}
|
|
11522
11808
|
function inspectProjectNotebookIntegration(env2 = process.env) {
|
|
11523
11809
|
const home = env2.HOME;
|
|
11524
11810
|
if (!home) return { skill_installed: false, hooks_projected: false, details: ["HOME is unavailable"] };
|
|
11525
|
-
const skillsRoot =
|
|
11526
|
-
const link =
|
|
11811
|
+
const skillsRoot = join17(home, ".agents", "skills");
|
|
11812
|
+
const link = join17(skillsRoot, "project-notebook");
|
|
11527
11813
|
try {
|
|
11528
11814
|
const expected = expectedPackedSkill();
|
|
11529
|
-
const dataRoot =
|
|
11530
|
-
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}`);
|
|
11531
11817
|
const probe = probeCanonicalSkillexRootProjection(skillsRoot, link);
|
|
11532
11818
|
if (probe.state === "declined") return { skill_installed: false, hooks_projected: false, details: probe.block.details, blocked: probe.block };
|
|
11533
|
-
const stat =
|
|
11819
|
+
const stat = lstatSync12(link);
|
|
11534
11820
|
if (!stat.isSymbolicLink()) {
|
|
11535
11821
|
return {
|
|
11536
11822
|
skill_installed: false,
|
|
@@ -11567,8 +11853,8 @@ function inspectProjectNotebookIntegration(env2 = process.env) {
|
|
|
11567
11853
|
}
|
|
11568
11854
|
}
|
|
11569
11855
|
function projectorArguments(source, command, input) {
|
|
11570
|
-
const script =
|
|
11571
|
-
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];
|
|
11572
11858
|
if (command === "check") args.push("--json");
|
|
11573
11859
|
else if (input.stateHome) args.push("--state-home", input.stateHome);
|
|
11574
11860
|
return args;
|
|
@@ -11590,8 +11876,8 @@ function checkProjectNotebookHooks(input = {}) {
|
|
|
11590
11876
|
verifyProjectNotebookSkillExport(source);
|
|
11591
11877
|
const home = env2.HOME;
|
|
11592
11878
|
if (!home) throw new NotebookError("NOT_CONFIGURED", "HOME is required to check Project Notebook hooks");
|
|
11593
|
-
const target =
|
|
11594
|
-
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 });
|
|
11595
11881
|
if (result2.status !== 0 && result2.status !== 1) throw new NotebookError("CONFLICT", (result2.stderr || "Project Notebook projector check failed").trim().slice(0, 512));
|
|
11596
11882
|
try {
|
|
11597
11883
|
const parsed = JSON.parse(result2.stdout);
|
|
@@ -11608,13 +11894,13 @@ function installProjectNotebookIntegration(input = {}) {
|
|
|
11608
11894
|
const source = realpathSync9(skill.path);
|
|
11609
11895
|
const home = env2.HOME;
|
|
11610
11896
|
if (!home) throw new NotebookError("NOT_CONFIGURED", "HOME is required to install Project Notebook hooks");
|
|
11611
|
-
const target =
|
|
11612
|
-
const stateHome =
|
|
11613
|
-
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 });
|
|
11614
11900
|
if (result2.status !== 0) {
|
|
11615
11901
|
if (skill.installed) {
|
|
11616
11902
|
try {
|
|
11617
|
-
const stat =
|
|
11903
|
+
const stat = lstatSync12(skill.path);
|
|
11618
11904
|
if (stat.isSymbolicLink() && realpathSync9(skill.path) === source) unlinkSync6(skill.path);
|
|
11619
11905
|
} catch {
|
|
11620
11906
|
}
|
|
@@ -11628,14 +11914,14 @@ function boundedDiagnostic(value, max = 512) {
|
|
|
11628
11914
|
return message.replace(/[\u0000-\u001f\u007f]/gu, " ").replace(/\s+/gu, " ").slice(0, max);
|
|
11629
11915
|
}
|
|
11630
11916
|
function assertNoSymlinkComponents(path, allowMissing = false) {
|
|
11631
|
-
const absolute =
|
|
11917
|
+
const absolute = resolve15(path);
|
|
11632
11918
|
const root = parse3(absolute).root;
|
|
11633
11919
|
let cursor = root;
|
|
11634
11920
|
for (const component of absolute.slice(root.length).split(sep6).filter(Boolean)) {
|
|
11635
|
-
cursor =
|
|
11921
|
+
cursor = join17(cursor, component);
|
|
11636
11922
|
let stat;
|
|
11637
11923
|
try {
|
|
11638
|
-
stat =
|
|
11924
|
+
stat = lstatSync12(cursor);
|
|
11639
11925
|
} catch (error) {
|
|
11640
11926
|
if (allowMissing && error.code === "ENOENT") return;
|
|
11641
11927
|
throw error;
|
|
@@ -11835,7 +12121,7 @@ var NotebookModule = class {
|
|
|
11835
12121
|
return {
|
|
11836
12122
|
changedFiles: [
|
|
11837
12123
|
...installed.skill.installed ? [installed.skill.path] : [],
|
|
11838
|
-
...installed.hooksChanged ? [
|
|
12124
|
+
...installed.hooksChanged ? [resolve17(env2.PJ_PROJECT_NOTEBOOK_CLAUDE_SETTINGS ?? `${home}/.claude/settings.json`)] : []
|
|
11839
12125
|
],
|
|
11840
12126
|
...installed.blocked ? { blocked: installed.blocked } : {}
|
|
11841
12127
|
};
|
|
@@ -12241,17 +12527,17 @@ init_remote_mutation_journal();
|
|
|
12241
12527
|
function integrationEnvironment(module, ctx) {
|
|
12242
12528
|
const env2 = { ...module.environment, HOME: ctx.homeDir };
|
|
12243
12529
|
if (module.environment.HOME !== ctx.homeDir) {
|
|
12244
|
-
env2.XDG_DATA_HOME =
|
|
12245
|
-
env2.XDG_STATE_HOME =
|
|
12246
|
-
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");
|
|
12247
12533
|
}
|
|
12248
12534
|
return env2;
|
|
12249
12535
|
}
|
|
12250
12536
|
function resolvedForPlan(plan) {
|
|
12251
|
-
const manifestPath =
|
|
12537
|
+
const manifestPath = join18(plan.project.repo_path, ".project.json");
|
|
12252
12538
|
let manifest = plan.manifest;
|
|
12253
12539
|
if (existsSync13(manifestPath)) {
|
|
12254
|
-
const parsed = JSON.parse(
|
|
12540
|
+
const parsed = JSON.parse(readFileSync14(manifestPath, "utf8"));
|
|
12255
12541
|
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error(`${manifestPath} must contain a JSON object`);
|
|
12256
12542
|
manifest = parsed;
|
|
12257
12543
|
}
|
|
@@ -12392,7 +12678,7 @@ var PRODUCTION_RUNTIME = {
|
|
|
12392
12678
|
executePlan: executeProjectInitPlan,
|
|
12393
12679
|
preflightBmad: preflightBmadLifecycle,
|
|
12394
12680
|
runGit(cwd, args, options) {
|
|
12395
|
-
const result2 =
|
|
12681
|
+
const result2 = spawnSync12("git", [...args], {
|
|
12396
12682
|
cwd,
|
|
12397
12683
|
encoding: "utf8",
|
|
12398
12684
|
env: options?.env ? { ...process.env, ...options.env } : process.env
|
|
@@ -12406,13 +12692,13 @@ var PRODUCTION_RUNTIME = {
|
|
|
12406
12692
|
}
|
|
12407
12693
|
};
|
|
12408
12694
|
function unsafeToRemove(targetDir) {
|
|
12409
|
-
const absolute =
|
|
12695
|
+
const absolute = resolve18(targetDir);
|
|
12410
12696
|
let cursor = absolute;
|
|
12411
12697
|
const seen = /* @__PURE__ */ new Set();
|
|
12412
12698
|
while (!seen.has(cursor)) {
|
|
12413
12699
|
seen.add(cursor);
|
|
12414
12700
|
try {
|
|
12415
|
-
const stat =
|
|
12701
|
+
const stat = lstatSync13(cursor);
|
|
12416
12702
|
if (stat.isSymbolicLink()) {
|
|
12417
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`;
|
|
12418
12704
|
}
|
|
@@ -12440,12 +12726,12 @@ function publicMigration(report) {
|
|
|
12440
12726
|
};
|
|
12441
12727
|
}
|
|
12442
12728
|
function hasGitRepository(runtime, targetDir) {
|
|
12443
|
-
if (!existsSync14(
|
|
12729
|
+
if (!existsSync14(join19(targetDir, ".git"))) return false;
|
|
12444
12730
|
return runtime.runGit(targetDir, ["rev-parse", "--is-inside-work-tree"]).status === 0;
|
|
12445
12731
|
}
|
|
12446
12732
|
function refreshPlanFromCanonicalManifest(plan) {
|
|
12447
|
-
const manifestPath =
|
|
12448
|
-
const manifest = JSON.parse(
|
|
12733
|
+
const manifestPath = join19(plan.project.repo_path, ".project.json");
|
|
12734
|
+
const manifest = JSON.parse(readFileSync15(manifestPath, "utf8"));
|
|
12449
12735
|
if (!manifest || typeof manifest !== "object" || Array.isArray(manifest)) {
|
|
12450
12736
|
throw new Error(`${manifestPath} must contain a JSON object`);
|
|
12451
12737
|
}
|
|
@@ -12537,6 +12823,8 @@ var ProjectRecipe = class extends Recipe {
|
|
|
12537
12823
|
repoRoot: targetDir,
|
|
12538
12824
|
bmadVersionPin: mode === "create" ? BMAD_INSTALLER_VERSION : ctx.bmadVersionPin
|
|
12539
12825
|
};
|
|
12826
|
+
let bmadCompanion;
|
|
12827
|
+
let g33DeferredForEnrollment = false;
|
|
12540
12828
|
let migrationReport;
|
|
12541
12829
|
let audit;
|
|
12542
12830
|
let notebookPlan;
|
|
@@ -12577,6 +12865,10 @@ var ProjectRecipe = class extends Recipe {
|
|
|
12577
12865
|
message: planBlocked ? "Project plan skipped after failed preflight" : executed.ok ? "Project plan executed" : executed.errors.join("; ")
|
|
12578
12866
|
});
|
|
12579
12867
|
if (errors.length === 0 && executed.ok && mode === "create") {
|
|
12868
|
+
try {
|
|
12869
|
+
g33DeferredForEnrollment = !g33CompanionEnabled(targetDir);
|
|
12870
|
+
} catch {
|
|
12871
|
+
}
|
|
12580
12872
|
const dependencyResult = await this.registry.initDependencies(this.metadata.id, transactionContext, normalized, ["notebook"]);
|
|
12581
12873
|
logs.push(...dependencyResult.logs);
|
|
12582
12874
|
errors.push(...dependencyResult.errors);
|
|
@@ -12657,7 +12949,7 @@ var ProjectRecipe = class extends Recipe {
|
|
|
12657
12949
|
if (hasGitRepository(this.runtime, targetDir)) {
|
|
12658
12950
|
phases.push({ id: "project.git", status: "unchanged", changedFiles: [], message: "Git repository already initialized" });
|
|
12659
12951
|
} else {
|
|
12660
|
-
const gitPath =
|
|
12952
|
+
const gitPath = join19(targetDir, ".git");
|
|
12661
12953
|
for (const { args, label, options } of [
|
|
12662
12954
|
{ args: ["init", "--initial-branch=main"], label: "git init" },
|
|
12663
12955
|
{ args: ["add", "-A"], label: "git add" },
|
|
@@ -12757,6 +13049,17 @@ var ProjectRecipe = class extends Recipe {
|
|
|
12757
13049
|
errors.push(`project manifest refresh after external effects failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
12758
13050
|
}
|
|
12759
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
|
+
}
|
|
12760
13063
|
if (registryFinalizerEligible && registryActions.length) {
|
|
12761
13064
|
rollbackEligible = false;
|
|
12762
13065
|
const registryPlan = { ...plan, actions: registryActions };
|
|
@@ -12810,8 +13113,8 @@ var ProjectRecipe = class extends Recipe {
|
|
|
12810
13113
|
} else try {
|
|
12811
13114
|
rmSync5(targetDir, { recursive: true, force: true });
|
|
12812
13115
|
const insideTarget = (path) => {
|
|
12813
|
-
const relative12 = relativePath(
|
|
12814
|
-
return relative12 === "" || !relative12.startsWith("..") && !
|
|
13116
|
+
const relative12 = relativePath(resolve18(targetDir), resolve18(path));
|
|
13117
|
+
return relative12 === "" || !relative12.startsWith("..") && !isAbsolute9(relative12);
|
|
12815
13118
|
};
|
|
12816
13119
|
const orphaned = [...new Set(changedFiles.filter((path) => !insideTarget(path)))].sort();
|
|
12817
13120
|
changedFiles.length = 0;
|
|
@@ -12847,7 +13150,8 @@ var ProjectRecipe = class extends Recipe {
|
|
|
12847
13150
|
audit,
|
|
12848
13151
|
selectedOperations: normalized.selectedOperations ?? [],
|
|
12849
13152
|
selectedParityRules: normalized.selectedRuleIds ?? [],
|
|
12850
|
-
migrationReport
|
|
13153
|
+
migrationReport,
|
|
13154
|
+
...bmadCompanion ? { bmadCompanion } : {}
|
|
12851
13155
|
};
|
|
12852
13156
|
}
|
|
12853
13157
|
printNextSteps() {
|
|
@@ -12855,12 +13159,12 @@ var ProjectRecipe = class extends Recipe {
|
|
|
12855
13159
|
};
|
|
12856
13160
|
|
|
12857
13161
|
// src/recipes/registry.ts
|
|
12858
|
-
import { readFileSync as
|
|
12859
|
-
import { join as
|
|
13162
|
+
import { readFileSync as readFileSync16 } from "node:fs";
|
|
13163
|
+
import { join as join20 } from "node:path";
|
|
12860
13164
|
function projectWaivers(repoRoot) {
|
|
12861
13165
|
let waive;
|
|
12862
13166
|
try {
|
|
12863
|
-
waive = JSON.parse(
|
|
13167
|
+
waive = JSON.parse(readFileSync16(join20(repoRoot, ".project.json"), "utf8"))?.parity?.waive;
|
|
12864
13168
|
} catch {
|
|
12865
13169
|
return /* @__PURE__ */ new Map();
|
|
12866
13170
|
}
|
|
@@ -13201,8 +13505,8 @@ var recipeRegistry = new RecipeRegistry([
|
|
|
13201
13505
|
|
|
13202
13506
|
// src/commands/AgentHooksCommands.ts
|
|
13203
13507
|
import { homedir as homedir6 } from "node:os";
|
|
13204
|
-
import { join as
|
|
13205
|
-
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";
|
|
13206
13510
|
import { fileURLToPath as fileURLToPath4 } from "node:url";
|
|
13207
13511
|
init_project();
|
|
13208
13512
|
import { parse as parseToml3 } from "smol-toml";
|
|
@@ -13215,16 +13519,16 @@ function resolveTemplateRoot() {
|
|
|
13215
13519
|
try {
|
|
13216
13520
|
let dir = dirname15(fileURLToPath4(import.meta.url));
|
|
13217
13521
|
for (let i = 0; i < 8; i++) {
|
|
13218
|
-
candidates.push(
|
|
13522
|
+
candidates.push(join21(dir, "templates", "commonproject", "template"));
|
|
13219
13523
|
const parent = dirname15(dir);
|
|
13220
13524
|
if (parent === dir) break;
|
|
13221
13525
|
dir = parent;
|
|
13222
13526
|
}
|
|
13223
13527
|
} catch {
|
|
13224
13528
|
}
|
|
13225
|
-
candidates.push(
|
|
13529
|
+
candidates.push(join21(homedir6(), "code", "pjangler", "templates", "commonproject", "template"));
|
|
13226
13530
|
for (const c of candidates) {
|
|
13227
|
-
if (existsSync15(
|
|
13531
|
+
if (existsSync15(join21(c, ".agents", "hooks", "hooks.master.json"))) return c;
|
|
13228
13532
|
}
|
|
13229
13533
|
throw new Error(
|
|
13230
13534
|
"Could not locate the CommonProject template. Set PJANGLER_COMMONPROJECT_TEMPLATE to <repo>/templates/commonproject/template."
|
|
@@ -13250,8 +13554,8 @@ var CopyAgentHooksTree = class extends Command {
|
|
|
13250
13554
|
const created = [];
|
|
13251
13555
|
const skipped = [];
|
|
13252
13556
|
for (const { rel, dir } of items) {
|
|
13253
|
-
const src =
|
|
13254
|
-
const dest =
|
|
13557
|
+
const src = join21(templateRoot, rel);
|
|
13558
|
+
const dest = join21(this.context.targetDir, rel);
|
|
13255
13559
|
if (!existsSync15(src)) continue;
|
|
13256
13560
|
if (existsSync15(dest) && !this.context.force) {
|
|
13257
13561
|
skipped.push(rel);
|
|
@@ -13279,14 +13583,14 @@ var WireMiseAgentHooks = class _WireMiseAgentHooks extends Command {
|
|
|
13279
13583
|
if (!resolveAgentHooksLayer2()) {
|
|
13280
13584
|
return { success: true, message: this.formatMessage(AGENT_HOOKS_SKIP_MESSAGE) };
|
|
13281
13585
|
}
|
|
13282
|
-
const misePath =
|
|
13586
|
+
const misePath = join21(this.context.targetDir, "mise.toml");
|
|
13283
13587
|
if (!existsSync15(misePath)) {
|
|
13284
13588
|
return {
|
|
13285
13589
|
success: false,
|
|
13286
13590
|
message: "\u26A0\uFE0F No mise.toml found \u2014 run `pj add mise` first, then re-run."
|
|
13287
13591
|
};
|
|
13288
13592
|
}
|
|
13289
|
-
let content =
|
|
13593
|
+
let content = readFileSync17(misePath, "utf8");
|
|
13290
13594
|
if (content.includes(_WireMiseAgentHooks.MARKER)) {
|
|
13291
13595
|
return { success: true, message: this.formatMessage("\u2713 mise.toml already wired for agent-hooks") };
|
|
13292
13596
|
}
|
|
@@ -13509,7 +13813,7 @@ if __name__ == "__main__":
|
|
|
13509
13813
|
|
|
13510
13814
|
// src/commands/AddMiseCodegraphScript.ts
|
|
13511
13815
|
import { chmodSync as chmodSync5 } from "fs";
|
|
13512
|
-
import { join as
|
|
13816
|
+
import { join as join22 } from "path";
|
|
13513
13817
|
var AddMiseCodegraphScript = class extends Command {
|
|
13514
13818
|
async invoke() {
|
|
13515
13819
|
const filePath = ".mise/scripts/codegraph.sh";
|
|
@@ -13563,7 +13867,7 @@ fi
|
|
|
13563
13867
|
`;
|
|
13564
13868
|
this.writeFile(filePath, content);
|
|
13565
13869
|
if (!this.context.dryRun) {
|
|
13566
|
-
chmodSync5(
|
|
13870
|
+
chmodSync5(join22(this.context.targetDir, filePath), 493);
|
|
13567
13871
|
}
|
|
13568
13872
|
return {
|
|
13569
13873
|
success: true,
|
|
@@ -13601,18 +13905,18 @@ SECRET_KEY=""
|
|
|
13601
13905
|
// src/parity/index.ts
|
|
13602
13906
|
import { existsSync as existsSync16 } from "node:fs";
|
|
13603
13907
|
import { homedir as homedir7 } from "node:os";
|
|
13604
|
-
import { dirname as dirname16, join as
|
|
13908
|
+
import { dirname as dirname16, join as join23, resolve as resolve19 } from "node:path";
|
|
13605
13909
|
import { fileURLToPath as fileURLToPath5 } from "node:url";
|
|
13606
13910
|
function resolvePjanglerRoot2() {
|
|
13607
13911
|
let dir = dirname16(fileURLToPath5(import.meta.url));
|
|
13608
13912
|
while (dir !== dirname16(dir)) {
|
|
13609
|
-
if (existsSync16(
|
|
13913
|
+
if (existsSync16(join23(dir, "package.json")) && existsSync16(join23(dir, "templates", "commonproject", "copier.yml"))) return dir;
|
|
13610
13914
|
dir = dirname16(dir);
|
|
13611
13915
|
}
|
|
13612
|
-
return
|
|
13916
|
+
return resolve19(process.cwd());
|
|
13613
13917
|
}
|
|
13614
13918
|
function lifecycleContext(repoArg, dryRun, acceptRegistryMatches = false, overrides = {}) {
|
|
13615
|
-
const repoRoot =
|
|
13919
|
+
const repoRoot = resolve19(repoArg ?? process.cwd());
|
|
13616
13920
|
return {
|
|
13617
13921
|
...overrides,
|
|
13618
13922
|
targetDir: repoRoot,
|
|
@@ -13781,13 +14085,13 @@ init_version();
|
|
|
13781
14085
|
// src/project/info.ts
|
|
13782
14086
|
init_boardUrl();
|
|
13783
14087
|
init_project();
|
|
13784
|
-
import { readFileSync as
|
|
13785
|
-
import { join as
|
|
14088
|
+
import { readFileSync as readFileSync18 } from "node:fs";
|
|
14089
|
+
import { join as join24, resolve as resolve20 } from "node:path";
|
|
13786
14090
|
function readManifest2(root) {
|
|
13787
|
-
const path =
|
|
14091
|
+
const path = join24(root, ".project.json");
|
|
13788
14092
|
let manifest;
|
|
13789
14093
|
try {
|
|
13790
|
-
const value = JSON.parse(
|
|
14094
|
+
const value = JSON.parse(readFileSync18(path, "utf8"));
|
|
13791
14095
|
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("expected a JSON object");
|
|
13792
14096
|
manifest = value;
|
|
13793
14097
|
} catch (error) {
|
|
@@ -13827,8 +14131,8 @@ function readProjectInfo(input = {}) {
|
|
|
13827
14131
|
const info = {
|
|
13828
14132
|
ok: true,
|
|
13829
14133
|
project_id: projectId,
|
|
13830
|
-
repo_path:
|
|
13831
|
-
manifest_path:
|
|
14134
|
+
repo_path: resolve20(root),
|
|
14135
|
+
manifest_path: join24(resolve20(root), ".project.json"),
|
|
13832
14136
|
manifest,
|
|
13833
14137
|
registry: { location, status: "indexed" }
|
|
13834
14138
|
};
|
|
@@ -13849,27 +14153,27 @@ function readProjectInfo(input = {}) {
|
|
|
13849
14153
|
info.registry = { location, status: "not-indexed", message: "This project is not indexed. Run pj init --apply from its repository." };
|
|
13850
14154
|
return info;
|
|
13851
14155
|
}
|
|
13852
|
-
if (
|
|
14156
|
+
if (resolve20(indexed.repo_path) !== resolve20(root)) {
|
|
13853
14157
|
info.registry = { location, status: "stale", message: `The registry points to ${indexed.repo_path}; this manifest is at ${root}. Reindex the repository.` };
|
|
13854
14158
|
}
|
|
13855
14159
|
return info;
|
|
13856
14160
|
}
|
|
13857
14161
|
|
|
13858
14162
|
// src/describe/index.ts
|
|
13859
|
-
import { existsSync as existsSync17, readFileSync as
|
|
13860
|
-
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";
|
|
13861
14165
|
init_project();
|
|
13862
14166
|
|
|
13863
14167
|
// src/describe/activity.ts
|
|
13864
|
-
import { spawn as spawn2, spawnSync as
|
|
14168
|
+
import { spawn as spawn2, spawnSync as spawnSync13 } from "node:child_process";
|
|
13865
14169
|
import { statSync as statSync5 } from "node:fs";
|
|
13866
|
-
import { join as
|
|
14170
|
+
import { join as join25 } from "node:path";
|
|
13867
14171
|
var ACTIVE_WINDOW_SECONDS = 24 * 60 * 60;
|
|
13868
14172
|
var MAX_DIRTY_STATS = 500;
|
|
13869
14173
|
var GIT_TIMEOUT_MS = 5e3;
|
|
13870
14174
|
var GIT_MAX_BUFFER = 16 * 1024 * 1024;
|
|
13871
14175
|
function git3(repo, args) {
|
|
13872
|
-
const result2 =
|
|
14176
|
+
const result2 = spawnSync13("git", ["-C", repo, ...args], {
|
|
13873
14177
|
encoding: "utf8",
|
|
13874
14178
|
timeout: GIT_TIMEOUT_MS,
|
|
13875
14179
|
maxBuffer: GIT_MAX_BUFFER
|
|
@@ -13992,7 +14296,7 @@ function uncommittedSource(repo, paths) {
|
|
|
13992
14296
|
let newest = 0;
|
|
13993
14297
|
for (const path of paths.slice(0, MAX_DIRTY_STATS)) {
|
|
13994
14298
|
try {
|
|
13995
|
-
const mtime = Math.floor(statSync5(
|
|
14299
|
+
const mtime = Math.floor(statSync5(join25(repo, path)).mtimeMs / 1e3);
|
|
13996
14300
|
if (mtime > newest) newest = mtime;
|
|
13997
14301
|
} catch {
|
|
13998
14302
|
}
|
|
@@ -14101,7 +14405,7 @@ var CONFIG_FILES = [
|
|
|
14101
14405
|
];
|
|
14102
14406
|
function readJson(path) {
|
|
14103
14407
|
try {
|
|
14104
|
-
const parsed = JSON.parse(
|
|
14408
|
+
const parsed = JSON.parse(readFileSync19(path, "utf8"));
|
|
14105
14409
|
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : void 0;
|
|
14106
14410
|
} catch {
|
|
14107
14411
|
return void 0;
|
|
@@ -14123,7 +14427,7 @@ function describeType(repo) {
|
|
|
14123
14427
|
const evidence = [];
|
|
14124
14428
|
const note = (signal, file) => evidence.push(`${signal} (${file})`);
|
|
14125
14429
|
for (const marker of LANGUAGE_MARKERS) {
|
|
14126
|
-
if (!existsSync17(
|
|
14430
|
+
if (!existsSync17(join26(repo, marker.file))) continue;
|
|
14127
14431
|
if (!languages.includes(marker.language)) {
|
|
14128
14432
|
languages.push(marker.language);
|
|
14129
14433
|
note(marker.language, marker.file);
|
|
@@ -14137,9 +14441,9 @@ function describeType(repo) {
|
|
|
14137
14441
|
}
|
|
14138
14442
|
} catch {
|
|
14139
14443
|
}
|
|
14140
|
-
const pkg = readJson(
|
|
14444
|
+
const pkg = readJson(join26(repo, "package.json"));
|
|
14141
14445
|
if (pkg) {
|
|
14142
|
-
if (existsSync17(
|
|
14446
|
+
if (existsSync17(join26(repo, "tsconfig.json"))) {
|
|
14143
14447
|
const index = languages.indexOf("javascript");
|
|
14144
14448
|
if (index >= 0) languages.splice(index, 1);
|
|
14145
14449
|
if (!languages.includes("typescript")) {
|
|
@@ -14173,26 +14477,26 @@ function describeType(repo) {
|
|
|
14173
14477
|
["hermes-fleet-host", "agents/hermes"]
|
|
14174
14478
|
];
|
|
14175
14479
|
for (const [role, marker] of roleMarkers) {
|
|
14176
|
-
if (!existsSync17(
|
|
14480
|
+
if (!existsSync17(join26(repo, marker))) continue;
|
|
14177
14481
|
roles.push(role);
|
|
14178
14482
|
note(role, marker);
|
|
14179
14483
|
}
|
|
14180
14484
|
return { primaryLanguage: languages[0], languages, roles, evidence };
|
|
14181
14485
|
}
|
|
14182
14486
|
function describeIdentity(repo, registryPath) {
|
|
14183
|
-
const manifestPath =
|
|
14487
|
+
const manifestPath = join26(repo, ".project.json");
|
|
14184
14488
|
const manifest = readJson(manifestPath);
|
|
14185
14489
|
const drift = [];
|
|
14186
|
-
let
|
|
14490
|
+
let record3;
|
|
14187
14491
|
let registryReadable = true;
|
|
14188
14492
|
try {
|
|
14189
14493
|
const registry = loadProjectRegistry(registryPath);
|
|
14190
14494
|
const identity = manifest?.project_id ?? manifest?.project_slug;
|
|
14191
14495
|
const slug = typeof identity === "string" ? identity.toLowerCase() : void 0;
|
|
14192
|
-
const resolved =
|
|
14193
|
-
|
|
14496
|
+
const resolved = resolve21(repo);
|
|
14497
|
+
record3 = (slug ? registry.projects[slug] : void 0) ?? Object.values(registry.projects).find((project) => resolve21(project.repo_path) === resolved);
|
|
14194
14498
|
const statuses = registry.__registry_status;
|
|
14195
|
-
const status =
|
|
14499
|
+
const status = record3 && statuses?.[record3.slug];
|
|
14196
14500
|
if (status && status.status !== "ok") {
|
|
14197
14501
|
drift.push({ note: `canonical manifest is ${status.status}; displaying the last indexed record. ${status.error ?? ""}`, command: "pjangler doctor" });
|
|
14198
14502
|
}
|
|
@@ -14200,14 +14504,14 @@ function describeIdentity(repo, registryPath) {
|
|
|
14200
14504
|
registryReadable = false;
|
|
14201
14505
|
drift.push({ note: `registry unreadable: ${err instanceof Error ? err.message : String(err)}` });
|
|
14202
14506
|
}
|
|
14203
|
-
if (manifest && !
|
|
14507
|
+
if (manifest && !record3 && registryReadable) {
|
|
14204
14508
|
drift.push({ note: ".project.json exists but this repo is not in the pjangler registry" });
|
|
14205
14509
|
}
|
|
14206
|
-
if (
|
|
14207
|
-
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" });
|
|
14208
14512
|
}
|
|
14209
|
-
if (
|
|
14210
|
-
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" });
|
|
14211
14515
|
}
|
|
14212
14516
|
const manifestProvider = manifest?.ticket_provider;
|
|
14213
14517
|
const provider = manifestProvider ? {
|
|
@@ -14216,13 +14520,13 @@ function describeIdentity(repo, registryPath) {
|
|
|
14216
14520
|
identifier: manifestProvider.identifier,
|
|
14217
14521
|
board_id: manifestProvider.board_id,
|
|
14218
14522
|
state: manifestProvider.state
|
|
14219
|
-
} :
|
|
14220
|
-
if (manifestProvider &&
|
|
14523
|
+
} : record3?.ticket_provider;
|
|
14524
|
+
if (manifestProvider && record3) {
|
|
14221
14525
|
const fields = [
|
|
14222
|
-
["type", provider?.type,
|
|
14223
|
-
["workspace", provider?.workspace,
|
|
14224
|
-
["identifier", provider?.identifier,
|
|
14225
|
-
["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]
|
|
14226
14530
|
];
|
|
14227
14531
|
for (const [field2, fromManifest, fromRegistry] of fields) {
|
|
14228
14532
|
if ((fromManifest ?? "") === (fromRegistry ?? "")) continue;
|
|
@@ -14232,7 +14536,7 @@ function describeIdentity(repo, registryPath) {
|
|
|
14232
14536
|
}
|
|
14233
14537
|
}
|
|
14234
14538
|
const manifestAgents = manifest?.agents ?? {};
|
|
14235
|
-
const agents =
|
|
14539
|
+
const agents = record3 ? Object.entries(record3.agents).map(([name, agent]) => ({
|
|
14236
14540
|
name,
|
|
14237
14541
|
role: agent.role,
|
|
14238
14542
|
provisioningState: agent.provisioning_state,
|
|
@@ -14245,11 +14549,11 @@ function describeIdentity(repo, registryPath) {
|
|
|
14245
14549
|
}));
|
|
14246
14550
|
return {
|
|
14247
14551
|
manifest: Boolean(manifest),
|
|
14248
|
-
registered: Boolean(
|
|
14552
|
+
registered: Boolean(record3),
|
|
14249
14553
|
registryPath,
|
|
14250
|
-
slug:
|
|
14251
|
-
name:
|
|
14252
|
-
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,
|
|
14253
14557
|
ticketProvider: provider ? {
|
|
14254
14558
|
type: provider.type,
|
|
14255
14559
|
workspace: provider.workspace,
|
|
@@ -14273,12 +14577,12 @@ function describeSubsystems(repo, findings) {
|
|
|
14273
14577
|
const markers = SUBSYSTEM_MARKERS[metadata.id] ?? [];
|
|
14274
14578
|
const evidence = metadata.id === "notebook" ? (() => {
|
|
14275
14579
|
try {
|
|
14276
|
-
const manifest = JSON.parse(
|
|
14580
|
+
const manifest = JSON.parse(readFileSync19(join26(repo, ".project.json"), "utf8"));
|
|
14277
14581
|
return manifest.notebook && typeof manifest.notebook === "object" ? [".project.json#notebook"] : [];
|
|
14278
14582
|
} catch {
|
|
14279
14583
|
return [];
|
|
14280
14584
|
}
|
|
14281
|
-
})() : markers.filter((marker) => existsSync17(
|
|
14585
|
+
})() : markers.filter((marker) => existsSync17(join26(repo, marker)));
|
|
14282
14586
|
const rules = (byRecipe.get(metadata.id) ?? []).map((finding2) => ({
|
|
14283
14587
|
id: finding2.id,
|
|
14284
14588
|
title: finding2.title,
|
|
@@ -14296,8 +14600,8 @@ function describeSubsystems(repo, findings) {
|
|
|
14296
14600
|
function describeNotebook(repo, registryPath) {
|
|
14297
14601
|
try {
|
|
14298
14602
|
const registry = loadProjectRegistry(registryPath);
|
|
14299
|
-
const project = Object.values(registry.projects).find((entry) =>
|
|
14300
|
-
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;
|
|
14301
14605
|
const declared = Boolean(project?.notebook || manifest?.notebook && typeof manifest.notebook === "object");
|
|
14302
14606
|
if (!declared) return { declared: false, bindingState: null, notebookId: null, overviewNoteId: null, health: null, remoteCheck: "skip", captureAdmission: null };
|
|
14303
14607
|
const config = loadEffectiveNotebookConfig(repo, registryPath);
|
|
@@ -14315,7 +14619,7 @@ function describeNotebook(repo, registryPath) {
|
|
|
14315
14619
|
}
|
|
14316
14620
|
}
|
|
14317
14621
|
function describeConfigFiles(repo) {
|
|
14318
|
-
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 }));
|
|
14319
14623
|
}
|
|
14320
14624
|
function describeNextSteps(description, findings) {
|
|
14321
14625
|
const steps = [];
|
|
@@ -14393,7 +14697,7 @@ function describeNextSteps(description, findings) {
|
|
|
14393
14697
|
return steps;
|
|
14394
14698
|
}
|
|
14395
14699
|
async function describeProject(input = {}) {
|
|
14396
|
-
const repo =
|
|
14700
|
+
const repo = resolve21(input.repoArg ?? process.cwd());
|
|
14397
14701
|
if (!existsSync17(repo)) throw new Error(`Path does not exist: ${repo}`);
|
|
14398
14702
|
if (!statSync6(repo).isDirectory()) throw new Error(`Not a directory: ${repo}`);
|
|
14399
14703
|
const registryPath = input.registryPath ?? projectRegistryPath();
|
|
@@ -14582,7 +14886,7 @@ function validateExternalEffectConsent(input, options) {
|
|
|
14582
14886
|
return selected;
|
|
14583
14887
|
}
|
|
14584
14888
|
function resolveTargetDir(targetDir) {
|
|
14585
|
-
const dir =
|
|
14889
|
+
const dir = resolve22(targetDir ?? process.cwd());
|
|
14586
14890
|
if (!existsSync18(dir)) {
|
|
14587
14891
|
throw new Error(`Target directory does not exist: ${dir}`);
|
|
14588
14892
|
}
|
|
@@ -14594,7 +14898,7 @@ function resolveTargetDir(targetDir) {
|
|
|
14594
14898
|
function resolvePjanglerRoot3() {
|
|
14595
14899
|
let dir = dirname17(fileURLToPath6(import.meta.url));
|
|
14596
14900
|
while (dir !== dirname17(dir)) {
|
|
14597
|
-
if (existsSync18(
|
|
14901
|
+
if (existsSync18(join27(dir, "package.json")) && existsSync18(join27(dir, "templates", "commonproject", "copier.yml"))) {
|
|
14598
14902
|
return dir;
|
|
14599
14903
|
}
|
|
14600
14904
|
dir = dirname17(dir);
|
|
@@ -14889,12 +15193,12 @@ server.registerTool(
|
|
|
14889
15193
|
);
|
|
14890
15194
|
const pjanglerRoot = resolvePjanglerRoot3();
|
|
14891
15195
|
const projectSlug = validateSafePathSegment(input.projectSlug ?? slugify(input.projectName), "Project slug");
|
|
14892
|
-
const explicitTargetDir = input.targetDir ?
|
|
14893
|
-
const parentDir =
|
|
15196
|
+
const explicitTargetDir = input.targetDir ? resolve22(input.targetDir) : void 0;
|
|
15197
|
+
const parentDir = resolve22(input.parentDir ?? (explicitTargetDir ? dirname17(explicitTargetDir) : process.cwd()));
|
|
14894
15198
|
if (!existsSync18(parentDir) || !statSync7(parentDir).isDirectory()) throw new Error(`Parent directory does not exist: ${parentDir}`);
|
|
14895
15199
|
const targetDir = resolveContainedPath(
|
|
14896
15200
|
parentDir,
|
|
14897
|
-
explicitTargetDir ??
|
|
15201
|
+
explicitTargetDir ?? join27(parentDir, projectSlug),
|
|
14898
15202
|
"Bootstrap target"
|
|
14899
15203
|
);
|
|
14900
15204
|
const overwrite = input.overwrite ?? input.force ?? false;
|
|
@@ -15017,7 +15321,7 @@ server.registerTool(
|
|
|
15017
15321
|
registryPath: input.registryPath,
|
|
15018
15322
|
force: input.force ?? false,
|
|
15019
15323
|
overwrite: input.force ?? false,
|
|
15020
|
-
scaffold: !(input.targetDir && existsSync18(
|
|
15324
|
+
scaffold: !(input.targetDir && existsSync18(join27(resolve22(input.targetDir), ".git")))
|
|
15021
15325
|
});
|
|
15022
15326
|
if (!input.apply) return asText(publicCompositeProjectResponse(publicProjectPlan(plan), plan));
|
|
15023
15327
|
const preflight = await preflightProjectApply(plan, resolvePjanglerRoot3());
|